diff --git a/apps/dokploy/components/dashboard/application/domains/add-domain.tsx b/apps/dokploy/components/dashboard/application/domains/add-domain.tsx index d77796d0a..3f7edb4b2 100644 --- a/apps/dokploy/components/dashboard/application/domains/add-domain.tsx +++ b/apps/dokploy/components/dashboard/application/domains/add-domain.tsx @@ -174,7 +174,9 @@ export const AddDomain = ({ isLoading={isLoadingGenerate} onClick={() => { generateDomain({ - appName: application?.appName || "", + applicationId: + application?.applicationId || "", + // appName: application?.appName || "", }) .then((domain) => { field.onChange(domain); diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index 7f7c201a1..1c645139b 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -39,6 +39,7 @@ import { } from "@/server/utils/filesystem/directory"; import { readConfig, + readConfigInServer, removeTraefikConfig, writeConfig, } from "@/server/utils/traefik/application"; @@ -335,8 +336,15 @@ export const applicationRouter = createTRPCRouter({ .input(apiFindOneApplication) .query(async ({ input }) => { const application = await findApplicationById(input.applicationId); - - const traefikConfig = readConfig(application.appName); + let traefikConfig = null; + if (application.serverId) { + traefikConfig = await readConfigInServer( + application.serverId, + application.appName, + ); + } else { + traefikConfig = readConfig(application.appName); + } return traefikConfig; }), diff --git a/apps/dokploy/server/api/routers/domain.ts b/apps/dokploy/server/api/routers/domain.ts index 309dd91ad..f3d308810 100644 --- a/apps/dokploy/server/api/routers/domain.ts +++ b/apps/dokploy/server/api/routers/domain.ts @@ -47,9 +47,9 @@ export const domainRouter = createTRPCRouter({ return await findDomainsByComposeId(input.composeId); }), generateDomain: protectedProcedure - .input(apiCreateTraefikMeDomain) + .input(apiFindOneApplication) .mutation(async ({ input }) => { - return generateTraefikMeDomain(input.appName); + return generateTraefikMeDomain(input.applicationId); }), update: protectedProcedure diff --git a/apps/dokploy/server/api/services/domain.ts b/apps/dokploy/server/api/services/domain.ts index 53687d28b..9a338c285 100644 --- a/apps/dokploy/server/api/services/domain.ts +++ b/apps/dokploy/server/api/services/domain.ts @@ -1,9 +1,5 @@ import { db } from "@/server/db"; -import { - type apiCreateDomain, - type apiFindDomainByApplication, - domains, -} from "@/server/db/schema"; +import { type apiCreateDomain, domains } from "@/server/db/schema"; import { manageDomain } from "@/server/utils/traefik/domain"; import { generateRandomDomain } from "@/templates/utils"; import { TRPCError } from "@trpc/server"; @@ -38,10 +34,10 @@ export const createDomain = async (input: typeof apiCreateDomain._type) => { }; export const generateTraefikMeDomain = async (appName: string) => { - const admin = await findAdmin(); + const application = await findApplicationById(appName); return generateRandomDomain({ - serverIp: admin.serverIp || "", - projectName: appName, + serverIp: application.server?.ipAddress || "", + projectName: application.appName, }); }; diff --git a/apps/dokploy/server/constants/index.ts b/apps/dokploy/server/constants/index.ts index d02fb5fc9..67a8bfe57 100644 --- a/apps/dokploy/server/constants/index.ts +++ b/apps/dokploy/server/constants/index.ts @@ -7,7 +7,7 @@ export const BASE_PATH = : path.join(process.cwd(), ".docker"); export const IS_CLOUD = process.env.IS_CLOUD === "true"; export const MAIN_TRAEFIK_PATH = `${BASE_PATH}/traefik`; -export const DYNAMIC_TRAEFIK_PATH = `${BASE_PATH}/traefik/dynamic`; +export const DYNAMIC_TRAEFIK_PATH = `/etc/dokploy/traefik/dynamic`; export const LOGS_PATH = `/etc/dokploy/logs`; export const APPLICATIONS_PATH = `/etc/dokploy/applications`; export const COMPOSE_PATH = `/etc/dokploy/compose`; diff --git a/apps/dokploy/server/setup/traefik-setup.ts b/apps/dokploy/server/setup/traefik-setup.ts index 54b26bb12..f78097f08 100644 --- a/apps/dokploy/server/setup/traefik-setup.ts +++ b/apps/dokploy/server/setup/traefik-setup.ts @@ -215,6 +215,55 @@ export const getDefaultTraefikConfig = () => { return yamlStr; }; +export const getDefaultServerTraefikConfig = () => { + const configObject: MainTraefikConfig = { + providers: { + swarm: { + exposedByDefault: false, + watch: false, + }, + docker: { + exposedByDefault: false, + }, + file: { + directory: "/etc/dokploy/traefik/dynamic", + watch: true, + }, + }, + entryPoints: { + web: { + address: `:${TRAEFIK_PORT}`, + }, + websecure: { + address: `:${TRAEFIK_SSL_PORT}`, + http: { + tls: { + certResolver: "letsencrypt", + }, + }, + }, + }, + api: { + insecure: true, + }, + certificatesResolvers: { + letsencrypt: { + acme: { + email: "test@localhost.com", + storage: "/etc/dokploy/traefik/dynamic/acme.json", + httpChallenge: { + entryPoint: "web", + }, + }, + }, + }, + }; + + const yamlStr = dump(configObject); + + return yamlStr; +}; + export const createDefaultTraefikConfig = () => { const mainConfig = path.join(MAIN_TRAEFIK_PATH, "traefik.yml"); const acmeJsonPath = path.join(DYNAMIC_TRAEFIK_PATH, "acme.json"); diff --git a/apps/dokploy/server/utils/servers/command.ts b/apps/dokploy/server/utils/servers/command.ts index 15a3126f3..163e081c1 100644 --- a/apps/dokploy/server/utils/servers/command.ts +++ b/apps/dokploy/server/utils/servers/command.ts @@ -11,7 +11,6 @@ export const executeCommand = async (serverId: string, command: string) => { return new Promise((resolve, reject) => { client .on("ready", () => { - console.log("Client :: ready", command); client.exec(command, (err, stream) => { if (err) { console.error("Execution error:", err); diff --git a/apps/dokploy/server/utils/servers/setup-server.ts b/apps/dokploy/server/utils/servers/setup-server.ts index 59df53f60..bcb97d002 100644 --- a/apps/dokploy/server/utils/servers/setup-server.ts +++ b/apps/dokploy/server/utils/servers/setup-server.ts @@ -22,6 +22,7 @@ import { Client } from "ssh2"; import { readSSHKey } from "../filesystem/ssh"; import { getDefaultMiddlewares, + getDefaultServerTraefikConfig, getDefaultTraefikConfig, } from "@/server/setup/traefik-setup"; @@ -220,7 +221,7 @@ const validatePorts = () => ` `; const createTraefikConfig = () => { - const config = getDefaultTraefikConfig(); + const config = getDefaultServerTraefikConfig(); const command = ` if [ -f "/etc/dokploy/traefik/dynamic/acme.json" ]; then diff --git a/apps/dokploy/server/utils/traefik/application.ts b/apps/dokploy/server/utils/traefik/application.ts index 87216fb3d..cabbf5c2d 100644 --- a/apps/dokploy/server/utils/traefik/application.ts +++ b/apps/dokploy/server/utils/traefik/application.ts @@ -4,6 +4,9 @@ import type { Domain } from "@/server/api/services/domain"; import { DYNAMIC_TRAEFIK_PATH, MAIN_TRAEFIK_PATH } from "@/server/constants"; import { dump, load } from "js-yaml"; import type { FileConfig, HttpLoadBalancerService } from "./file-types"; +import { findServerById } from "@/server/api/services/server"; +import { Client } from "ssh2"; +import { readSSHKey } from "../filesystem/ssh"; export const createTraefikConfig = (appName: string) => { const defaultPort = 3000; @@ -67,6 +70,62 @@ export const loadOrCreateConfig = (appName: string): FileConfig => { return { http: { routers: {}, services: {} } }; }; +export const loadOrCreateConfigRemote = async ( + serverId: string, + appName: string, +) => { + const server = await findServerById(serverId); + if (!server.sshKeyId) return { http: { routers: {}, services: {} } }; + + const keys = await readSSHKey(server.sshKeyId); + const client = new Client(); + let fileConfig: FileConfig; + const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`); + return new Promise((resolve, reject) => { + client + .on("ready", () => { + client.exec(`cat ${configPath}`, (err, stream) => { + if (err) { + console.error("Execution error:", err); + return { http: { routers: {}, services: {} } }; + } + stream + .on("close", (code, signal) => { + client.end(); + if (code === 0) { + if (!fileConfig) { + fileConfig = { http: { routers: {}, services: {} } }; + } + resolve( + (load(fileConfig) as FileConfig) || { + http: { routers: {}, services: {} }, + }, + ); + } else { + console.log(fileConfig); + + resolve({ http: { routers: {}, services: {} } }); + + // reject(new Error(`Command exited with code ${code}`)); + } + }) + .on("data", (data: string) => { + console.log(data.toString()); + fileConfig = data.toString() as unknown as FileConfig; + }) + .stderr.on("data", (data) => {}); + }); + }) + .connect({ + host: server.ipAddress, + port: server.port, + username: server.username, + privateKey: keys.privateKey, + timeout: 99999, + }); + }); +}; + export const readConfig = (appName: string) => { const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`); if (fs.existsSync(configPath)) { @@ -76,6 +135,53 @@ export const readConfig = (appName: string) => { return null; }; +export const readConfigInServer = async (serverId: string, appName: string) => { + const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`); + let content = ""; + // if (fs.existsSync(configPath)) { + // const yamlStr = fs.readFileSync(configPath, "utf8"); + // return yamlStr; + // } + + const client = new Client(); + const server = await findServerById(serverId); + if (!server.sshKeyId) return; + const keys = await readSSHKey(server.sshKeyId); + return new Promise((resolve, reject) => { + client + .on("ready", () => { + const bashCommand = ` + cat ${configPath} + `; + + client.exec(bashCommand, (err, stream) => { + if (err) { + reject(err); + return; + } + stream + .on("close", () => { + client.end(); + resolve(content); + }) + .on("data", (data: string) => { + content = data.toString(); + }) + .stderr.on("data", (data) => { + reject(new Error(`stderr: ${data.toString()}`)); + }); + }); + }) + .connect({ + host: server.ipAddress, + port: server.port, + username: server.username, + privateKey: keys.privateKey, + timeout: 99999, + }); + }); +}; + export const readMonitoringConfig = () => { const configPath = path.join(DYNAMIC_TRAEFIK_PATH, "access.log"); if (fs.existsSync(configPath)) { @@ -122,6 +228,7 @@ export const writeTraefikConfig = ( try { const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`); const yamlStr = dump(traefikConfig); + console.log(yamlStr); fs.writeFileSync(configPath, yamlStr, "utf8"); } catch (e) { console.error("Error saving the YAML config file:", e); diff --git a/apps/dokploy/server/utils/traefik/domain.ts b/apps/dokploy/server/utils/traefik/domain.ts index 3a34f0456..c4ef77c91 100644 --- a/apps/dokploy/server/utils/traefik/domain.ts +++ b/apps/dokploy/server/utils/traefik/domain.ts @@ -3,14 +3,25 @@ import type { ApplicationNested } from "../builders"; import { createServiceConfig, loadOrCreateConfig, + loadOrCreateConfigRemote, removeTraefikConfig, writeTraefikConfig, } from "./application"; import type { FileConfig, HttpRouter } from "./file-types"; +import { DYNAMIC_TRAEFIK_PATH } from "@/server/constants"; +import path from "node:path"; +import { dump } from "js-yaml"; +import { executeCommand } from "../servers/command"; export const manageDomain = async (app: ApplicationNested, domain: Domain) => { const { appName } = app; - const config: FileConfig = loadOrCreateConfig(appName); + let config: FileConfig; + + if (app.serverId) { + config = await loadOrCreateConfigRemote(app.serverId, appName); + } else { + config = loadOrCreateConfig(appName); + } const serviceName = `${appName}-service-${domain.uniqueConfigKey}`; const routerName = `${appName}-router-${domain.uniqueConfigKey}`; const routerNameSecure = `${appName}-router-websecure-${domain.uniqueConfigKey}`; @@ -36,7 +47,21 @@ export const manageDomain = async (app: ApplicationNested, domain: Domain) => { } config.http.services[serviceName] = createServiceConfig(appName, domain); - writeTraefikConfig(config, appName); + + if (app.serverId) { + const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`); + const yamlStr = dump(config); + + console.log(yamlStr); + + const command = ` + echo '${yamlStr}' > ${configPath} + `; + + await executeCommand(app.serverId, command); + } else { + writeTraefikConfig(config, appName); + } }; export const removeDomain = async (appName: string, uniqueKey: number) => { diff --git a/apps/dokploy/templates/utils/index.ts b/apps/dokploy/templates/utils/index.ts index 7be92dad2..6a87abdd2 100644 --- a/apps/dokploy/templates/utils/index.ts +++ b/apps/dokploy/templates/utils/index.ts @@ -5,6 +5,7 @@ import type { Domain } from "@/server/api/services/domain"; import { TRPCError } from "@trpc/server"; import { templates } from "../templates"; import type { TemplatesKeys } from "../types/templates-data.type"; +import { IS_CLOUD } from "@/server/constants"; export interface Schema { serverIp: string; @@ -28,7 +29,7 @@ export const generateRandomDomain = ({ }: Schema): string => { const hash = randomBytes(3).toString("hex"); const slugIp = serverIp.replaceAll(".", "-"); - return `${projectName}-${hash}${process.env.NODE_ENV === "production" ? `-${slugIp}` : ""}.traefik.me`; + return `${projectName}-${hash}${process.env.NODE_ENV === "production" || IS_CLOUD ? `-${slugIp}` : ""}.traefik.me`; }; export const generateHash = (projectName: string, quantity = 3): string => {