mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: add domains in external server
This commit is contained in:
@@ -174,7 +174,9 @@ export const AddDomain = ({
|
|||||||
isLoading={isLoadingGenerate}
|
isLoading={isLoadingGenerate}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
generateDomain({
|
generateDomain({
|
||||||
appName: application?.appName || "",
|
applicationId:
|
||||||
|
application?.applicationId || "",
|
||||||
|
// appName: application?.appName || "",
|
||||||
})
|
})
|
||||||
.then((domain) => {
|
.then((domain) => {
|
||||||
field.onChange(domain);
|
field.onChange(domain);
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
} from "@/server/utils/filesystem/directory";
|
} from "@/server/utils/filesystem/directory";
|
||||||
import {
|
import {
|
||||||
readConfig,
|
readConfig,
|
||||||
|
readConfigInServer,
|
||||||
removeTraefikConfig,
|
removeTraefikConfig,
|
||||||
writeConfig,
|
writeConfig,
|
||||||
} from "@/server/utils/traefik/application";
|
} from "@/server/utils/traefik/application";
|
||||||
@@ -335,8 +336,15 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
let traefikConfig = null;
|
||||||
const traefikConfig = readConfig(application.appName);
|
if (application.serverId) {
|
||||||
|
traefikConfig = await readConfigInServer(
|
||||||
|
application.serverId,
|
||||||
|
application.appName,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
traefikConfig = readConfig(application.appName);
|
||||||
|
}
|
||||||
return traefikConfig;
|
return traefikConfig;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -47,9 +47,9 @@ export const domainRouter = createTRPCRouter({
|
|||||||
return await findDomainsByComposeId(input.composeId);
|
return await findDomainsByComposeId(input.composeId);
|
||||||
}),
|
}),
|
||||||
generateDomain: protectedProcedure
|
generateDomain: protectedProcedure
|
||||||
.input(apiCreateTraefikMeDomain)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
return generateTraefikMeDomain(input.appName);
|
return generateTraefikMeDomain(input.applicationId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import {
|
import { type apiCreateDomain, domains } from "@/server/db/schema";
|
||||||
type apiCreateDomain,
|
|
||||||
type apiFindDomainByApplication,
|
|
||||||
domains,
|
|
||||||
} from "@/server/db/schema";
|
|
||||||
import { manageDomain } from "@/server/utils/traefik/domain";
|
import { manageDomain } from "@/server/utils/traefik/domain";
|
||||||
import { generateRandomDomain } from "@/templates/utils";
|
import { generateRandomDomain } from "@/templates/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
@@ -38,10 +34,10 @@ export const createDomain = async (input: typeof apiCreateDomain._type) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const generateTraefikMeDomain = async (appName: string) => {
|
export const generateTraefikMeDomain = async (appName: string) => {
|
||||||
const admin = await findAdmin();
|
const application = await findApplicationById(appName);
|
||||||
return generateRandomDomain({
|
return generateRandomDomain({
|
||||||
serverIp: admin.serverIp || "",
|
serverIp: application.server?.ipAddress || "",
|
||||||
projectName: appName,
|
projectName: application.appName,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const BASE_PATH =
|
|||||||
: path.join(process.cwd(), ".docker");
|
: path.join(process.cwd(), ".docker");
|
||||||
export const IS_CLOUD = process.env.IS_CLOUD === "true";
|
export const IS_CLOUD = process.env.IS_CLOUD === "true";
|
||||||
export const MAIN_TRAEFIK_PATH = `${BASE_PATH}/traefik`;
|
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 LOGS_PATH = `/etc/dokploy/logs`;
|
||||||
export const APPLICATIONS_PATH = `/etc/dokploy/applications`;
|
export const APPLICATIONS_PATH = `/etc/dokploy/applications`;
|
||||||
export const COMPOSE_PATH = `/etc/dokploy/compose`;
|
export const COMPOSE_PATH = `/etc/dokploy/compose`;
|
||||||
|
|||||||
@@ -215,6 +215,55 @@ export const getDefaultTraefikConfig = () => {
|
|||||||
return yamlStr;
|
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 = () => {
|
export const createDefaultTraefikConfig = () => {
|
||||||
const mainConfig = path.join(MAIN_TRAEFIK_PATH, "traefik.yml");
|
const mainConfig = path.join(MAIN_TRAEFIK_PATH, "traefik.yml");
|
||||||
const acmeJsonPath = path.join(DYNAMIC_TRAEFIK_PATH, "acme.json");
|
const acmeJsonPath = path.join(DYNAMIC_TRAEFIK_PATH, "acme.json");
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export const executeCommand = async (serverId: string, command: string) => {
|
|||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
client
|
client
|
||||||
.on("ready", () => {
|
.on("ready", () => {
|
||||||
console.log("Client :: ready", command);
|
|
||||||
client.exec(command, (err, stream) => {
|
client.exec(command, (err, stream) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error("Execution error:", err);
|
console.error("Execution error:", err);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { Client } from "ssh2";
|
|||||||
import { readSSHKey } from "../filesystem/ssh";
|
import { readSSHKey } from "../filesystem/ssh";
|
||||||
import {
|
import {
|
||||||
getDefaultMiddlewares,
|
getDefaultMiddlewares,
|
||||||
|
getDefaultServerTraefikConfig,
|
||||||
getDefaultTraefikConfig,
|
getDefaultTraefikConfig,
|
||||||
} from "@/server/setup/traefik-setup";
|
} from "@/server/setup/traefik-setup";
|
||||||
|
|
||||||
@@ -220,7 +221,7 @@ const validatePorts = () => `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const createTraefikConfig = () => {
|
const createTraefikConfig = () => {
|
||||||
const config = getDefaultTraefikConfig();
|
const config = getDefaultServerTraefikConfig();
|
||||||
|
|
||||||
const command = `
|
const command = `
|
||||||
if [ -f "/etc/dokploy/traefik/dynamic/acme.json" ]; then
|
if [ -f "/etc/dokploy/traefik/dynamic/acme.json" ]; then
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import type { Domain } from "@/server/api/services/domain";
|
|||||||
import { DYNAMIC_TRAEFIK_PATH, MAIN_TRAEFIK_PATH } from "@/server/constants";
|
import { DYNAMIC_TRAEFIK_PATH, MAIN_TRAEFIK_PATH } from "@/server/constants";
|
||||||
import { dump, load } from "js-yaml";
|
import { dump, load } from "js-yaml";
|
||||||
import type { FileConfig, HttpLoadBalancerService } from "./file-types";
|
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) => {
|
export const createTraefikConfig = (appName: string) => {
|
||||||
const defaultPort = 3000;
|
const defaultPort = 3000;
|
||||||
@@ -67,6 +70,62 @@ export const loadOrCreateConfig = (appName: string): FileConfig => {
|
|||||||
return { http: { routers: {}, services: {} } };
|
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<FileConfig>((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) => {
|
export const readConfig = (appName: string) => {
|
||||||
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
||||||
if (fs.existsSync(configPath)) {
|
if (fs.existsSync(configPath)) {
|
||||||
@@ -76,6 +135,53 @@ export const readConfig = (appName: string) => {
|
|||||||
return null;
|
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<string>((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 = () => {
|
export const readMonitoringConfig = () => {
|
||||||
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, "access.log");
|
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, "access.log");
|
||||||
if (fs.existsSync(configPath)) {
|
if (fs.existsSync(configPath)) {
|
||||||
@@ -122,6 +228,7 @@ export const writeTraefikConfig = (
|
|||||||
try {
|
try {
|
||||||
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
||||||
const yamlStr = dump(traefikConfig);
|
const yamlStr = dump(traefikConfig);
|
||||||
|
console.log(yamlStr);
|
||||||
fs.writeFileSync(configPath, yamlStr, "utf8");
|
fs.writeFileSync(configPath, yamlStr, "utf8");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error saving the YAML config file:", e);
|
console.error("Error saving the YAML config file:", e);
|
||||||
|
|||||||
@@ -3,14 +3,25 @@ import type { ApplicationNested } from "../builders";
|
|||||||
import {
|
import {
|
||||||
createServiceConfig,
|
createServiceConfig,
|
||||||
loadOrCreateConfig,
|
loadOrCreateConfig,
|
||||||
|
loadOrCreateConfigRemote,
|
||||||
removeTraefikConfig,
|
removeTraefikConfig,
|
||||||
writeTraefikConfig,
|
writeTraefikConfig,
|
||||||
} from "./application";
|
} from "./application";
|
||||||
import type { FileConfig, HttpRouter } from "./file-types";
|
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) => {
|
export const manageDomain = async (app: ApplicationNested, domain: Domain) => {
|
||||||
const { appName } = app;
|
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 serviceName = `${appName}-service-${domain.uniqueConfigKey}`;
|
||||||
const routerName = `${appName}-router-${domain.uniqueConfigKey}`;
|
const routerName = `${appName}-router-${domain.uniqueConfigKey}`;
|
||||||
const routerNameSecure = `${appName}-router-websecure-${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);
|
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) => {
|
export const removeDomain = async (appName: string, uniqueKey: number) => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { Domain } from "@/server/api/services/domain";
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { templates } from "../templates";
|
import { templates } from "../templates";
|
||||||
import type { TemplatesKeys } from "../types/templates-data.type";
|
import type { TemplatesKeys } from "../types/templates-data.type";
|
||||||
|
import { IS_CLOUD } from "@/server/constants";
|
||||||
|
|
||||||
export interface Schema {
|
export interface Schema {
|
||||||
serverIp: string;
|
serverIp: string;
|
||||||
@@ -28,7 +29,7 @@ export const generateRandomDomain = ({
|
|||||||
}: Schema): string => {
|
}: Schema): string => {
|
||||||
const hash = randomBytes(3).toString("hex");
|
const hash = randomBytes(3).toString("hex");
|
||||||
const slugIp = serverIp.replaceAll(".", "-");
|
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 => {
|
export const generateHash = (projectName: string, quantity = 3): string => {
|
||||||
|
|||||||
Reference in New Issue
Block a user