diff --git a/apps/dokploy/server/api/routers/preview-deployment.ts b/apps/dokploy/server/api/routers/preview-deployment.ts index 0c325a9c68..769aa20e18 100644 --- a/apps/dokploy/server/api/routers/preview-deployment.ts +++ b/apps/dokploy/server/api/routers/preview-deployment.ts @@ -1,4 +1,5 @@ import { + createPreviewDeploymentFromImage, findApplicationById, findPreviewDeploymentById, findPreviewDeploymentsByApplicationId, @@ -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"; @@ -115,4 +119,47 @@ 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", + }); + } + + const previewDeployment = await createPreviewDeploymentFromImage(input); + + const jobData: DeploymentJob = { + applicationId: input.applicationId, + titleLog: `Deploy Docker 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 previewDeployment; + } + await myQueue.add( + "deployments", + { ...jobData }, + { + removeOnComplete: true, + removeOnFail: true, + }, + ); + return previewDeployment; + }), }); diff --git a/packages/server/src/db/schema/preview-deployments.ts b/packages/server/src/db/schema/preview-deployments.ts index 3bdab2c250..54e62e5d12 100644 --- a/packages/server/src/db/schema/preview-deployments.ts +++ b/packages/server/src/db/schema/preview-deployments.ts @@ -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()), @@ -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(), +}); diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts index 458ab34f5b..8232beb8ff 100644 --- a/packages/server/src/services/application.ts +++ b/packages/server/src/services/application.ts @@ -418,10 +418,14 @@ export const deployPreviewApplication = async ({ application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`; application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`; application.rollbackActive = false; - application.buildRegistry = null; + if (!application.buildServerId || application.buildServerId === application.serverId) { + application.buildRegistry = null; + } application.rollbackRegistry = null; application.registry = null; + const buildServerId = + application.buildServerId || application.serverId; let command = "set -e;"; if (application.sourceType === "github") { command += await cloneGithubRepository({ @@ -430,10 +434,17 @@ 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 (application.sourceType === "github" || application.sourceType === "docker") { 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); } @@ -537,14 +548,22 @@ export const rebuildPreviewApplication = async ({ application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`; application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`; application.rollbackActive = false; - application.buildRegistry = null; + if (!application.buildServerId || application.buildServerId === application.serverId) { + application.buildRegistry = null; + } 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 { + command += await getBuildCommand(application); + } const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`; if (serverId) { await execAsyncRemote(serverId, commandWithLog); diff --git a/packages/server/src/services/deployment.ts b/packages/server/src/services/deployment.ts index 24e1590a98..0b44d928df 100644 --- a/packages/server/src/services/deployment.ts +++ b/packages/server/src/services/deployment.ts @@ -161,6 +161,10 @@ export const createDeploymentPreview = async ( deployment.previewDeploymentId, ); try { + const buildServerId = + previewDeployment?.application?.buildServerId || + previewDeployment?.application?.serverId; + await removeLastTenDeployments( deployment.previewDeploymentId, "previewDeployment", @@ -168,15 +172,13 @@ export const createDeploymentPreview = async ( ); const appName = `${previewDeployment.appName}`; - const { LOGS_PATH } = paths(!!previewDeployment?.application?.serverId); + const { LOGS_PATH } = paths(!!buildServerId); const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss"); const fileName = `${appName}-${formattedDateTime}.log`; const logFilePath = path.join(LOGS_PATH, appName, fileName); - if (previewDeployment?.application?.serverId) { - const server = await findServerById( - previewDeployment?.application?.serverId, - ); + if (buildServerId) { + const server = await findServerById(buildServerId); const command = ` mkdir -p ${LOGS_PATH}/${appName}; @@ -200,6 +202,10 @@ export const createDeploymentPreview = async ( description: deployment.description || "", previewDeploymentId: deployment.previewDeploymentId, startedAt: new Date().toISOString(), + ...(previewDeployment?.application?.buildServerId && { + buildServerId: + previewDeployment.application.buildServerId, + }), }) .returning(); if (deploymentCreate.length === 0 || !deploymentCreate[0]) { diff --git a/packages/server/src/services/preview-deployment.ts b/packages/server/src/services/preview-deployment.ts index 1ece3bc539..1f63a73371 100644 --- a/packages/server/src/services/preview-deployment.ts +++ b/packages/server/src/services/preview-deployment.ts @@ -1,6 +1,7 @@ import { db } from "@dokploy/server/db"; import { type apiCreatePreviewDeployment, + type apiCreatePreviewDeploymentFromImage, deployments, organization, previewDeployments, @@ -97,7 +98,7 @@ export const removePreviewDeployment = async (previewDeploymentId: string) => { }); } }; -// testing-tesoitnmg-ddq0ul-preview-ihl44o + export const updatePreviewDeployment = async ( previewDeploymentId: string, previewDeploymentData: Partial, @@ -129,44 +130,39 @@ export const findPreviewDeploymentsByApplicationId = async ( return deploymentsList; }; -export const createPreviewDeployment = async ( - schema: typeof apiCreatePreviewDeployment._type, -) => { - const application = await findApplicationById(schema.applicationId); +/** + * Generates appName, resolves organization and wildcard domain for a preview. + * Returns all data needed before DB insert (callers may need `host` for GitHub comments). + */ +const preparePreview = async (applicationId: string) => { + const application = await findApplicationById(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( + const host = await generateWildcardDomain( application.previewWildcard || "*.traefik.me", appName, application.server?.ipAddress || "", org?.ownerId || "", ); - const octokit = authGithub(application?.github as Github); - - const runningComment = getIssueComment( - application.name, - "initializing", - `${application.previewHttps ? "https" : "http"}://${generateDomain}`, - ); - - 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}`, - }); + return { application, appName, host }; +}; +/** + * Inserts a preview deployment record and sets up domain + Traefik routing. + */ +const insertAndConfigurePreview = async ( + application: Awaited>, + appName: string, + host: string, + values: typeof previewDeployments.$inferInsert, +) => { const previewDeployment = await db .insert(previewDeployments) - .values({ - ...schema, - appName: appName, - pullRequestCommentId: `${issue.data.id}`, - }) + .values(values) .returning() .then((value) => value[0]); @@ -178,7 +174,7 @@ export const createPreviewDeployment = async ( } const newDomain = await createDomain({ - host: generateDomain, + host, path: application.previewPath, port: application.previewPort, https: application.previewHttps, @@ -189,14 +185,11 @@ export const createPreviewDeployment = async ( }); application.appName = appName; - await manageDomain(application, newDomain); await db .update(previewDeployments) - .set({ - domainId: newDomain.domainId, - }) + .set({ domainId: newDomain.domainId }) .where( eq( previewDeployments.previewDeploymentId, @@ -207,6 +200,56 @@ export const createPreviewDeployment = async ( return previewDeployment; }; +export const createPreviewDeployment = async ( + schema: typeof apiCreatePreviewDeployment._type, +) => { + const { application, appName, host } = await preparePreview( + schema.applicationId, + ); + + const octokit = authGithub(application?.github as Github); + + const runningComment = getIssueComment( + application.name, + "initializing", + `${application.previewHttps ? "https" : "http"}://${host}`, + ); + + 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}`, + }); + + return insertAndConfigurePreview(application, appName, host, { + ...schema, + appName, + pullRequestCommentId: `${issue.data.id}`, + }); +}; + +export const createPreviewDeploymentFromImage = async ( + schema: typeof apiCreatePreviewDeploymentFromImage._type, +) => { + const { application, appName, host } = await preparePreview( + schema.applicationId, + ); + + return insertAndConfigurePreview(application, appName, host, { + applicationId: schema.applicationId, + appName, + branch: "main", + pullRequestId: schema.pullRequestNumber || "", + pullRequestNumber: schema.pullRequestNumber || "", + pullRequestURL: schema.pullRequestURL || "", + pullRequestTitle: + schema.pullRequestTitle || `Docker image: ${schema.dockerImage}`, + pullRequestCommentId: "", + dockerImage: schema.dockerImage, + }); +}; + export const findPreviewDeploymentsByPullRequestId = async ( pullRequestId: string, ) => { diff --git a/packages/server/src/utils/builders/index.ts b/packages/server/src/utils/builders/index.ts index 5bf3a790e0..e32a9cc479 100644 --- a/packages/server/src/utils/builders/index.ts +++ b/packages/server/src/utils/builders/index.ts @@ -182,7 +182,7 @@ export const mechanizeDockerContainer = async ( }); } catch (error) { console.log(error); - await docker.createService(settings); + await docker.createService(settings.authconfig, settings); } };