Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 62 additions & 1 deletion apps/dokploy/server/api/routers/preview-deployment.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
createPreviewDeploymentFromImage,
findApplicationById,
findPreviewDeploymentById,
findPreviewDeploymentsByApplicationId,
Expand All @@ -7,7 +8,10 @@ import {
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { apiFindAllByApplication } from "@/server/db/schema";
import {
apiFindAllByApplication,
apiCreatePreviewDeploymentFromImage,
} from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
Expand Down Expand Up @@ -115,4 +119,61 @@ export const previewDeploymentRouter = createTRPCRouter({
);
return true;
}),
deployFromImage: protectedProcedure
.input(apiCreatePreviewDeploymentFromImage)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}

if (!application.isPreviewDeploymentsActive) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Preview deployments are not enabled for this application",
});
}

const { previewDeployment, previewDomain } =
await createPreviewDeploymentFromImage(input);

const jobData: DeploymentJob = {
applicationId: input.applicationId,
titleLog: `Deploy image: ${input.dockerImage}`,
descriptionLog: "",
type: "deploy",
applicationType: "application-preview",
previewDeploymentId: previewDeployment.previewDeploymentId,
server: !!application.serverId,
};

if (IS_CLOUD && application.serverId) {
jobData.serverId = application.serverId;
deploy(jobData).catch((error) => {
console.error("Background deployment failed:", error);
});
return {
previewDeploymentId: previewDeployment.previewDeploymentId,
previewDomain,
};
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
return {
previewDeploymentId: previewDeployment.previewDeploymentId,
previewDomain,
};
}),
});
11 changes: 10 additions & 1 deletion packages/server/src/db/schema/preview-deployments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const previewDeployments = pgTable("preview_deployments", {
domainId: text("domainId").references(() => domains.domainId, {
onDelete: "cascade",
}),
dockerImage: text("dockerImage"),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
Expand Down Expand Up @@ -67,8 +68,16 @@ export const apiCreatePreviewDeployment = createSchema
pullRequestNumber: true,
pullRequestURL: true,
pullRequestTitle: true,
dockerImage: true,
})
.extend({
applicationId: z.string().min(1),
// deploymentId: z.string().min(1),
});

export const apiCreatePreviewDeploymentFromImage = z.object({
applicationId: z.string().min(1),
dockerImage: z.string().min(1),
pullRequestNumber: z.string().optional(),
pullRequestTitle: z.string().optional(),
pullRequestURL: z.string().optional(),
});
26 changes: 21 additions & 5 deletions packages/server/src/services/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,10 +430,19 @@ export const deployPreviewApplication = async ({
branch: previewDeployment.branch,
});
command += await getBuildCommand(application);
} else if (application.sourceType === "docker") {
if (previewDeployment.dockerImage) {
application.dockerImage = previewDeployment.dockerImage;
}
command += await buildRemoteDocker(application);
}

if (command !== "set -e;") {
const buildServerId =
application.buildServerId || application.serverId;
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (application.serverId) {
await execAsyncRemote(application.serverId, commandWithLog);
if (buildServerId) {
await execAsyncRemote(buildServerId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
Expand Down Expand Up @@ -541,10 +550,17 @@ export const rebuildPreviewApplication = async ({
application.rollbackRegistry = null;
application.registry = null;

const serverId = application.serverId;
const serverId = application.buildServerId || application.serverId;
let command = "set -e;";
// Only rebuild, don't clone repository
command += await getBuildCommand(application);
if (application.sourceType === "docker") {
if (previewDeployment.dockerImage) {
application.dockerImage = previewDeployment.dockerImage;
}
command += await buildRemoteDocker(application);
} else {
// Only rebuild, don't clone repository
command += await getBuildCommand(application);
}
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (serverId) {
await execAsyncRemote(serverId, commandWithLog);
Expand Down
91 changes: 91 additions & 0 deletions packages/server/src/services/preview-deployment.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { db } from "@dokploy/server/db";
import {
type apiCreatePreviewDeployment,
type apiCreatePreviewDeploymentFromImage,
deployments,
organization,
previewDeployments,
Expand Down Expand Up @@ -231,6 +232,96 @@ export const findPreviewDeploymentByApplicationId = async (
return previewDeploymentResult;
};

export const createPreviewDeploymentFromImage = async (
schema: typeof apiCreatePreviewDeploymentFromImage._type,
) => {
const application = await findApplicationById(schema.applicationId);
const appName = `preview-${application.appName}-${generatePassword(6)}`;

const org = await db.query.organization.findFirst({
where: eq(organization.id, application.environment.project.organizationId),
});
const generateDomain = await generateWildcardDomain(
application.previewWildcard || "*.traefik.me",
appName,
application.server?.ipAddress || "",
org?.ownerId || "",
);

const previewDomain = `${application.previewHttps ? "https" : "http"}://${generateDomain}`;
let pullRequestCommentId = "";

// If a GitHub provider is configured and PR number is provided, post a comment
if (application.github && schema.pullRequestNumber) {
try {
const octokit = authGithub(application.github as Github);
const runningComment = getIssueComment(
application.name,
"initializing",
previewDomain,
);
const issue = await octokit.rest.issues.createComment({
owner: application.owner || "",
repo: application.repository || "",
issue_number: Number.parseInt(schema.pullRequestNumber),
body: `### Dokploy Preview Deployment\n\n${runningComment}`,
});
pullRequestCommentId = `${issue.data.id}`;
} catch (_error) {
// GitHub comment is optional for image-based previews
}
}

const previewDeployment = await db
.insert(previewDeployments)
.values({
applicationId: schema.applicationId,
appName: appName,
dockerImage: schema.dockerImage,
branch: "docker-image",
pullRequestId: schema.pullRequestNumber || "0",
pullRequestNumber: schema.pullRequestNumber || "0",
pullRequestURL: schema.pullRequestURL || "",
pullRequestTitle: schema.pullRequestTitle || `Preview: ${schema.dockerImage}`,
pullRequestCommentId: pullRequestCommentId || "0",
})
.returning()
.then((value) => value[0]);

if (!previewDeployment) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the preview deployment",
});
}

const newDomain = await createDomain({
host: generateDomain,
path: application.previewPath,
port: application.previewPort,
https: application.previewHttps,
certificateType: application.previewCertificateType,
customCertResolver: application.previewCustomCertResolver,
domainType: "preview",
previewDeploymentId: previewDeployment.previewDeploymentId,
});

application.appName = appName;
await manageDomain(application, newDomain);

await db
.update(previewDeployments)
.set({ domainId: newDomain.domainId })
.where(
eq(
previewDeployments.previewDeploymentId,
previewDeployment.previewDeploymentId,
),
);

return { previewDeployment, previewDomain };
};

const generateWildcardDomain = async (
baseDomain: string,
appName: string,
Expand Down