Merge branch 'canary' into Traefik--Enable-dashboard-dokploy-traefik-container-gone,all-services-domains-down

This commit is contained in:
Mauricio Siu
2025-12-06 13:09:06 -06:00
145 changed files with 60719 additions and 4821 deletions

View File

@@ -110,7 +110,8 @@ export const getDokployUrl = async () => {
const admin = await findAdmin();
if (admin.user.host) {
return `https://${admin.user.host}`;
const protocol = admin.user.https ? "https" : "http";
return `${protocol}://${admin.user.host}`;
}
return `http://${admin.user.serverIp}:${process.env.PORT}`;
};

View File

@@ -7,37 +7,25 @@ import {
} 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";
ExecError,
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
import { cloneBitbucketRepository } from "@dokploy/server/utils/providers/bitbucket";
import { buildRemoteDocker } from "@dokploy/server/utils/providers/docker";
import {
cloneGitRepository,
getCustomGitCloneCommand,
getGitCommitInfo,
} 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 { cloneGiteaRepository } from "@dokploy/server/utils/providers/gitea";
import { cloneGithubRepository } from "@dokploy/server/utils/providers/github";
import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab";
import { createTraefikConfig } from "@dokploy/server/utils/traefik/application";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
@@ -46,6 +34,7 @@ import { getDokployUrl } from "./admin";
import {
createDeployment,
createDeploymentPreview,
updateDeployment,
updateDeploymentStatus,
} from "./deployment";
import { type Domain, getDomainHost } from "./domain";
@@ -60,7 +49,6 @@ import {
updatePreviewDeployment,
} from "./preview-deployment";
import { validUniqueServerAppName } from "./project";
import { createRollback } from "./rollbacks";
export type Application = typeof applications.$inferSelect;
export const createApplication = async (
@@ -123,6 +111,8 @@ export const findApplicationById = async (applicationId: string) => {
gitea: true,
server: true,
previewDeployments: true,
buildRegistry: true,
rollbackRegistry: true,
},
});
if (!application) {
@@ -183,6 +173,7 @@ export const deployApplication = async ({
descriptionLog: string;
}) => {
const application = await findApplicationById(applicationId);
const serverId = application.buildServerId || application.serverId;
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.environment.projectId}/environment/${application.environmentId}/services/application/${application.applicationId}?tab=deployments`;
const deployment = await createDeployment({
@@ -192,44 +183,34 @@ export const deployApplication = async ({
});
try {
let command = "set -e;";
if (application.sourceType === "github") {
await cloneGithubRepository({
...application,
logPath: deployment.logPath,
});
await buildApplication(application, deployment.logPath);
command += await cloneGithubRepository(application);
} else if (application.sourceType === "gitlab") {
await cloneGitlabRepository(application, deployment.logPath);
await buildApplication(application, deployment.logPath);
command += await cloneGitlabRepository(application);
} else if (application.sourceType === "gitea") {
await cloneGiteaRepository(application, deployment.logPath);
await buildApplication(application, deployment.logPath);
command += await cloneGiteaRepository(application);
} 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);
command += await cloneBitbucketRepository(application);
} 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);
command += await cloneGitRepository(application);
} else if (application.sourceType === "docker") {
command += await buildRemoteDocker(application);
}
command += await getBuildCommand(application);
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (serverId) {
await execAsyncRemote(serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
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.environment.project.name,
applicationName: application.name,
@@ -237,8 +218,24 @@ export const deployApplication = async ({
buildLink,
organizationId: application.environment.project.organizationId,
domains: application.domains,
environmentName: application.environment.name,
});
} catch (error) {
let command = "";
// Only log details for non-ExecError errors
if (!(error instanceof ExecError)) {
const message = error instanceof Error ? error.message : String(error);
const encodedMessage = encodeBase64(message);
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
}
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error");
@@ -253,8 +250,19 @@ export const deployApplication = async ({
});
throw error;
}
} finally {
// Only extract commit info for non-docker sources
if (application.sourceType !== "docker") {
const commitInfo = await getGitCommitInfo(application);
if (commitInfo) {
await updateDeployment(deployment.deploymentId, {
title: commitInfo.message,
description: `Commit: ${commitInfo.hash}`,
});
}
}
}
return true;
};
@@ -268,50 +276,9 @@ export const rebuildApplication = async ({
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 serverId = application.buildServerId || application.serverId;
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.environment.projectId}/environment/${application.environmentId}/services/application/${application.applicationId}?tab=deployments`;
const deployment = await createDeployment({
applicationId: applicationId,
title: titleLog,
@@ -319,53 +286,19 @@ export const deployRemoteApplication = async ({
});
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);
let command = "set -e;";
// Check case for docker only
command += await getBuildCommand(application);
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (serverId) {
await execAsyncRemote(serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
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.environment.project.name,
applicationName: application.name,
@@ -373,32 +306,26 @@ export const deployRemoteApplication = async ({
buildLink,
organizationId: application.environment.project.organizationId,
domains: application.domains,
environmentName: application.environment.name,
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
let command = "";
const encodedContent = encodeBase64(errorMessage);
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}";`,
);
// Only log details for non-ExecError errors
if (!(error instanceof ExecError)) {
const message = error instanceof Error ? error.message : String(error);
const encodedMessage = encodeBase64(message);
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
}
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error");
await sendBuildErrorNotifications({
projectName: application.environment.project.name,
applicationName: application.name,
applicationType: "application",
errorMessage: `Please check the logs for details: ${errorMessage}`,
buildLink,
organizationId: application.environment.project.organizationId,
});
throw error;
}
@@ -474,15 +401,27 @@ export const deployPreviewApplication = async ({
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
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;
application.rollbackRegistry = null;
application.registry = null;
let command = "set -e;";
if (application.sourceType === "github") {
await cloneGithubRepository({
command += await cloneGithubRepository({
...application,
appName: previewDeployment.appName,
branch: previewDeployment.branch,
logPath: deployment.logPath,
});
await buildApplication(application, deployment.logPath);
command += await getBuildCommand(application);
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (application.serverId) {
await execAsyncRemote(application.serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
await mechanizeDockerContainer(application);
}
const successComment = getIssueComment(
application.name,
@@ -513,170 +452,10 @@ export const deployPreviewApplication = async ({
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}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
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) => {
if (appName === "dokploy") {
return await getAdvancedStats(appName);
}
const filter = {
status: ["running"],
label: [`com.docker.swarm.service.name=${appName}`],

View File

@@ -78,7 +78,6 @@ const BUNNY_CDN_IPS = new Set([
"89.187.188.227",
"89.187.188.228",
"139.180.134.196",
"89.38.96.158",
"89.187.162.249",
"89.187.162.242",
"185.102.217.65",
@@ -106,12 +105,9 @@ const BUNNY_CDN_IPS = new Set([
"200.25.38.69",
"200.25.42.70",
"200.25.36.166",
"195.206.229.106",
"194.242.11.186",
"185.164.35.8",
"94.20.154.22",
"185.93.1.244",
"156.59.145.154",
"143.244.49.177",
"138.199.46.66",
"138.199.37.227",
@@ -136,7 +132,6 @@ const BUNNY_CDN_IPS = new Set([
"84.17.59.115",
"89.187.165.194",
"138.199.15.193",
"89.35.237.170",
"37.19.216.130",
"185.93.1.247",
"185.93.3.244",
@@ -150,6 +145,7 @@ const BUNNY_CDN_IPS = new Set([
"84.17.63.178",
"200.25.32.131",
"37.19.207.34",
"37.19.207.38",
"192.189.65.146",
"143.244.45.177",
"185.93.1.249",
@@ -168,9 +164,7 @@ const BUNNY_CDN_IPS = new Set([
"129.227.217.178",
"129.227.217.179",
"200.25.69.94",
"128.1.52.179",
"200.25.16.103",
"15.235.54.226",
"102.67.138.155",
"156.146.43.65",
"195.181.163.203",
@@ -278,13 +272,11 @@ const BUNNY_CDN_IPS = new Set([
"107.155.47.146",
"193.201.190.174",
"156.59.95.218",
"213.170.143.139",
"129.227.186.154",
"195.238.127.98",
"200.25.22.6",
"204.16.244.92",
"200.25.70.101",
"200.25.66.100",
"139.180.209.182",
"103.108.231.41",
"103.108.229.5",
@@ -387,46 +379,13 @@ const BUNNY_CDN_IPS = new Set([
"38.54.5.37",
"38.54.3.92",
"185.165.170.74",
"207.121.80.118",
"207.121.46.228",
"207.121.46.236",
"207.121.46.244",
"207.121.46.252",
"216.202.235.164",
"207.121.46.220",
"207.121.75.132",
"207.121.80.12",
"207.121.80.172",
"207.121.90.60",
"207.121.90.68",
"207.121.97.204",
"207.121.90.252",
"207.121.97.236",
"207.121.99.12",
"138.199.24.219",
"185.93.2.251",
"138.199.46.65",
"207.121.41.196",
"207.121.99.20",
"207.121.99.36",
"207.121.99.44",
"207.121.99.52",
"207.121.99.60",
"207.121.23.68",
"207.121.23.124",
"207.121.23.244",
"207.121.23.180",
"207.121.23.188",
"207.121.23.196",
"207.121.23.204",
"207.121.24.52",
"207.121.24.60",
"207.121.24.68",
"207.121.24.76",
"207.121.24.92",
"207.121.24.100",
"207.121.24.108",
"207.121.24.116",
"154.95.86.76",
"5.9.99.73",
"78.46.92.118",
@@ -434,14 +393,52 @@ const BUNNY_CDN_IPS = new Set([
"78.46.156.89",
"88.198.9.155",
"144.76.79.22",
"103.1.215.93",
"103.137.12.33",
"103.107.196.31",
"116.90.72.155",
"103.137.14.5",
"116.90.75.65",
"37.19.207.37",
"208.83.234.224",
"79.127.237.104",
"79.127.243.187",
"45.156.248.73",
"79.127.134.225",
"79.127.134.226",
"79.127.134.227",
"79.127.134.228",
"79.127.134.229",
"79.127.134.230",
"79.127.134.231",
"79.127.134.130",
"79.127.134.131",
"79.127.134.132",
"79.127.134.234",
"79.127.134.235",
"185.111.111.154",
"185.111.111.155",
"185.111.111.156",
"185.111.111.157",
"185.111.111.158",
"185.111.111.159",
"185.111.111.160",
"141.227.142.242",
"94.128.254.166",
"195.206.229.69",
"200.25.86.90",
"148.113.190.161",
"46.151.194.242",
"46.151.194.243",
"212.102.40.120",
"213.170.143.100",
"154.93.86.71",
"143.244.60.196",
"143.244.60.197",
"143.244.60.195",
"79.127.134.129",
"79.127.134.133",
"152.233.22.97",
"152.233.22.98",
"152.233.22.100",
"152.233.22.99",
"152.233.22.101",
"152.233.22.102",
"152.233.22.103",
"116.202.155.146",
"116.202.193.178",
"116.202.224.168",
@@ -502,6 +499,12 @@ const BUNNY_CDN_IPS = new Set([
"103.60.15.166",
"103.60.15.167",
"103.60.15.168",
"176.9.139.94",
"148.251.129.132",
"148.251.131.73",
"148.251.131.74",
"136.243.70.170",
"148.251.131.238",
"109.248.43.116",
"109.248.43.117",
"109.248.43.162",
@@ -527,7 +530,9 @@ const BUNNY_CDN_IPS = new Set([
"139.180.129.216",
"139.99.174.7",
"89.187.169.18",
"143.244.38.133",
"89.187.179.7",
"169.150.213.50",
"143.244.62.213",
"185.93.3.246",
"195.181.163.198",
@@ -535,7 +540,6 @@ const BUNNY_CDN_IPS = new Set([
"84.17.37.211",
"212.102.50.54",
"212.102.46.115",
"143.244.38.135",
"169.150.238.21",
"169.150.207.51",
"169.150.207.49",
@@ -546,7 +550,6 @@ const BUNNY_CDN_IPS = new Set([
"169.150.247.139",
"169.150.247.177",
"169.150.247.178",
"169.150.213.49",
"212.102.46.119",
"84.17.38.234",
"84.17.38.233",
@@ -558,7 +561,6 @@ const BUNNY_CDN_IPS = new Set([
"169.150.247.138",
"169.150.247.184",
"169.150.247.185",
"156.146.58.83",
"212.102.43.88",
"89.187.169.26",
"109.61.89.57",
@@ -587,6 +589,17 @@ const BUNNY_CDN_IPS = new Set([
"138.199.4.177",
"37.19.222.34",
"46.151.193.85",
"79.127.237.99",
"212.104.158.30",
"212.104.158.31",
"212.104.158.32",
"212.104.158.33",
"212.104.158.34",
"212.104.158.28",
"212.104.158.29",
"212.104.158.35",
"212.104.158.36",
"212.104.158.37",
"212.104.158.17",
"212.104.158.18",
"212.104.158.19",
@@ -595,12 +608,20 @@ const BUNNY_CDN_IPS = new Set([
"212.104.158.22",
"212.104.158.24",
"212.104.158.26",
"79.127.237.134",
"89.187.184.177",
"89.187.184.179",
"89.187.184.173",
"89.187.184.178",
"89.187.184.176",
"212.104.158.25",
"212.104.158.27",
"212.104.158.67",
"212.104.158.10",
"212.104.158.12",
"212.104.158.64",
"212.104.158.16",
"212.104.158.23",
"212.104.158.54",
]);
// Arvancloud IP ranges

View File

@@ -7,14 +7,10 @@ import {
cleanAppName,
compose,
} from "@dokploy/server/db/schema";
import {
buildCompose,
getBuildComposeCommand,
} from "@dokploy/server/utils/builders/compose";
import { getBuildComposeCommand } from "@dokploy/server/utils/builders/compose";
import { randomizeSpecificationFile } from "@dokploy/server/utils/docker/compose";
import {
cloneCompose,
cloneComposeRemote,
loadDockerCompose,
loadDockerComposeRemote,
} from "@dokploy/server/utils/docker/domain";
@@ -22,38 +18,28 @@ import type { ComposeSpecification } from "@dokploy/server/utils/docker/types";
import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error";
import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success";
import {
ExecError,
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
import {
cloneBitbucketRepository,
getBitbucketCloneCommand,
} from "@dokploy/server/utils/providers/bitbucket";
import { cloneBitbucketRepository } from "@dokploy/server/utils/providers/bitbucket";
import {
cloneGitRepository,
getCustomGitCloneCommand,
getGitCommitInfo,
} 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 {
createComposeFile,
getCreateComposeFileCommand,
} from "@dokploy/server/utils/providers/raw";
import { cloneGiteaRepository } from "@dokploy/server/utils/providers/gitea";
import { cloneGithubRepository } from "@dokploy/server/utils/providers/github";
import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab";
import { getCreateComposeFileCommand } from "@dokploy/server/utils/providers/raw";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { encodeBase64 } from "../utils/docker/utils";
import { getDokployUrl } from "./admin";
import { createDeploymentCompose, updateDeploymentStatus } from "./deployment";
import {
createDeploymentCompose,
updateDeployment,
updateDeploymentStatus,
} from "./deployment";
import { validUniqueServerAppName } from "./project";
export type Compose = typeof compose.$inferSelect;
@@ -163,10 +149,11 @@ export const loadServices = async (
const compose = await findComposeById(composeId);
if (type === "fetch") {
const command = await cloneCompose(compose);
if (compose.serverId) {
await cloneComposeRemote(compose);
await execAsyncRemote(compose.serverId, command);
} else {
await cloneCompose(compose);
await execAsync(command);
}
}
@@ -235,24 +222,41 @@ export const deployCompose = async ({
});
try {
const entity = {
...compose,
type: "compose" as const,
};
let command = "set -e;";
if (compose.sourceType === "github") {
await cloneGithubRepository({
...compose,
logPath: deployment.logPath,
type: "compose",
});
command += await cloneGithubRepository(entity);
} else if (compose.sourceType === "gitlab") {
await cloneGitlabRepository(compose, deployment.logPath, true);
command += await cloneGitlabRepository(entity);
} else if (compose.sourceType === "bitbucket") {
await cloneBitbucketRepository(compose, deployment.logPath, true);
command += await cloneBitbucketRepository(entity);
} else if (compose.sourceType === "git") {
await cloneGitRepository(compose, deployment.logPath, true);
command += await cloneGitRepository(entity);
} else if (compose.sourceType === "gitea") {
await cloneGiteaRepository(compose, deployment.logPath, true);
command += await cloneGiteaRepository(entity);
} else if (compose.sourceType === "raw") {
await createComposeFile(compose, deployment.logPath);
command += getCreateComposeFileCommand(entity);
}
await buildCompose(compose, deployment.logPath);
let commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (compose.serverId) {
await execAsyncRemote(compose.serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
command = "set -e;";
command += await getBuildComposeCommand(entity);
commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (compose.serverId) {
await execAsyncRemote(compose.serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
await updateDeploymentStatus(deployment.deploymentId, "done");
await updateCompose(composeId, {
composeStatus: "done",
@@ -265,8 +269,24 @@ export const deployCompose = async ({
buildLink,
organizationId: compose.environment.project.organizationId,
domains: compose.domains,
environmentName: compose.environment.name,
});
} catch (error) {
let command = "";
// Only log details for non-ExecError errors
if (!(error instanceof ExecError)) {
const message = error instanceof Error ? error.message : String(error);
const encodedMessage = encodeBase64(message);
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
}
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
if (compose.serverId) {
await execAsyncRemote(compose.serverId, command);
} else {
await execAsync(command);
}
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateCompose(composeId, {
composeStatus: "error",
@@ -281,6 +301,19 @@ export const deployCompose = async ({
organizationId: compose.environment.project.organizationId,
});
throw error;
} finally {
if (compose.sourceType !== "raw") {
const commitInfo = await getGitCommitInfo({
...compose,
type: "compose",
});
if (commitInfo) {
await updateDeployment(deployment.deploymentId, {
title: commitInfo.message,
description: `Commit: ${commitInfo.hash}`,
});
}
}
}
};
@@ -302,154 +335,23 @@ export const rebuildCompose = async ({
});
try {
let command = "set -e;";
if (compose.sourceType === "raw") {
await createComposeFile(compose, deployment.logPath);
command += getCreateComposeFileCommand(compose);
}
await buildCompose(compose, deployment.logPath);
await updateDeploymentStatus(deployment.deploymentId, "done");
await updateCompose(composeId, {
composeStatus: "done",
});
} catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateCompose(composeId, {
composeStatus: "error",
});
throw error;
}
return true;
};
export const deployRemoteCompose = async ({
composeId,
titleLog = "Manual deployment",
descriptionLog = "",
}: {
composeId: string;
titleLog: string;
descriptionLog: string;
}) => {
const compose = await findComposeById(composeId);
const buildLink = `${await getDokployUrl()}/dashboard/project/${
compose.environment.projectId
}/environment/${compose.environmentId}/services/compose/${compose.composeId}?tab=deployments`;
const deployment = await createDeploymentCompose({
composeId: composeId,
title: titleLog,
description: descriptionLog,
});
try {
let commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (compose.serverId) {
let command = "set -e;";
if (compose.sourceType === "github") {
command += await getGithubCloneCommand({
...compose,
logPath: deployment.logPath,
type: "compose",
serverId: compose.serverId,
});
} else if (compose.sourceType === "gitlab") {
command += await getGitlabCloneCommand(
compose,
deployment.logPath,
true,
);
} else if (compose.sourceType === "bitbucket") {
command += await getBitbucketCloneCommand(
compose,
deployment.logPath,
true,
);
} else if (compose.sourceType === "git") {
command += await getCustomGitCloneCommand(
compose,
deployment.logPath,
true,
);
console.log(command);
} else if (compose.sourceType === "raw") {
command += getCreateComposeFileCommand(compose, deployment.logPath);
} else if (compose.sourceType === "gitea") {
command += await getGiteaCloneCommand(
compose,
deployment.logPath,
true,
);
}
await execAsyncRemote(compose.serverId, command);
await getBuildComposeCommand(compose, deployment.logPath);
}
await updateDeploymentStatus(deployment.deploymentId, "done");
await updateCompose(composeId, {
composeStatus: "done",
});
await sendBuildSuccessNotifications({
projectName: compose.environment.project.name,
applicationName: compose.name,
applicationType: "compose",
buildLink,
organizationId: compose.environment.project.organizationId,
domains: compose.domains,
});
} catch (error) {
// @ts-ignore
const encodedContent = encodeBase64(error?.message);
await execAsyncRemote(
compose.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 updateCompose(composeId, {
composeStatus: "error",
});
await sendBuildErrorNotifications({
projectName: compose.environment.project.name,
applicationName: compose.name,
applicationType: "compose",
// @ts-ignore
errorMessage: error?.message || "Error building",
buildLink,
organizationId: compose.environment.project.organizationId,
});
throw error;
}
};
export const rebuildRemoteCompose = async ({
composeId,
titleLog = "Rebuild deployment",
descriptionLog = "",
}: {
composeId: string;
titleLog: string;
descriptionLog: string;
}) => {
const compose = await findComposeById(composeId);
const deployment = await createDeploymentCompose({
composeId: composeId,
title: titleLog,
description: descriptionLog,
});
try {
if (compose.sourceType === "raw") {
const command = getCreateComposeFileCommand(compose, deployment.logPath);
await execAsyncRemote(compose.serverId, command);
await execAsyncRemote(compose.serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
command += await getBuildComposeCommand(compose);
commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (compose.serverId) {
await getBuildComposeCommand(compose, deployment.logPath);
await execAsyncRemote(compose.serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
await updateDeploymentStatus(deployment.deploymentId, "done");
@@ -457,16 +359,21 @@ export const rebuildRemoteCompose = async ({
composeStatus: "done",
});
} catch (error) {
// @ts-ignore
const encodedContent = encodeBase64(error?.message);
let command = "";
await execAsyncRemote(
compose.serverId,
`
echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath};
echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};
echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`,
);
// Only log details for non-ExecError errors
if (!(error instanceof ExecError)) {
const message = error instanceof Error ? error.message : String(error);
const encodedMessage = encodeBase64(message);
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
}
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
if (compose.serverId) {
await execAsyncRemote(compose.serverId, command);
} else {
await execAsync(command);
}
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateCompose(composeId, {
composeStatus: "error",
@@ -501,7 +408,7 @@ export const removeCompose = async (
} else {
const command = `
docker network disconnect ${compose.appName} dokploy-traefik;
cd ${projectPath} && docker compose -p ${compose.appName} down ${
cd ${projectPath} && env -i PATH="$PATH" docker compose -p ${compose.appName} down ${
deleteVolumes ? "--volumes" : ""
} && rm -rf ${projectPath}`;
@@ -528,7 +435,7 @@ export const startCompose = async (composeId: string) => {
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
const path =
compose.sourceType === "raw" ? "docker-compose.yml" : compose.composePath;
const baseCommand = `docker compose -p ${compose.appName} -f ${path} up -d`;
const baseCommand = `env -i PATH="$PATH" docker compose -p ${compose.appName} -f ${path} up -d`;
if (compose.composeType === "docker-compose") {
if (compose.serverId) {
await execAsyncRemote(
@@ -563,14 +470,17 @@ export const stopCompose = async (composeId: string) => {
if (compose.serverId) {
await execAsyncRemote(
compose.serverId,
`cd ${join(COMPOSE_PATH, compose.appName)} && docker compose -p ${
`cd ${join(COMPOSE_PATH, compose.appName)} && env -i PATH="$PATH" docker compose -p ${
compose.appName
} stop`,
);
} else {
await execAsync(`docker compose -p ${compose.appName} stop`, {
cwd: join(COMPOSE_PATH, compose.appName),
});
await execAsync(
`env -i PATH="$PATH" docker compose -p ${compose.appName} stop`,
{
cwd: join(COMPOSE_PATH, compose.appName),
},
);
}
}

View File

@@ -74,24 +74,26 @@ export const createDeployment = async (
>,
) => {
const application = await findApplicationById(deployment.applicationId);
try {
await removeLastTenDeployments(
deployment.applicationId,
"application",
application.serverId,
);
const { LOGS_PATH } = paths(!!application.serverId);
const serverId = application.buildServerId || application.serverId;
const { LOGS_PATH } = paths(!!serverId);
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
const fileName = `${application.appName}-${formattedDateTime}.log`;
const logFilePath = path.join(LOGS_PATH, application.appName, fileName);
if (application.serverId) {
const server = await findServerById(application.serverId);
if (serverId) {
const server = await findServerById(serverId);
const command = `
mkdir -p ${LOGS_PATH}/${application.appName};
echo "Initializing deployment" >> ${logFilePath};
echo "Building on ${serverId ? "Build Server" : "Dokploy Server"}" >> ${logFilePath};
`;
await execAsyncRemote(server.serverId, command);
@@ -99,7 +101,7 @@ export const createDeployment = async (
await fsPromises.mkdir(path.join(LOGS_PATH, application.appName), {
recursive: true,
});
await fsPromises.writeFile(logFilePath, "Initializing deployment");
await fsPromises.writeFile(logFilePath, "Initializing deployment\n");
}
const deploymentCreate = await db
@@ -111,6 +113,9 @@ export const createDeployment = async (
logPath: logFilePath,
description: deployment.description || "",
startedAt: new Date().toISOString(),
...(application.buildServerId && {
buildServerId: application.buildServerId,
}),
})
.returning();
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
@@ -249,7 +254,7 @@ export const createDeploymentCompose = async (
const command = `
mkdir -p ${LOGS_PATH}/${compose.appName};
echo "Initializing deployment" >> ${logFilePath};
echo "Initializing deployment\n" >> ${logFilePath};
`;
await execAsyncRemote(server.serverId, command);
@@ -257,7 +262,7 @@ echo "Initializing deployment" >> ${logFilePath};
await fsPromises.mkdir(path.join(LOGS_PATH, compose.appName), {
recursive: true,
});
await fsPromises.writeFile(logFilePath, "Initializing deployment");
await fsPromises.writeFile(logFilePath, "Initializing deployment\n");
}
const deploymentCreate = await db

View File

@@ -19,6 +19,7 @@ export const createDomain = async (input: typeof apiCreateDomain._type) => {
.insert(domains)
.values({
...input,
host: input.host?.trim(),
})
.returning()
.then((response) => response[0]);
@@ -120,6 +121,7 @@ export const updateDomainById = async (
.update(domains)
.set({
...domainData,
...(domainData.host && { host: domainData.host.trim() }),
})
.where(eq(domains.domainId, domainId))
.returning();

View File

@@ -34,13 +34,43 @@ export const findEnvironmentById = async (environmentId: string) => {
const environment = await db.query.environments.findFirst({
where: eq(environments.environmentId, environmentId),
with: {
applications: true,
mariadb: true,
mongo: true,
mysql: true,
postgres: true,
redis: true,
compose: true,
applications: {
with: {
deployments: true,
server: true,
},
},
mariadb: {
with: {
server: true,
},
},
mongo: {
with: {
server: true,
},
},
mysql: {
with: {
server: true,
},
},
postgres: {
with: {
server: true,
},
},
redis: {
with: {
server: true,
},
},
compose: {
with: {
deployments: true,
server: true,
},
},
project: true,
},
});

View File

@@ -498,7 +498,7 @@ export const createNtfyNotification = async (
.values({
serverUrl: input.serverUrl,
topic: input.topic,
accessToken: input.accessToken,
accessToken: input.accessToken ?? null,
priority: input.priority,
})
.returning()
@@ -569,7 +569,7 @@ export const updateNtfyNotification = async (
.set({
serverUrl: input.serverUrl,
topic: input.topic,
accessToken: input.accessToken,
accessToken: input.accessToken ?? null,
priority: input.priority,
})
.where(eq(ntfy.ntfyId, input.ntfyId));

View File

@@ -13,6 +13,19 @@ import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
export function getMountPath(dockerImage: string): string {
const versionMatch = dockerImage.match(/postgres:(\d+)/);
if (versionMatch?.[1]) {
const version = Number.parseInt(versionMatch[1], 10);
if (version >= 18) {
// PostgreSQL 18+ uses /var/lib/postgresql/{version}/docker as the default PGDATA
return `/var/lib/postgresql/${version}/docker`;
}
}
return "/var/lib/postgresql/data";
}
export type Postgres = typeof postgres.$inferSelect;
export const createPostgres = async (input: typeof apiCreatePostgres._type) => {

View File

@@ -7,7 +7,8 @@ import {
deployments as deploymentsSchema,
rollbacks,
} from "../db/schema";
import { type ApplicationNested, getAuthConfig } from "../utils/builders";
import type { ApplicationNested } from "../utils/builders";
import { getRegistryTag } from "../utils/cluster/upload";
import {
calculateResources,
generateBindMounts,
@@ -22,11 +23,12 @@ import { findDeploymentById } from "./deployment";
import type { Mount } from "./mount";
import type { Port } from "./port";
import type { Project } from "./project";
import type { Registry } from "./registry";
export const createRollback = async (
input: z.infer<typeof createRollbackSchema>,
) => {
await db.transaction(async (tx) => {
return await db.transaction(async (tx) => {
const { fullContext, ...other } = input;
const rollback = await tx
.insert(rollbacks)
@@ -70,9 +72,11 @@ export const createRollback = async (
})
.where(eq(deploymentsSchema.deploymentId, rollback.deploymentId));
await createRollbackImage(rest, tagImage);
const updatedRollback = await tx.query.rollbacks.findFirst({
where: eq(rollbacks.rollbackId, rollback.rollbackId),
});
return rollback;
return updatedRollback;
});
};
@@ -103,27 +107,6 @@ export const findRollbackById = async (rollbackId: string) => {
return result;
};
const createRollbackImage = async (
application: ApplicationNested,
tagImage: string,
) => {
const docker = await getRemoteDocker(application.serverId);
const appTagName =
application.sourceType === "docker"
? application.dockerImage
: `${application.appName}:latest`;
const result = docker.getImage(appTagName || "");
const [repo, version] = tagImage.split(":");
await result.tag({
repo,
tag: version,
});
};
const deleteRollbackImage = async (image: string, serverId?: string | null) => {
const command = `docker image rm ${image} --force`;
@@ -179,7 +162,6 @@ export const rollback = async (rollbackId: string) => {
if (!result.fullContext) {
throw new Error("Rollback context not found");
}
// Use the full context for rollback
await rollbackApplication(
application.appName,
@@ -199,6 +181,7 @@ const rollbackApplication = async (
};
mounts: Mount[];
ports: Port[];
rollbackRegistry?: Registry;
},
) => {
if (!fullContext) {
@@ -245,16 +228,24 @@ const rollbackApplication = async (
fullContext.environment.project.env,
);
// For rollback, we use the provided image instead of calculating it
const authConfig = getAuthConfig(fullContext as ApplicationNested);
// Build the full registry image path if rollbackRegistry is available
// e.g., "appName:v5" -> "siumauricio/appName:v5" or "registry.com/prefix/appName:v5"
let rollbackImage = image;
if (fullContext.rollbackRegistry) {
rollbackImage = getRegistryTag(fullContext.rollbackRegistry, image);
}
const settings: CreateServiceOptions = {
authconfig: authConfig,
authconfig: {
password: fullContext.rollbackRegistry?.password || "",
username: fullContext.rollbackRegistry?.username || "",
serveraddress: fullContext.rollbackRegistry?.registryUrl || "",
},
Name: appName,
TaskTemplate: {
ContainerSpec: {
HealthCheck,
Image: image,
Image: rollbackImage,
Env: envVariables,
Mounts: [...volumesMount, ...bindsMount],
...(command
@@ -297,7 +288,8 @@ const rollbackApplication = async (
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
},
});
} catch {
} catch (error) {
console.error(error);
await docker.createService(settings);
}
};

View File

@@ -59,10 +59,8 @@ export const getUpdateData = async (): Promise<IUpdateData> => {
let currentDigest: string;
try {
currentDigest = await getServiceImageDigest();
} catch {
// Docker service might not exist locally
// You can run the # Installation command for docker service create mentioned in the below docs to test it locally:
// https://docs.dokploy.com/docs/core/manual-installation
} catch (error) {
// TODO: Docker versions 29.0.0 change the way to get the service image digest, so we need to update this in the future we upgrade to that version.
return DEFAULT_UPDATE_DATA;
}