mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
- Added logic to create a rollback entry if the application has an active rollback during the deployment process. - Enhanced the rollback handling by determining the appropriate tag image based on the application's source type (docker or app name).
691 lines
19 KiB
TypeScript
691 lines
19 KiB
TypeScript
import { docker } from "@dokploy/server/constants";
|
|
import { db } from "@dokploy/server/db";
|
|
import {
|
|
type apiCreateApplication,
|
|
applications,
|
|
buildAppName,
|
|
} from "@dokploy/server/db/schema";
|
|
import { getAdvancedStats } from "@dokploy/server/monitoring/utils";
|
|
import {
|
|
buildApplication,
|
|
getBuildCommand,
|
|
mechanizeDockerContainer,
|
|
} from "@dokploy/server/utils/builders";
|
|
import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error";
|
|
import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success";
|
|
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
|
import {
|
|
cloneBitbucketRepository,
|
|
getBitbucketCloneCommand,
|
|
} from "@dokploy/server/utils/providers/bitbucket";
|
|
import {
|
|
buildDocker,
|
|
buildRemoteDocker,
|
|
} from "@dokploy/server/utils/providers/docker";
|
|
import {
|
|
cloneGitRepository,
|
|
getCustomGitCloneCommand,
|
|
} from "@dokploy/server/utils/providers/git";
|
|
import {
|
|
cloneGiteaRepository,
|
|
getGiteaCloneCommand,
|
|
} from "@dokploy/server/utils/providers/gitea";
|
|
import {
|
|
cloneGithubRepository,
|
|
getGithubCloneCommand,
|
|
} from "@dokploy/server/utils/providers/github";
|
|
import {
|
|
cloneGitlabRepository,
|
|
getGitlabCloneCommand,
|
|
} from "@dokploy/server/utils/providers/gitlab";
|
|
import { createTraefikConfig } from "@dokploy/server/utils/traefik/application";
|
|
import { TRPCError } from "@trpc/server";
|
|
import { eq } from "drizzle-orm";
|
|
import { encodeBase64 } from "../utils/docker/utils";
|
|
import { getDokployUrl } from "./admin";
|
|
import {
|
|
createDeployment,
|
|
createDeploymentPreview,
|
|
updateDeploymentStatus,
|
|
} from "./deployment";
|
|
import { type Domain, getDomainHost } from "./domain";
|
|
import {
|
|
createPreviewDeploymentComment,
|
|
getIssueComment,
|
|
issueCommentExists,
|
|
updateIssueComment,
|
|
} from "./github";
|
|
import {
|
|
findPreviewDeploymentById,
|
|
updatePreviewDeployment,
|
|
} from "./preview-deployment";
|
|
import { validUniqueServerAppName } from "./project";
|
|
import { createRollback } from "./rollbacks";
|
|
export type Application = typeof applications.$inferSelect;
|
|
|
|
export const createApplication = async (
|
|
input: typeof apiCreateApplication._type,
|
|
) => {
|
|
const appName = buildAppName("app", input.appName);
|
|
|
|
const valid = await validUniqueServerAppName(appName);
|
|
if (!valid) {
|
|
throw new TRPCError({
|
|
code: "CONFLICT",
|
|
message: "Application with this 'AppName' already exists",
|
|
});
|
|
}
|
|
|
|
return await db.transaction(async (tx) => {
|
|
const newApplication = await tx
|
|
.insert(applications)
|
|
.values({
|
|
...input,
|
|
appName,
|
|
})
|
|
.returning()
|
|
.then((value) => value[0]);
|
|
|
|
if (!newApplication) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "Error creating the application",
|
|
});
|
|
}
|
|
|
|
if (process.env.NODE_ENV === "development") {
|
|
createTraefikConfig(newApplication.appName);
|
|
}
|
|
|
|
return newApplication;
|
|
});
|
|
};
|
|
|
|
export const findApplicationById = async (applicationId: string) => {
|
|
const application = await db.query.applications.findFirst({
|
|
where: eq(applications.applicationId, applicationId),
|
|
with: {
|
|
project: true,
|
|
domains: true,
|
|
deployments: true,
|
|
mounts: true,
|
|
redirects: true,
|
|
security: true,
|
|
ports: true,
|
|
registry: true,
|
|
gitlab: true,
|
|
github: true,
|
|
bitbucket: true,
|
|
gitea: true,
|
|
server: true,
|
|
previewDeployments: true,
|
|
},
|
|
});
|
|
if (!application) {
|
|
throw new TRPCError({
|
|
code: "NOT_FOUND",
|
|
message: "Application not found",
|
|
});
|
|
}
|
|
return application;
|
|
};
|
|
|
|
export const findApplicationByName = async (appName: string) => {
|
|
const application = await db.query.applications.findFirst({
|
|
where: eq(applications.appName, appName),
|
|
});
|
|
|
|
return application;
|
|
};
|
|
|
|
export const updateApplication = async (
|
|
applicationId: string,
|
|
applicationData: Partial<Application>,
|
|
) => {
|
|
const { appName, ...rest } = applicationData;
|
|
const application = await db
|
|
.update(applications)
|
|
.set({
|
|
...rest,
|
|
})
|
|
.where(eq(applications.applicationId, applicationId))
|
|
.returning();
|
|
|
|
return application[0];
|
|
};
|
|
|
|
export const updateApplicationStatus = async (
|
|
applicationId: string,
|
|
applicationStatus: Application["applicationStatus"],
|
|
) => {
|
|
const application = await db
|
|
.update(applications)
|
|
.set({
|
|
applicationStatus: applicationStatus,
|
|
})
|
|
.where(eq(applications.applicationId, applicationId))
|
|
.returning();
|
|
|
|
return application;
|
|
};
|
|
|
|
export const deployApplication = async ({
|
|
applicationId,
|
|
titleLog = "Manual deployment",
|
|
descriptionLog = "",
|
|
}: {
|
|
applicationId: string;
|
|
titleLog: string;
|
|
descriptionLog: string;
|
|
}) => {
|
|
const application = await findApplicationById(applicationId);
|
|
|
|
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.projectId}/services/application/${application.applicationId}?tab=deployments`;
|
|
const deployment = await createDeployment({
|
|
applicationId: applicationId,
|
|
title: titleLog,
|
|
description: descriptionLog,
|
|
});
|
|
|
|
try {
|
|
if (application.sourceType === "github") {
|
|
await cloneGithubRepository({
|
|
...application,
|
|
logPath: deployment.logPath,
|
|
});
|
|
await buildApplication(application, deployment.logPath);
|
|
} else if (application.sourceType === "gitlab") {
|
|
await cloneGitlabRepository(application, deployment.logPath);
|
|
await buildApplication(application, deployment.logPath);
|
|
} else if (application.sourceType === "gitea") {
|
|
await cloneGiteaRepository(application, deployment.logPath);
|
|
await buildApplication(application, deployment.logPath);
|
|
} else if (application.sourceType === "bitbucket") {
|
|
await cloneBitbucketRepository(application, deployment.logPath);
|
|
await buildApplication(application, deployment.logPath);
|
|
} else if (application.sourceType === "docker") {
|
|
await buildDocker(application, deployment.logPath);
|
|
} else if (application.sourceType === "git") {
|
|
await cloneGitRepository(application, deployment.logPath);
|
|
await buildApplication(application, deployment.logPath);
|
|
} else if (application.sourceType === "drop") {
|
|
await buildApplication(application, deployment.logPath);
|
|
}
|
|
|
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
|
await updateApplicationStatus(applicationId, "done");
|
|
|
|
if (application.rollbackActive) {
|
|
const tagImage =
|
|
application.sourceType === "docker"
|
|
? application.dockerImage
|
|
: application.appName;
|
|
await createRollback({
|
|
appName: tagImage || "",
|
|
deploymentId: deployment.deploymentId,
|
|
});
|
|
}
|
|
|
|
await sendBuildSuccessNotifications({
|
|
projectName: application.project.name,
|
|
applicationName: application.name,
|
|
applicationType: "application",
|
|
buildLink,
|
|
organizationId: application.project.organizationId,
|
|
domains: application.domains,
|
|
});
|
|
} catch (error) {
|
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
|
await updateApplicationStatus(applicationId, "error");
|
|
await sendBuildErrorNotifications({
|
|
projectName: application.project.name,
|
|
applicationName: application.name,
|
|
applicationType: "application",
|
|
// @ts-ignore
|
|
errorMessage: error?.message || "Error building",
|
|
buildLink,
|
|
organizationId: application.project.organizationId,
|
|
});
|
|
|
|
throw error;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
export const rebuildApplication = async ({
|
|
applicationId,
|
|
titleLog = "Rebuild deployment",
|
|
descriptionLog = "",
|
|
}: {
|
|
applicationId: string;
|
|
titleLog: string;
|
|
descriptionLog: string;
|
|
}) => {
|
|
const application = await findApplicationById(applicationId);
|
|
|
|
const deployment = await createDeployment({
|
|
applicationId: applicationId,
|
|
title: titleLog,
|
|
description: descriptionLog,
|
|
});
|
|
|
|
try {
|
|
if (application.sourceType === "github") {
|
|
await buildApplication(application, deployment.logPath);
|
|
} else if (application.sourceType === "gitlab") {
|
|
await buildApplication(application, deployment.logPath);
|
|
} else if (application.sourceType === "bitbucket") {
|
|
await buildApplication(application, deployment.logPath);
|
|
} else if (application.sourceType === "docker") {
|
|
await buildDocker(application, deployment.logPath);
|
|
} else if (application.sourceType === "git") {
|
|
await buildApplication(application, deployment.logPath);
|
|
} else if (application.sourceType === "drop") {
|
|
await buildApplication(application, deployment.logPath);
|
|
}
|
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
|
await updateApplicationStatus(applicationId, "done");
|
|
} catch (error) {
|
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
|
await updateApplicationStatus(applicationId, "error");
|
|
throw error;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
export const deployRemoteApplication = async ({
|
|
applicationId,
|
|
titleLog = "Manual deployment",
|
|
descriptionLog = "",
|
|
}: {
|
|
applicationId: string;
|
|
titleLog: string;
|
|
descriptionLog: string;
|
|
}) => {
|
|
const application = await findApplicationById(applicationId);
|
|
|
|
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.projectId}/services/application/${application.applicationId}?tab=deployments`;
|
|
const deployment = await createDeployment({
|
|
applicationId: applicationId,
|
|
title: titleLog,
|
|
description: descriptionLog,
|
|
});
|
|
|
|
try {
|
|
if (application.serverId) {
|
|
let command = "set -e;";
|
|
if (application.sourceType === "github") {
|
|
command += await getGithubCloneCommand({
|
|
...application,
|
|
serverId: application.serverId,
|
|
logPath: deployment.logPath,
|
|
});
|
|
} else if (application.sourceType === "gitlab") {
|
|
command += await getGitlabCloneCommand(application, deployment.logPath);
|
|
} else if (application.sourceType === "bitbucket") {
|
|
command += await getBitbucketCloneCommand(
|
|
application,
|
|
deployment.logPath,
|
|
);
|
|
} else if (application.sourceType === "gitea") {
|
|
command += await getGiteaCloneCommand(application, deployment.logPath);
|
|
} else if (application.sourceType === "git") {
|
|
command += await getCustomGitCloneCommand(
|
|
application,
|
|
deployment.logPath,
|
|
);
|
|
} else if (application.sourceType === "docker") {
|
|
command += await buildRemoteDocker(application, deployment.logPath);
|
|
}
|
|
|
|
if (application.sourceType !== "docker") {
|
|
command += getBuildCommand(application, deployment.logPath);
|
|
}
|
|
await execAsyncRemote(application.serverId, command);
|
|
await mechanizeDockerContainer(application);
|
|
}
|
|
|
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
|
await updateApplicationStatus(applicationId, "done");
|
|
|
|
if (application.rollbackActive) {
|
|
const tagImage =
|
|
application.sourceType === "docker"
|
|
? application.dockerImage
|
|
: application.appName;
|
|
await createRollback({
|
|
appName: tagImage || "",
|
|
deploymentId: deployment.deploymentId,
|
|
});
|
|
}
|
|
|
|
await sendBuildSuccessNotifications({
|
|
projectName: application.project.name,
|
|
applicationName: application.name,
|
|
applicationType: "application",
|
|
buildLink,
|
|
organizationId: application.project.organizationId,
|
|
domains: application.domains,
|
|
});
|
|
} catch (error) {
|
|
// @ts-ignore
|
|
const encodedContent = encodeBase64(error?.message);
|
|
|
|
await execAsyncRemote(
|
|
application.serverId,
|
|
`
|
|
echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath};
|
|
echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};
|
|
echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`,
|
|
);
|
|
|
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
|
await updateApplicationStatus(applicationId, "error");
|
|
await sendBuildErrorNotifications({
|
|
projectName: application.project.name,
|
|
applicationName: application.name,
|
|
applicationType: "application",
|
|
// @ts-ignore
|
|
errorMessage: error?.message || "Error building",
|
|
buildLink,
|
|
organizationId: application.project.organizationId,
|
|
});
|
|
|
|
throw error;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
export const deployPreviewApplication = async ({
|
|
applicationId,
|
|
titleLog = "Preview Deployment",
|
|
descriptionLog = "",
|
|
previewDeploymentId,
|
|
}: {
|
|
applicationId: string;
|
|
titleLog: string;
|
|
descriptionLog: string;
|
|
previewDeploymentId: string;
|
|
}) => {
|
|
const application = await findApplicationById(applicationId);
|
|
|
|
const deployment = await createDeploymentPreview({
|
|
title: titleLog,
|
|
description: descriptionLog,
|
|
previewDeploymentId: previewDeploymentId,
|
|
});
|
|
|
|
const previewDeployment =
|
|
await findPreviewDeploymentById(previewDeploymentId);
|
|
|
|
await updatePreviewDeployment(previewDeploymentId, {
|
|
createdAt: new Date().toISOString(),
|
|
});
|
|
|
|
const previewDomain = getDomainHost(previewDeployment?.domain as Domain);
|
|
const issueParams = {
|
|
owner: application?.owner || "",
|
|
repository: application?.repository || "",
|
|
issue_number: previewDeployment.pullRequestNumber,
|
|
comment_id: Number.parseInt(previewDeployment.pullRequestCommentId),
|
|
githubId: application?.githubId || "",
|
|
};
|
|
try {
|
|
const commentExists = await issueCommentExists({
|
|
...issueParams,
|
|
});
|
|
if (!commentExists) {
|
|
const result = await createPreviewDeploymentComment({
|
|
...issueParams,
|
|
previewDomain,
|
|
appName: previewDeployment.appName,
|
|
githubId: application?.githubId || "",
|
|
previewDeploymentId,
|
|
});
|
|
|
|
if (!result) {
|
|
throw new TRPCError({
|
|
code: "NOT_FOUND",
|
|
message: "Pull request comment not found",
|
|
});
|
|
}
|
|
|
|
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
|
|
}
|
|
const buildingComment = getIssueComment(
|
|
application.name,
|
|
"running",
|
|
previewDomain,
|
|
);
|
|
await updateIssueComment({
|
|
...issueParams,
|
|
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
|
});
|
|
application.appName = previewDeployment.appName;
|
|
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
|
application.buildArgs = application.previewBuildArgs;
|
|
|
|
if (application.sourceType === "github") {
|
|
await cloneGithubRepository({
|
|
...application,
|
|
appName: previewDeployment.appName,
|
|
branch: previewDeployment.branch,
|
|
logPath: deployment.logPath,
|
|
});
|
|
await buildApplication(application, deployment.logPath);
|
|
}
|
|
const successComment = getIssueComment(
|
|
application.name,
|
|
"success",
|
|
previewDomain,
|
|
);
|
|
await updateIssueComment({
|
|
...issueParams,
|
|
body: `### Dokploy Preview Deployment\n\n${successComment}`,
|
|
});
|
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
|
await updatePreviewDeployment(previewDeploymentId, {
|
|
previewStatus: "done",
|
|
});
|
|
} catch (error) {
|
|
const comment = getIssueComment(application.name, "error", previewDomain);
|
|
await updateIssueComment({
|
|
...issueParams,
|
|
body: `### Dokploy Preview Deployment\n\n${comment}`,
|
|
});
|
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
|
await updatePreviewDeployment(previewDeploymentId, {
|
|
previewStatus: "error",
|
|
});
|
|
throw error;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
export const deployRemotePreviewApplication = async ({
|
|
applicationId,
|
|
titleLog = "Preview Deployment",
|
|
descriptionLog = "",
|
|
previewDeploymentId,
|
|
}: {
|
|
applicationId: string;
|
|
titleLog: string;
|
|
descriptionLog: string;
|
|
previewDeploymentId: string;
|
|
}) => {
|
|
const application = await findApplicationById(applicationId);
|
|
|
|
const deployment = await createDeploymentPreview({
|
|
title: titleLog,
|
|
description: descriptionLog,
|
|
previewDeploymentId: previewDeploymentId,
|
|
});
|
|
|
|
const previewDeployment =
|
|
await findPreviewDeploymentById(previewDeploymentId);
|
|
|
|
await updatePreviewDeployment(previewDeploymentId, {
|
|
createdAt: new Date().toISOString(),
|
|
});
|
|
|
|
const previewDomain = getDomainHost(previewDeployment?.domain as Domain);
|
|
const issueParams = {
|
|
owner: application?.owner || "",
|
|
repository: application?.repository || "",
|
|
issue_number: previewDeployment.pullRequestNumber,
|
|
comment_id: Number.parseInt(previewDeployment.pullRequestCommentId),
|
|
githubId: application?.githubId || "",
|
|
};
|
|
try {
|
|
const commentExists = await issueCommentExists({
|
|
...issueParams,
|
|
});
|
|
if (!commentExists) {
|
|
const result = await createPreviewDeploymentComment({
|
|
...issueParams,
|
|
previewDomain,
|
|
appName: previewDeployment.appName,
|
|
githubId: application?.githubId || "",
|
|
previewDeploymentId,
|
|
});
|
|
|
|
if (!result) {
|
|
throw new TRPCError({
|
|
code: "NOT_FOUND",
|
|
message: "Pull request comment not found",
|
|
});
|
|
}
|
|
|
|
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
|
|
}
|
|
const buildingComment = getIssueComment(
|
|
application.name,
|
|
"running",
|
|
previewDomain,
|
|
);
|
|
await updateIssueComment({
|
|
...issueParams,
|
|
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
|
});
|
|
application.appName = previewDeployment.appName;
|
|
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
|
application.buildArgs = application.previewBuildArgs;
|
|
|
|
if (application.serverId) {
|
|
let command = "set -e;";
|
|
if (application.sourceType === "github") {
|
|
command += await getGithubCloneCommand({
|
|
...application,
|
|
appName: previewDeployment.appName,
|
|
branch: previewDeployment.branch,
|
|
serverId: application.serverId,
|
|
logPath: deployment.logPath,
|
|
});
|
|
}
|
|
|
|
command += getBuildCommand(application, deployment.logPath);
|
|
await execAsyncRemote(application.serverId, command);
|
|
await mechanizeDockerContainer(application);
|
|
}
|
|
|
|
const successComment = getIssueComment(
|
|
application.name,
|
|
"success",
|
|
previewDomain,
|
|
);
|
|
await updateIssueComment({
|
|
...issueParams,
|
|
body: `### Dokploy Preview Deployment\n\n${successComment}`,
|
|
});
|
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
|
await updatePreviewDeployment(previewDeploymentId, {
|
|
previewStatus: "done",
|
|
});
|
|
} catch (error) {
|
|
const comment = getIssueComment(application.name, "error", previewDomain);
|
|
await updateIssueComment({
|
|
...issueParams,
|
|
body: `### Dokploy Preview Deployment\n\n${comment}`,
|
|
});
|
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
|
await updatePreviewDeployment(previewDeploymentId, {
|
|
previewStatus: "error",
|
|
});
|
|
throw error;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
export const rebuildRemoteApplication = async ({
|
|
applicationId,
|
|
titleLog = "Rebuild deployment",
|
|
descriptionLog = "",
|
|
}: {
|
|
applicationId: string;
|
|
titleLog: string;
|
|
descriptionLog: string;
|
|
}) => {
|
|
const application = await findApplicationById(applicationId);
|
|
|
|
const deployment = await createDeployment({
|
|
applicationId: applicationId,
|
|
title: titleLog,
|
|
description: descriptionLog,
|
|
});
|
|
|
|
try {
|
|
if (application.serverId) {
|
|
if (application.sourceType !== "docker") {
|
|
let command = "set -e;";
|
|
command += getBuildCommand(application, deployment.logPath);
|
|
await execAsyncRemote(application.serverId, command);
|
|
}
|
|
await mechanizeDockerContainer(application);
|
|
}
|
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
|
await updateApplicationStatus(applicationId, "done");
|
|
} catch (error) {
|
|
// @ts-ignore
|
|
const encodedContent = encodeBase64(error?.message);
|
|
|
|
await execAsyncRemote(
|
|
application.serverId,
|
|
`
|
|
echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath};
|
|
echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};
|
|
echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`,
|
|
);
|
|
|
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
|
await updateApplicationStatus(applicationId, "error");
|
|
throw error;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
export const getApplicationStats = async (appName: string) => {
|
|
const filter = {
|
|
status: ["running"],
|
|
label: [`com.docker.swarm.service.name=${appName}`],
|
|
};
|
|
|
|
const containers = await docker.listContainers({
|
|
filters: JSON.stringify(filter),
|
|
});
|
|
|
|
const container = containers[0];
|
|
if (!container || container?.State !== "running") {
|
|
return null;
|
|
}
|
|
|
|
const data = await getAdvancedStats(appName);
|
|
|
|
return data;
|
|
};
|