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 => { const path = getComposePath(compose); if (existsSync(path)) { const yamlStr = readFileSync(path, "utf8"); const parsedConfig = parse(yamlStr) as ComposeSpecification; return parsedConfig; } return null; }; export const loadDockerComposeRemote = async ( compose: Compose, ): Promise => { 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) 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, "web"); if (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: "web" | "websecure", ) => { const { host, port, 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[] = []; // Add HTTPS redirect for web entrypoint (must be first) if (entrypoint === "web" && https) { middlewares.push("redirect-to-https@file"); } // Add stripPath middleware if needed if (stripPath && path && path !== "/") { const middlewareName = `stripprefix-${appName}-${uniqueConfigKey}`; // Only define middleware once (on web entrypoint) if (entrypoint === "web") { labels.push( `traefik.http.middlewares.${middlewareName}.stripprefix.prefixes=${path}`, ); } middlewares.push(middlewareName); } // Add internalPath middleware if needed if (internalPath && internalPath !== "/" && internalPath.startsWith("/")) { const middlewareName = `addprefix-${appName}-${uniqueConfigKey}`; // Only define middleware once (on web entrypoint) if (entrypoint === "web") { labels.push( `traefik.http.middlewares.${middlewareName}.addprefix.prefix=${internalPath}`, ); } middlewares.push(middlewareName); } // 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") { 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}`, ); } } 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; };