import fs, { existsSync, readFileSync } from "node:fs";import { join } from "node:path";import { paths } from "@dokploy/server/constants";import type { Compose } from "@dokploy/server/services/compose";import type { Domain } from "@dokploy/server/services/domain";import { parse, stringify } from "yaml";import { execAsyncRemote } from "../process/execAsync";import { cloneBitbucketRepository } from "../providers/bitbucket";import { cloneGitRepository } from "../providers/git";import { cloneGiteaRepository } from "../providers/gitea";import { cloneGithubRepository } from "../providers/github";import { cloneGitlabRepository } from "../providers/gitlab";import { getCreateComposeFileCommand } from "../providers/raw";import { randomizeDeployableSpecificationFile } from "./collision";import { randomizeSpecificationFile } from "./compose";import type { ComposeSpecification, DefinitionsService, PropertiesNetworks,} from "./types";import { encodeBase64 } from "./utils"; export const cloneCompose = async (compose: Compose) => { let command = "set -e;"; const entity = { ...compose, type: "compose" as const, }; if (compose.sourceType === "github") { command += await cloneGithubRepository(entity); } else if (compose.sourceType === "gitlab") { command += await cloneGitlabRepository(entity); } else if (compose.sourceType === "bitbucket") { command += await cloneBitbucketRepository(entity); } else if (compose.sourceType === "git") { command += await cloneGitRepository(entity); } else if (compose.sourceType === "gitea") { command += await cloneGiteaRepository(entity); } else if (compose.sourceType === "raw") { command += getCreateComposeFileCommand(compose); } return command;}; export const getComposePath = (compose: Compose) => { const { COMPOSE_PATH } = paths(!!compose.serverId); const { appName, sourceType, composePath } = compose; let path = ""; if (sourceType === "raw") { path = "docker-compose.yml"; } else { path = composePath; } return join(COMPOSE_PATH, appName, "code", path);}; export const loadDockerCompose = async ( compose: Compose,): Promise<ComposeSpecification | null> => { const path = getComposePath(compose); if (existsSync(path)) { const yamlStr = readFileSync(path, "utf8"); const parsedConfig = parse(yamlStr, { maxAliasCount: 10000, }) as ComposeSpecification; return parsedConfig; } return null;}; export const loadDockerComposeRemote = async ( compose: Compose,): Promise<ComposeSpecification | null> => { const path = getComposePath(compose); try { if (!compose.serverId) { return null; } const { stdout, stderr } = await execAsyncRemote( compose.serverId, `cat ${path}`, ); if (stderr) { return null; } if (!stdout) return null; const parsedConfig = parse(stdout, { maxAliasCount: 10000, }) as ComposeSpecification; return parsedConfig; } catch { return null; }}; export const readComposeFile = async (compose: Compose) => { const path = getComposePath(compose); if (existsSync(path)) { const yamlStr = readFileSync(path, "utf8"); return yamlStr; } return null;}; export const writeDomainsToCompose = async ( compose: Compose, domains: Domain[],) => { try { const composeConverted = await addDomainToCompose(compose, domains); const path = getComposePath(compose); if (!composeConverted) { return `echo "❌ Error: Compose file not found";exit 1; `; } const composeString = stringify(composeConverted, { lineWidth: 1000 }); const encodedContent = encodeBase64(composeString); return `echo "${encodedContent}" | base64 -d > "${path}";`; } catch (error) { // @ts-ignore return `echo "❌ Has occurred an error: ${error?.message || error}";exit 1; `; }};export const addDomainToCompose = async ( compose: Compose, domains: Domain[],) => { const { appName } = compose; let result: ComposeSpecification | null; if (compose.serverId) { result = await loadDockerComposeRemote(compose); } else { result = await loadDockerCompose(compose); } if (!result) { return null; } if (compose.isolatedDeployment) { const randomized = randomizeDeployableSpecificationFile( result, compose.isolatedDeploymentsVolume, compose.suffix || compose.appName, ); result = randomized; } else if (compose.randomize) { const randomized = randomizeSpecificationFile(result, compose.suffix); result = randomized; } for (const domain of domains) { const { serviceName, https } = domain; if (!serviceName) { throw new Error(`Domain "${domain.host}" is missing a service name`); } if (!result?.services?.[serviceName]) { throw new Error( `Domain "${domain.host}" is attached to service "${serviceName}" which does not exist in the compose`, ); } const httpLabels = createDomainLabels( appName, domain, domain.customEntrypoint || "web", ); if (!domain.customEntrypoint && https) { const httpsLabels = createDomainLabels(appName, domain, "websecure"); httpLabels.push(...httpsLabels); } let labels: DefinitionsService["labels"] = []; if (compose.composeType === "docker-compose") { if (!result.services[serviceName].labels) { result.services[serviceName].labels = []; } labels = result.services[serviceName].labels; } else { // Stack Case if (!result.services[serviceName].deploy) { result.services[serviceName].deploy = {}; } if (!result.services[serviceName].deploy.labels) { result.services[serviceName].deploy.labels = []; } labels = result.services[serviceName].deploy.labels; } if (Array.isArray(labels)) { if (!labels.includes("traefik.enable=true")) { labels.unshift("traefik.enable=true"); } labels.unshift(...httpLabels); if (!compose.isolatedDeployment) { if (compose.composeType === "docker-compose") { if (!labels.includes("traefik.docker.network=dokploy-network")) { labels.unshift("traefik.docker.network=dokploy-network"); } } else { // Stack Case if (!labels.includes("traefik.swarm.network=dokploy-network")) { labels.unshift("traefik.swarm.network=dokploy-network"); } } } } if (!compose.isolatedDeployment) { // Add the dokploy-network to the service result.services[serviceName].networks = addDokployNetworkToService( result.services[serviceName].networks, ); } } // Add dokploy-network to the root of the compose file if (!compose.isolatedDeployment) { result.networks = addDokployNetworkToRoot(result.networks); } return result;}; export const writeComposeFile = async ( compose: Compose, composeSpec: ComposeSpecification,) => { const path = getComposePath(compose); try { const composeFile = stringify(composeSpec, { lineWidth: 1000, }); fs.writeFileSync(path, composeFile, "utf8"); } catch (e) { console.error("Error saving the YAML config file:", e); }}; export const createDomainLabels = ( appName: string, domain: Domain, entrypoint: string,) => { const { host, port, customEntrypoint, https, uniqueConfigKey, certificateType, path, customCertResolver, stripPath, internalPath, } = domain; const routerName = `${appName}-${uniqueConfigKey}-${entrypoint}`; const labels = [ `traefik.http.routers.${routerName}.rule=Host(\`${host}\`)${path && path !== "/" ? ` && PathPrefix(\`${path}\`)` : ""}`, `traefik.http.routers.${routerName}.entrypoints=${entrypoint}`, `traefik.http.services.${routerName}.loadbalancer.server.port=${port}`, `traefik.http.routers.${routerName}.service=${routerName}`, ]; // Collect middlewares for this router const middlewares: string[] = []; const isRedirectRouter = entrypoint === "web" && https && !customEntrypoint; // Web router with HTTPS only needs redirect — all other middlewares // run on the websecure router where the request actually lands. if (isRedirectRouter) { middlewares.push("redirect-to-https@file"); } // Add stripPath middleware if needed if (stripPath && path && path !== "/") { const middlewareName = `stripprefix-${appName}-${uniqueConfigKey}`; // Define middleware on web (or custom) entrypoint so Traefik registers it if (entrypoint === "web" || customEntrypoint) { labels.push( `traefik.http.middlewares.${middlewareName}.stripprefix.prefixes=${path}`, ); } if (!isRedirectRouter) { middlewares.push(middlewareName); } } // Add internalPath middleware if needed if (internalPath && internalPath !== "/" && internalPath.startsWith("/")) { const middlewareName = `addprefix-${appName}-${uniqueConfigKey}`; // Define middleware on web (or custom) entrypoint so Traefik registers it if (entrypoint === "web" || customEntrypoint) { labels.push( `traefik.http.middlewares.${middlewareName}.addprefix.prefix=${internalPath}`, ); } if (!isRedirectRouter) { middlewares.push(middlewareName); } } // Add custom middlewares (skip for redirect-only router) if (!isRedirectRouter && domain.middlewares?.length) { middlewares.push(...domain.middlewares); } // Apply middlewares to router if any exist if (middlewares.length > 0) { labels.push( `traefik.http.routers.${routerName}.middlewares=${middlewares.join(",")}`, ); } // Add TLS configuration for websecure if (entrypoint === "websecure" || (customEntrypoint && https)) { if (certificateType === "letsencrypt") { labels.push( `traefik.http.routers.${routerName}.tls.certresolver=letsencrypt`, ); } else if (certificateType === "custom" && customCertResolver) { labels.push( `traefik.http.routers.${routerName}.tls.certresolver=${customCertResolver}`, ); } else if (certificateType === "none" && https) { // No cert resolver, but HTTPS is enabled (default/custom certificate): // explicitly enable TLS so Traefik serves the router over HTTPS. labels.push(`traefik.http.routers.${routerName}.tls=true`); } } return labels;}; export const addDokployNetworkToService = ( networkService: DefinitionsService["networks"],) => { let networks = networkService; const network = "dokploy-network"; const defaultNetwork = "default"; if (!networks) { networks = []; } if (Array.isArray(networks)) { if (!networks.includes(network)) { networks.push(network); } if (!networks.includes(defaultNetwork)) { networks.push(defaultNetwork); } } else if (networks && typeof networks === "object") { if (!(network in networks)) { networks[network] = {}; } if (!(defaultNetwork in networks)) { networks[defaultNetwork] = {}; } } return networks;}; export const addDokployNetworkToRoot = ( networkRoot: PropertiesNetworks | undefined,) => { let networks = networkRoot; const network = "dokploy-network"; if (!networks) { networks = {}; } if (networks[network] || !networks[network]) { networks[network] = { external: true, }; } return networks;};
import fs, { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { paths } from "@dokploy/server/constants";
import type { Compose } from "@dokploy/server/services/compose";
import type { Domain } from "@dokploy/server/services/domain";
import { parse, stringify } from "yaml";
import { execAsyncRemote } from "../process/execAsync";
import { cloneBitbucketRepository } from "../providers/bitbucket";
import { cloneGitRepository } from "../providers/git";
import { cloneGiteaRepository } from "../providers/gitea";
import { cloneGithubRepository } from "../providers/github";
import { cloneGitlabRepository } from "../providers/gitlab";
import { getCreateComposeFileCommand } from "../providers/raw";
import { randomizeDeployableSpecificationFile } from "./collision";
import { randomizeSpecificationFile } from "./compose";
import type {
ComposeSpecification,
DefinitionsService,
PropertiesNetworks,
} from "./types";
import { encodeBase64 } from "./utils";
export const cloneCompose = async (compose: Compose) => {
let command = "set -e;";
const entity = {
...compose,
type: "compose" as const,
};
if (compose.sourceType === "github") {
command += await cloneGithubRepository(entity);
} else if (compose.sourceType === "gitlab") {
command += await cloneGitlabRepository(entity);
} else if (compose.sourceType === "bitbucket") {
command += await cloneBitbucketRepository(entity);
} else if (compose.sourceType === "git") {
command += await cloneGitRepository(entity);
} else if (compose.sourceType === "gitea") {
command += await cloneGiteaRepository(entity);
} else if (compose.sourceType === "raw") {
command += getCreateComposeFileCommand(compose);
}
return command;
};
export const getComposePath = (compose: Compose) => {
const { COMPOSE_PATH } = paths(!!compose.serverId);
const { appName, sourceType, composePath } = compose;
let path = "";
if (sourceType === "raw") {
path = "docker-compose.yml";
} else {
path = composePath;
}
return join(COMPOSE_PATH, appName, "code", path);
};
export const loadDockerCompose = async (
compose: Compose,
): Promise<ComposeSpecification | null> => {
const path = getComposePath(compose);
if (existsSync(path)) {
const yamlStr = readFileSync(path, "utf8");
const parsedConfig = parse(yamlStr, {
maxAliasCount: 10000,
}) as ComposeSpecification;
return parsedConfig;
}
return null;
};
export const loadDockerComposeRemote = async (
compose: Compose,
): Promise<ComposeSpecification | null> => {
const path = getComposePath(compose);
try {
if (!compose.serverId) {
return null;
}
const { stdout, stderr } = await execAsyncRemote(
compose.serverId,
`cat ${path}`,
);
if (stderr) {
return null;
}
if (!stdout) return null;
const parsedConfig = parse(stdout, {
maxAliasCount: 10000,
}) as ComposeSpecification;
return parsedConfig;
} catch {
return null;
}
};
export const readComposeFile = async (compose: Compose) => {
const path = getComposePath(compose);
if (existsSync(path)) {
const yamlStr = readFileSync(path, "utf8");
return yamlStr;
}
return null;
};
export const writeDomainsToCompose = async (
compose: Compose,
domains: Domain[],
) => {
try {
const composeConverted = await addDomainToCompose(compose, domains);
const path = getComposePath(compose);
if (!composeConverted) {
return `
echo "❌ Error: Compose file not found";
exit 1;
`;
}
const composeString = stringify(composeConverted, { lineWidth: 1000 });
const encodedContent = encodeBase64(composeString);
return `echo "${encodedContent}" | base64 -d > "${path}";`;
} catch (error) {
// @ts-ignore
return `echo "❌ Has occurred an error: ${error?.message || error}";
exit 1;
`;
}
};
export const addDomainToCompose = async (
compose: Compose,
domains: Domain[],
) => {
const { appName } = compose;
let result: ComposeSpecification | null;
if (compose.serverId) {
result = await loadDockerComposeRemote(compose);
} else {
result = await loadDockerCompose(compose);
}
if (!result) {
return null;
}
if (compose.isolatedDeployment) {
const randomized = randomizeDeployableSpecificationFile(
result,
compose.isolatedDeploymentsVolume,
compose.suffix || compose.appName,
);
result = randomized;
} else if (compose.randomize) {
const randomized = randomizeSpecificationFile(result, compose.suffix);
result = randomized;
}
for (const domain of domains) {
const { serviceName, https } = domain;
if (!serviceName) {
throw new Error(`Domain "${domain.host}" is missing a service name`);
}
if (!result?.services?.[serviceName]) {
throw new Error(
`Domain "${domain.host}" is attached to service "${serviceName}" which does not exist in the compose`,
);
}
const httpLabels = createDomainLabels(
appName,
domain,
domain.customEntrypoint || "web",
);
if (!domain.customEntrypoint && https) {
const httpsLabels = createDomainLabels(appName, domain, "websecure");
httpLabels.push(...httpsLabels);
}
let labels: DefinitionsService["labels"] = [];
if (compose.composeType === "docker-compose") {
if (!result.services[serviceName].labels) {
result.services[serviceName].labels = [];
}
labels = result.services[serviceName].labels;
} else {
// Stack Case
if (!result.services[serviceName].deploy) {
result.services[serviceName].deploy = {};
}
if (!result.services[serviceName].deploy.labels) {
result.services[serviceName].deploy.labels = [];
}
labels = result.services[serviceName].deploy.labels;
}
if (Array.isArray(labels)) {
if (!labels.includes("traefik.enable=true")) {
labels.unshift("traefik.enable=true");
}
labels.unshift(...httpLabels);
if (!compose.isolatedDeployment) {
if (compose.composeType === "docker-compose") {
if (!labels.includes("traefik.docker.network=dokploy-network")) {
labels.unshift("traefik.docker.network=dokploy-network");
}
} else {
// Stack Case
if (!labels.includes("traefik.swarm.network=dokploy-network")) {
labels.unshift("traefik.swarm.network=dokploy-network");
}
}
}
}
if (!compose.isolatedDeployment) {
// Add the dokploy-network to the service
result.services[serviceName].networks = addDokployNetworkToService(
result.services[serviceName].networks,
);
}
}
// Add dokploy-network to the root of the compose file
if (!compose.isolatedDeployment) {
result.networks = addDokployNetworkToRoot(result.networks);
}
return result;
};
export const writeComposeFile = async (
compose: Compose,
composeSpec: ComposeSpecification,
) => {
const path = getComposePath(compose);
try {
const composeFile = stringify(composeSpec, {
lineWidth: 1000,
});
fs.writeFileSync(path, composeFile, "utf8");
} catch (e) {
console.error("Error saving the YAML config file:", e);
}
};
export const createDomainLabels = (
appName: string,
domain: Domain,
entrypoint: string,
) => {
const {
host,
port,
customEntrypoint,
https,
uniqueConfigKey,
certificateType,
path,
customCertResolver,
stripPath,
internalPath,
} = domain;
const routerName = `${appName}-${uniqueConfigKey}-${entrypoint}`;
const labels = [
`traefik.http.routers.${routerName}.rule=Host(\`${host}\`)${path && path !== "/" ? ` && PathPrefix(\`${path}\`)` : ""}`,
`traefik.http.routers.${routerName}.entrypoints=${entrypoint}`,
`traefik.http.services.${routerName}.loadbalancer.server.port=${port}`,
`traefik.http.routers.${routerName}.service=${routerName}`,
];
// Collect middlewares for this router
const middlewares: string[] = [];
const isRedirectRouter = entrypoint === "web" && https && !customEntrypoint;
// Web router with HTTPS only needs redirect — all other middlewares
// run on the websecure router where the request actually lands.
if (isRedirectRouter) {
middlewares.push("redirect-to-https@file");
}
// Add stripPath middleware if needed
if (stripPath && path && path !== "/") {
const middlewareName = `stripprefix-${appName}-${uniqueConfigKey}`;
// Define middleware on web (or custom) entrypoint so Traefik registers it
if (entrypoint === "web" || customEntrypoint) {
labels.push(
`traefik.http.middlewares.${middlewareName}.stripprefix.prefixes=${path}`,
);
}
if (!isRedirectRouter) {
middlewares.push(middlewareName);
}
}
// Add internalPath middleware if needed
if (internalPath && internalPath !== "/" && internalPath.startsWith("/")) {
const middlewareName = `addprefix-${appName}-${uniqueConfigKey}`;
// Define middleware on web (or custom) entrypoint so Traefik registers it
if (entrypoint === "web" || customEntrypoint) {
labels.push(
`traefik.http.middlewares.${middlewareName}.addprefix.prefix=${internalPath}`,
);
}
if (!isRedirectRouter) {
middlewares.push(middlewareName);
}
}
// Add custom middlewares (skip for redirect-only router)
if (!isRedirectRouter && domain.middlewares?.length) {
middlewares.push(...domain.middlewares);
}
// Apply middlewares to router if any exist
if (middlewares.length > 0) {
labels.push(
`traefik.http.routers.${routerName}.middlewares=${middlewares.join(",")}`,
);
}
// Add TLS configuration for websecure
if (entrypoint === "websecure" || (customEntrypoint && https)) {
if (certificateType === "letsencrypt") {
labels.push(
`traefik.http.routers.${routerName}.tls.certresolver=letsencrypt`,
);
} else if (certificateType === "custom" && customCertResolver) {
labels.push(
`traefik.http.routers.${routerName}.tls.certresolver=${customCertResolver}`,
);
} else if (certificateType === "none" && https) {
// No cert resolver, but HTTPS is enabled (default/custom certificate):
// explicitly enable TLS so Traefik serves the router over HTTPS.
labels.push(`traefik.http.routers.${routerName}.tls=true`);
}
}
return labels;
};
export const addDokployNetworkToService = (
networkService: DefinitionsService["networks"],
) => {
let networks = networkService;
const network = "dokploy-network";
const defaultNetwork = "default";
if (!networks) {
networks = [];
}
if (Array.isArray(networks)) {
if (!networks.includes(network)) {
networks.push(network);
}
if (!networks.includes(defaultNetwork)) {
networks.push(defaultNetwork);
}
} else if (networks && typeof networks === "object") {
if (!(network in networks)) {
networks[network] = {};
}
if (!(defaultNetwork in networks)) {
networks[defaultNetwork] = {};
}
}
return networks;
};
export const addDokployNetworkToRoot = (
networkRoot: PropertiesNetworks | undefined,
) => {
let networks = networkRoot;
const network = "dokploy-network";
if (!networks) {
networks = {};
}
if (networks[network] || !networks[network]) {
networks[network] = {
external: true,
};
}
return networks;
};