mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-06 12:12:16 +00:00
refactor(traefik): update Traefik initialization to support standalone and service modes, enhance port handling with protocol specification
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
|
import { useTranslation } from "next-i18next";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -10,8 +11,6 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { useTranslation } from "next-i18next";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { EditTraefikEnv } from "../../web-server/edit-traefik-env";
|
import { EditTraefikEnv } from "../../web-server/edit-traefik-env";
|
||||||
import { ManageTraefikPorts } from "../../web-server/manage-traefik-ports";
|
import { ManageTraefikPorts } from "../../web-server/manage-traefik-ports";
|
||||||
import { ShowModalLogs } from "../../web-server/show-modal-logs";
|
import { ShowModalLogs } from "../../web-server/show-modal-logs";
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { ArrowRightLeft, Plus, Trash2 } from "lucide-react";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
|
import type React from "react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useFieldArray, useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
@@ -19,15 +27,15 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { ArrowRightLeft, Plus, Trash2 } from "lucide-react";
|
|
||||||
import { useTranslation } from "next-i18next";
|
|
||||||
import type React from "react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useFieldArray, useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -37,6 +45,7 @@ interface Props {
|
|||||||
const PortSchema = z.object({
|
const PortSchema = z.object({
|
||||||
targetPort: z.number().min(1, "Target port is required"),
|
targetPort: z.number().min(1, "Target port is required"),
|
||||||
publishedPort: z.number().min(1, "Published port is required"),
|
publishedPort: z.number().min(1, "Published port is required"),
|
||||||
|
protocol: z.enum(["tcp", "udp", "sctp"]),
|
||||||
});
|
});
|
||||||
|
|
||||||
const TraefikPortsSchema = z.object({
|
const TraefikPortsSchema = z.object({
|
||||||
@@ -75,12 +84,17 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentPorts) {
|
if (currentPorts) {
|
||||||
form.reset({ ports: currentPorts });
|
form.reset({
|
||||||
|
ports: currentPorts.map((port) => ({
|
||||||
|
...port,
|
||||||
|
protocol: port.protocol as "tcp" | "udp" | "sctp",
|
||||||
|
})),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [currentPorts, form]);
|
}, [currentPorts, form]);
|
||||||
|
|
||||||
const handleAddPort = () => {
|
const handleAddPort = () => {
|
||||||
append({ targetPort: 0, publishedPort: 0 });
|
append({ targetPort: 0, publishedPort: 0, protocol: "tcp" });
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (data: TraefikPortsForm) => {
|
const onSubmit = async (data: TraefikPortsForm) => {
|
||||||
@@ -96,7 +110,9 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div onClick={() => setOpen(true)}>{children}</div>
|
<button type="button" onClick={() => setOpen(true)}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogContent className="sm:max-w-3xl">
|
<DialogContent className="sm:max-w-3xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -143,8 +159,8 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
<ScrollArea className="h-[400px] pr-4">
|
<ScrollArea className="h-[400px] pr-4">
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
<Card key={field.id}>
|
<Card key={field.id} className="bg-transparent">
|
||||||
<CardContent className="grid grid-cols-[1fr_1fr_auto] gap-4 p-4 transparent">
|
<CardContent className="grid grid-cols-[1fr_1fr_1fr_auto] gap-4 p-4 transparent">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={`ports.${index}.targetPort`}
|
name={`ports.${index}.targetPort`}
|
||||||
@@ -168,7 +184,6 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
value={field.value || ""}
|
value={field.value || ""}
|
||||||
className="w-full dark:bg-black"
|
|
||||||
placeholder="e.g. 8080"
|
placeholder="e.g. 8080"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -200,7 +215,6 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
value={field.value || ""}
|
value={field.value || ""}
|
||||||
className="w-full dark:bg-black"
|
|
||||||
placeholder="e.g. 80"
|
placeholder="e.g. 80"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -208,6 +222,42 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`ports.${index}.protocol`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="text-sm font-medium text-muted-foreground">
|
||||||
|
Protocol
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a protocol" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{["tcp", "udp", "sctp"].map(
|
||||||
|
(protocol) => (
|
||||||
|
<SelectItem
|
||||||
|
key={protocol}
|
||||||
|
value={protocol}
|
||||||
|
>
|
||||||
|
{protocol}
|
||||||
|
</SelectItem>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex items-end">
|
<div className="flex items-end">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,3 +1,55 @@
|
|||||||
|
import {
|
||||||
|
canAccessToTraefikFiles,
|
||||||
|
checkGPUStatus,
|
||||||
|
cleanStoppedContainers,
|
||||||
|
cleanUpDockerBuilder,
|
||||||
|
cleanUpSystemPrune,
|
||||||
|
cleanUpUnusedImages,
|
||||||
|
cleanUpUnusedVolumes,
|
||||||
|
DEFAULT_UPDATE_DATA,
|
||||||
|
execAsync,
|
||||||
|
execAsyncRemote,
|
||||||
|
findServerById,
|
||||||
|
findUserById,
|
||||||
|
getDokployImage,
|
||||||
|
getDokployImageTag,
|
||||||
|
getLogCleanupStatus,
|
||||||
|
getUpdateData,
|
||||||
|
IS_CLOUD,
|
||||||
|
parseRawConfig,
|
||||||
|
paths,
|
||||||
|
prepareEnvironmentVariables,
|
||||||
|
processLogs,
|
||||||
|
pullLatestRelease,
|
||||||
|
readConfig,
|
||||||
|
readConfigInPath,
|
||||||
|
readDirectory,
|
||||||
|
readEnvironmentVariables,
|
||||||
|
readMainConfig,
|
||||||
|
readMonitoringConfig,
|
||||||
|
readPorts,
|
||||||
|
recreateDirectory,
|
||||||
|
reloadDockerResource,
|
||||||
|
sendDockerCleanupNotifications,
|
||||||
|
setupGPUSupport,
|
||||||
|
spawnAsync,
|
||||||
|
startLogCleanup,
|
||||||
|
stopLogCleanup,
|
||||||
|
updateLetsEncryptEmail,
|
||||||
|
updateServerById,
|
||||||
|
updateServerTraefik,
|
||||||
|
updateUser,
|
||||||
|
writeConfig,
|
||||||
|
writeMainConfig,
|
||||||
|
writeTraefikConfigInPath,
|
||||||
|
writeTraefikSetup,
|
||||||
|
} from "@dokploy/server";
|
||||||
|
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
import { dump, load } from "js-yaml";
|
||||||
|
import { scheduledJobs, scheduleJob } from "node-schedule";
|
||||||
|
import { z } from "zod";
|
||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import {
|
import {
|
||||||
apiAssignDomain,
|
apiAssignDomain,
|
||||||
@@ -11,54 +63,6 @@ import {
|
|||||||
apiUpdateDockerCleanup,
|
apiUpdateDockerCleanup,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import { removeJob, schedule } from "@/server/utils/backup";
|
import { removeJob, schedule } from "@/server/utils/backup";
|
||||||
import {
|
|
||||||
DEFAULT_UPDATE_DATA,
|
|
||||||
IS_CLOUD,
|
|
||||||
canAccessToTraefikFiles,
|
|
||||||
cleanStoppedContainers,
|
|
||||||
cleanUpDockerBuilder,
|
|
||||||
cleanUpSystemPrune,
|
|
||||||
cleanUpUnusedImages,
|
|
||||||
cleanUpUnusedVolumes,
|
|
||||||
execAsync,
|
|
||||||
execAsyncRemote,
|
|
||||||
findServerById,
|
|
||||||
findUserById,
|
|
||||||
getDokployImage,
|
|
||||||
getDokployImageTag,
|
|
||||||
getLogCleanupStatus,
|
|
||||||
getUpdateData,
|
|
||||||
initializeTraefik,
|
|
||||||
parseRawConfig,
|
|
||||||
paths,
|
|
||||||
prepareEnvironmentVariables,
|
|
||||||
processLogs,
|
|
||||||
pullLatestRelease,
|
|
||||||
readConfig,
|
|
||||||
readConfigInPath,
|
|
||||||
readDirectory,
|
|
||||||
readMainConfig,
|
|
||||||
readMonitoringConfig,
|
|
||||||
recreateDirectory,
|
|
||||||
sendDockerCleanupNotifications,
|
|
||||||
spawnAsync,
|
|
||||||
startLogCleanup,
|
|
||||||
stopLogCleanup,
|
|
||||||
updateLetsEncryptEmail,
|
|
||||||
updateServerById,
|
|
||||||
updateServerTraefik,
|
|
||||||
updateUser,
|
|
||||||
writeConfig,
|
|
||||||
writeMainConfig,
|
|
||||||
writeTraefikConfigInPath,
|
|
||||||
} from "@dokploy/server";
|
|
||||||
import { checkGPUStatus, setupGPUSupport } from "@dokploy/server";
|
|
||||||
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { sql } from "drizzle-orm";
|
|
||||||
import { dump, load } from "js-yaml";
|
|
||||||
import { scheduleJob, scheduledJobs } from "node-schedule";
|
|
||||||
import { z } from "zod";
|
|
||||||
import packageInfo from "../../../package.json";
|
import packageInfo from "../../../package.json";
|
||||||
import { appRouter } from "../root";
|
import { appRouter } from "../root";
|
||||||
import {
|
import {
|
||||||
@@ -73,10 +77,7 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const { stdout } = await execAsync(
|
await reloadDockerResource("dokploy");
|
||||||
"docker service inspect dokploy --format '{{.ID}}'",
|
|
||||||
);
|
|
||||||
await execAsync(`docker service update --force ${stdout.trim()}`);
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
cleanRedis: adminProcedure.mutation(async () => {
|
cleanRedis: adminProcedure.mutation(async () => {
|
||||||
@@ -101,20 +102,15 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
await reloadDockerResource("dokploy-redis");
|
||||||
|
|
||||||
await execAsync("docker service scale dokploy-redis=0");
|
|
||||||
await execAsync("docker service scale dokploy-redis=1");
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
reloadTraefik: adminProcedure
|
reloadTraefik: adminProcedure
|
||||||
.input(apiServerSchema)
|
.input(apiServerSchema)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
if (input?.serverId) {
|
await reloadDockerResource("dokploy-traefik", input?.serverId);
|
||||||
await execAsync("docker restart dokploy-traefik");
|
|
||||||
} else if (!IS_CLOUD) {
|
|
||||||
await execAsync("docker restart dokploy-traefik");
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
@@ -124,17 +120,28 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
toggleDashboard: adminProcedure
|
toggleDashboard: adminProcedure
|
||||||
.input(apiEnableDashboard)
|
.input(apiEnableDashboard)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const ports = (await getTraefikPorts(input.serverId)).filter(
|
const ports = await readPorts("dokploy-traefik", input.serverId);
|
||||||
(port) =>
|
const env = await readEnvironmentVariables(
|
||||||
port.targetPort !== 80 &&
|
"dokploy-traefik",
|
||||||
port.targetPort !== 443 &&
|
input.serverId,
|
||||||
port.targetPort !== 8080,
|
|
||||||
);
|
);
|
||||||
await initializeTraefik({
|
const preparedEnv = prepareEnvironmentVariables(env);
|
||||||
additionalPorts: ports,
|
let newPorts = ports;
|
||||||
enableDashboard: input.enableDashboard,
|
// If receive true, add 8080 to ports
|
||||||
|
if (input.enableDashboard) {
|
||||||
|
newPorts.push({
|
||||||
|
targetPort: 8080,
|
||||||
|
publishedPort: 8080,
|
||||||
|
protocol: "tcp",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
newPorts = ports.filter((port) => port.targetPort !== 8080);
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeTraefikSetup({
|
||||||
|
env: preparedEnv,
|
||||||
|
additionalPorts: newPorts,
|
||||||
serverId: input.serverId,
|
serverId: input.serverId,
|
||||||
force: true,
|
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
@@ -551,29 +558,23 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
readTraefikEnv: adminProcedure
|
readTraefikEnv: adminProcedure
|
||||||
.input(apiServerSchema)
|
.input(apiServerSchema)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const command =
|
const envVars = await readEnvironmentVariables(
|
||||||
"docker container inspect dokploy-traefik --format '{{json .Config.Env}}'";
|
"dokploy-traefik",
|
||||||
|
input?.serverId,
|
||||||
let result = "";
|
);
|
||||||
if (input?.serverId) {
|
return envVars;
|
||||||
const execResult = await execAsyncRemote(input.serverId, command);
|
|
||||||
result = execResult.stdout;
|
|
||||||
} else {
|
|
||||||
const execResult = await execAsync(command);
|
|
||||||
result = execResult.stdout;
|
|
||||||
}
|
|
||||||
const envVars = JSON.parse(result.trim());
|
|
||||||
return envVars.join("\n");
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
writeTraefikEnv: adminProcedure
|
writeTraefikEnv: adminProcedure
|
||||||
.input(z.object({ env: z.string(), serverId: z.string().optional() }))
|
.input(z.object({ env: z.string(), serverId: z.string().optional() }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const envs = prepareEnvironmentVariables(input.env);
|
const envs = prepareEnvironmentVariables(input.env);
|
||||||
await initializeTraefik({
|
const ports = await readPorts("dokploy-traefik", input?.serverId);
|
||||||
|
|
||||||
|
await writeTraefikSetup({
|
||||||
env: envs,
|
env: envs,
|
||||||
|
additionalPorts: ports,
|
||||||
serverId: input.serverId,
|
serverId: input.serverId,
|
||||||
force: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -581,22 +582,8 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
haveTraefikDashboardPortEnabled: adminProcedure
|
haveTraefikDashboardPortEnabled: adminProcedure
|
||||||
.input(apiServerSchema)
|
.input(apiServerSchema)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const command = `docker container inspect --format='{{json .NetworkSettings.Ports}}' dokploy-traefik`;
|
const ports = await readPorts("dokploy-traefik", input?.serverId);
|
||||||
|
return ports.some((port) => port.targetPort === 8080);
|
||||||
let stdout = "";
|
|
||||||
if (input?.serverId) {
|
|
||||||
const result = await execAsyncRemote(input.serverId, command);
|
|
||||||
stdout = result.stdout;
|
|
||||||
} else if (!IS_CLOUD) {
|
|
||||||
const result = await execAsync(command);
|
|
||||||
stdout = result.stdout;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ports = JSON.parse(stdout.trim());
|
|
||||||
return Object.entries(ports).some(([containerPort, bindings]) => {
|
|
||||||
const [port] = containerPort.split("/");
|
|
||||||
return port === "8080" && bindings && (bindings as any[]).length > 0;
|
|
||||||
});
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
readStatsLogs: adminProcedure
|
readStatsLogs: adminProcedure
|
||||||
@@ -793,6 +780,7 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
z.object({
|
z.object({
|
||||||
targetPort: z.number(),
|
targetPort: z.number(),
|
||||||
publishedPort: z.number(),
|
publishedPort: z.number(),
|
||||||
|
protocol: z.enum(["tcp", "udp", "sctp"]),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
@@ -805,10 +793,16 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
message: "Please set a serverId to update Traefik ports",
|
message: "Please set a serverId to update Traefik ports",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await initializeTraefik({
|
const env = await readEnvironmentVariables(
|
||||||
serverId: input.serverId,
|
"dokploy-traefik",
|
||||||
|
input?.serverId,
|
||||||
|
);
|
||||||
|
const preparedEnv = prepareEnvironmentVariables(env);
|
||||||
|
|
||||||
|
await writeTraefikSetup({
|
||||||
|
env: preparedEnv,
|
||||||
additionalPorts: input.additionalPorts,
|
additionalPorts: input.additionalPorts,
|
||||||
force: true,
|
serverId: input.serverId,
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -825,7 +819,8 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
getTraefikPorts: adminProcedure
|
getTraefikPorts: adminProcedure
|
||||||
.input(apiServerSchema)
|
.input(apiServerSchema)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return await getTraefikPorts(input?.serverId);
|
const ports = await readPorts("dokploy-traefik", input?.serverId);
|
||||||
|
return ports;
|
||||||
}),
|
}),
|
||||||
updateLogCleanup: adminProcedure
|
updateLogCleanup: adminProcedure
|
||||||
.input(
|
.input(
|
||||||
|
|||||||
@@ -1,10 +1,3 @@
|
|||||||
import {
|
|
||||||
createDefaultMiddlewares,
|
|
||||||
createDefaultServerTraefikConfig,
|
|
||||||
createDefaultTraefikConfig,
|
|
||||||
initializeTraefik,
|
|
||||||
} from "@dokploy/server/setup/traefik-setup";
|
|
||||||
|
|
||||||
import { execAsync } from "@dokploy/server";
|
import { execAsync } from "@dokploy/server";
|
||||||
import { setupDirectories } from "@dokploy/server/setup/config-paths";
|
import { setupDirectories } from "@dokploy/server/setup/config-paths";
|
||||||
import { initializePostgres } from "@dokploy/server/setup/postgres-setup";
|
import { initializePostgres } from "@dokploy/server/setup/postgres-setup";
|
||||||
@@ -13,6 +6,13 @@ import {
|
|||||||
initializeNetwork,
|
initializeNetwork,
|
||||||
initializeSwarm,
|
initializeSwarm,
|
||||||
} from "@dokploy/server/setup/setup";
|
} from "@dokploy/server/setup/setup";
|
||||||
|
import {
|
||||||
|
createDefaultMiddlewares,
|
||||||
|
createDefaultServerTraefikConfig,
|
||||||
|
createDefaultTraefikConfig,
|
||||||
|
initializeStandaloneTraefik,
|
||||||
|
} from "@dokploy/server/setup/traefik-setup";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
setupDirectories();
|
setupDirectories();
|
||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
createDefaultTraefikConfig();
|
createDefaultTraefikConfig();
|
||||||
createDefaultServerTraefikConfig();
|
createDefaultServerTraefikConfig();
|
||||||
await execAsync("docker pull traefik:v3.1.2");
|
await execAsync("docker pull traefik:v3.1.2");
|
||||||
await initializeTraefik();
|
await initializeStandaloneTraefik();
|
||||||
await initializeRedis();
|
await initializeRedis();
|
||||||
await initializePostgres();
|
await initializePostgres();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ import {
|
|||||||
execAsync,
|
execAsync,
|
||||||
execAsyncRemote,
|
execAsyncRemote,
|
||||||
} from "@dokploy/server/utils/process/execAsync";
|
} from "@dokploy/server/utils/process/execAsync";
|
||||||
|
import {
|
||||||
|
initializeStandaloneTraefik,
|
||||||
|
initializeTraefikService,
|
||||||
|
type TraefikOptions,
|
||||||
|
} from "../setup/traefik-setup";
|
||||||
|
|
||||||
export interface IUpdateData {
|
export interface IUpdateData {
|
||||||
latestVersion: string | null;
|
latestVersion: string | null;
|
||||||
@@ -243,3 +248,165 @@ export const cleanupFullDocker = async (serverId?: string | null) => {
|
|||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDockerResourceType = async (
|
||||||
|
resourceName: string,
|
||||||
|
serverId?: string,
|
||||||
|
) => {
|
||||||
|
let result = "";
|
||||||
|
const command = `
|
||||||
|
RESOURCE_NAME="${resourceName}"
|
||||||
|
if docker service inspect "$RESOURCE_NAME" &>/dev/null; then
|
||||||
|
echo "service"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if docker inspect "$RESOURCE_NAME" &>/dev/null; then
|
||||||
|
echo "standalone"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "unknown"
|
||||||
|
exit 0
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
const { stdout } = await execAsyncRemote(serverId, command);
|
||||||
|
result = stdout.trim();
|
||||||
|
} else {
|
||||||
|
const { stdout } = await execAsync(command);
|
||||||
|
result = stdout.trim();
|
||||||
|
}
|
||||||
|
if (result === "service") {
|
||||||
|
return "service";
|
||||||
|
}
|
||||||
|
if (result === "standalone") {
|
||||||
|
return "standalone";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reloadDockerResource = async (
|
||||||
|
resourceName: string,
|
||||||
|
serverId?: string,
|
||||||
|
) => {
|
||||||
|
const resourceType = await getDockerResourceType(resourceName, serverId);
|
||||||
|
let command = "";
|
||||||
|
if (resourceType === "service") {
|
||||||
|
command = `docker service update --force ${resourceName}`;
|
||||||
|
} else {
|
||||||
|
command = `docker restart ${resourceName}`;
|
||||||
|
}
|
||||||
|
if (serverId) {
|
||||||
|
await execAsyncRemote(serverId, command);
|
||||||
|
} else {
|
||||||
|
await execAsync(command);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readEnvironmentVariables = async (
|
||||||
|
resourceName: string,
|
||||||
|
serverId?: string,
|
||||||
|
) => {
|
||||||
|
const resourceType = await getDockerResourceType(resourceName, serverId);
|
||||||
|
let command = "";
|
||||||
|
if (resourceType === "service") {
|
||||||
|
command = `docker service inspect ${resourceName} --format '{{json .Spec.TaskTemplate.ContainerSpec.Env}}'`;
|
||||||
|
} else {
|
||||||
|
command = `docker container inspect ${resourceName} --format '{{json .Config.Env}}'`;
|
||||||
|
}
|
||||||
|
let result = "";
|
||||||
|
if (serverId) {
|
||||||
|
const { stdout } = await execAsyncRemote(serverId, command);
|
||||||
|
result = stdout.trim();
|
||||||
|
} else {
|
||||||
|
const { stdout } = await execAsync(command);
|
||||||
|
result = stdout.trim();
|
||||||
|
}
|
||||||
|
if (result === "null") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return JSON.parse(result)?.join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readPorts = async (
|
||||||
|
resourceName: string,
|
||||||
|
serverId?: string,
|
||||||
|
): Promise<
|
||||||
|
{ targetPort: number; publishedPort: number; protocol?: string }[]
|
||||||
|
> => {
|
||||||
|
const resourceType = await getDockerResourceType(resourceName, serverId);
|
||||||
|
let command = "";
|
||||||
|
if (resourceType === "service") {
|
||||||
|
command = `docker service inspect ${resourceName} --format '{{json .Spec.EndpointSpec.Ports}}'`;
|
||||||
|
} else {
|
||||||
|
command = `docker container inspect ${resourceName} --format '{{json .NetworkSettings.Ports}}'`;
|
||||||
|
}
|
||||||
|
let result = "";
|
||||||
|
if (serverId) {
|
||||||
|
const { stdout } = await execAsyncRemote(serverId, command);
|
||||||
|
result = stdout.trim();
|
||||||
|
} else {
|
||||||
|
const { stdout } = await execAsync(command);
|
||||||
|
result = stdout.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result === "null") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedResult = JSON.parse(result);
|
||||||
|
|
||||||
|
if (resourceType === "service") {
|
||||||
|
return parsedResult
|
||||||
|
.map((port: any) => ({
|
||||||
|
targetPort: port.TargetPort,
|
||||||
|
publishedPort: port.PublishedPort,
|
||||||
|
protocol: port.Protocol,
|
||||||
|
}))
|
||||||
|
.filter((port: any) => port.targetPort !== 80 && port.targetPort !== 443);
|
||||||
|
}
|
||||||
|
const ports: {
|
||||||
|
targetPort: number;
|
||||||
|
publishedPort: number;
|
||||||
|
protocol?: string;
|
||||||
|
}[] = [];
|
||||||
|
for (const key in parsedResult) {
|
||||||
|
if (Object.hasOwn(parsedResult, key)) {
|
||||||
|
const containerPortMapppings = parsedResult[key];
|
||||||
|
const protocol = key.split("/")[1];
|
||||||
|
const targetPort = Number.parseInt(key.split("/")[0] ?? "0", 10);
|
||||||
|
|
||||||
|
containerPortMapppings.forEach((mapping: any) => {
|
||||||
|
ports.push({
|
||||||
|
targetPort: targetPort,
|
||||||
|
publishedPort: Number.parseInt(mapping.HostPort, 10),
|
||||||
|
protocol: protocol,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ports.filter(
|
||||||
|
(port: any) => port.targetPort !== 80 && port.targetPort !== 443,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const writeTraefikSetup = async (
|
||||||
|
input: TraefikOptions,
|
||||||
|
serverId?: string,
|
||||||
|
) => {
|
||||||
|
const resourceType = await getDockerResourceType("dokploy-traefik", serverId);
|
||||||
|
if (resourceType === "service") {
|
||||||
|
await initializeTraefikService({
|
||||||
|
env: input.env,
|
||||||
|
additionalPorts: input.additionalPorts,
|
||||||
|
serverId: serverId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await initializeStandaloneTraefik({
|
||||||
|
env: input.env,
|
||||||
|
additionalPorts: input.additionalPorts,
|
||||||
|
serverId: serverId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { ContainerCreateOptions } from "dockerode";
|
import type { ContainerCreateOptions, CreateServiceOptions } from "dockerode";
|
||||||
import { dump } from "js-yaml";
|
import { dump } from "js-yaml";
|
||||||
import { paths } from "../constants";
|
import { paths } from "../constants";
|
||||||
import { getRemoteDocker } from "../utils/servers/remote-docker";
|
import { getRemoteDocker } from "../utils/servers/remote-docker";
|
||||||
@@ -15,23 +15,20 @@ export const TRAEFIK_HTTP3_PORT =
|
|||||||
Number.parseInt(process.env.TRAEFIK_HTTP3_PORT!, 10) || 443;
|
Number.parseInt(process.env.TRAEFIK_HTTP3_PORT!, 10) || 443;
|
||||||
export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.1.2";
|
export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.1.2";
|
||||||
|
|
||||||
interface TraefikOptions {
|
export interface TraefikOptions {
|
||||||
enableDashboard?: boolean;
|
|
||||||
env?: string[];
|
env?: string[];
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
additionalPorts?: {
|
additionalPorts?: {
|
||||||
targetPort: number;
|
targetPort: number;
|
||||||
publishedPort: number;
|
publishedPort: number;
|
||||||
|
protocol?: string;
|
||||||
}[];
|
}[];
|
||||||
force?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initializeTraefik = async ({
|
export const initializeStandaloneTraefik = async ({
|
||||||
enableDashboard = false,
|
|
||||||
env,
|
env,
|
||||||
serverId,
|
serverId,
|
||||||
additionalPorts = [],
|
additionalPorts = [],
|
||||||
force = false,
|
|
||||||
}: TraefikOptions = {}) => {
|
}: TraefikOptions = {}) => {
|
||||||
const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths(!!serverId);
|
const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths(!!serverId);
|
||||||
const imageName = `traefik:v${TRAEFIK_VERSION}`;
|
const imageName = `traefik:v${TRAEFIK_VERSION}`;
|
||||||
@@ -51,13 +48,17 @@ export const initializeTraefik = async ({
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const enableDashboard = additionalPorts.some(
|
||||||
|
(port) => port.targetPort === 8080,
|
||||||
|
);
|
||||||
|
|
||||||
if (enableDashboard) {
|
if (enableDashboard) {
|
||||||
exposedPorts["8080/tcp"] = {};
|
exposedPorts["8080/tcp"] = {};
|
||||||
portBindings["8080/tcp"] = [{ HostPort: "8080" }];
|
portBindings["8080/tcp"] = [{ HostPort: "8080" }];
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const port of additionalPorts) {
|
for (const port of additionalPorts) {
|
||||||
const portKey = `${port.targetPort}/tcp`;
|
const portKey = `${port.targetPort}/${port.protocol ?? "tcp"}`;
|
||||||
exposedPorts[portKey] = {};
|
exposedPorts[portKey] = {};
|
||||||
portBindings[portKey] = [{ HostPort: port.publishedPort.toString() }];
|
portBindings[portKey] = [{ HostPort: port.publishedPort.toString() }];
|
||||||
}
|
}
|
||||||
@@ -87,68 +88,117 @@ export const initializeTraefik = async ({
|
|||||||
|
|
||||||
const docker = await getRemoteDocker(serverId);
|
const docker = await getRemoteDocker(serverId);
|
||||||
try {
|
try {
|
||||||
try {
|
|
||||||
const service = docker.getService("dokploy-traefik");
|
|
||||||
await service?.remove({ force: true });
|
|
||||||
|
|
||||||
let attempts = 0;
|
|
||||||
const maxAttempts = 5;
|
|
||||||
while (attempts < maxAttempts) {
|
|
||||||
try {
|
|
||||||
await docker.listServices({
|
|
||||||
filters: { name: ["dokploy-traefik"] },
|
|
||||||
});
|
|
||||||
console.log("Waiting for service cleanup...");
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
||||||
attempts++;
|
|
||||||
} catch {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
console.log("No existing service to remove");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then try to remove any existing container
|
|
||||||
const container = docker.getContainer(containerName);
|
const container = docker.getContainer(containerName);
|
||||||
try {
|
try {
|
||||||
const inspect = await container.inspect();
|
|
||||||
if (inspect.State.Status === "running" && !force) {
|
|
||||||
console.log("Traefik already running");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await container.remove({ force: true });
|
await container.remove({ force: true });
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||||
} catch {
|
|
||||||
console.log("No existing container to remove");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create and start the new container
|
|
||||||
try {
|
|
||||||
await docker.createContainer(settings);
|
await docker.createContainer(settings);
|
||||||
const newContainer = docker.getContainer(containerName);
|
const newContainer = docker.getContainer(containerName);
|
||||||
await newContainer.start();
|
await newContainer.start();
|
||||||
console.log("Traefik container started successfully");
|
console.log("Traefik Started ✅");
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
if (error?.json?.message?.includes("port is already allocated")) {
|
console.error("Error in initializeStandaloneTraefik", error);
|
||||||
console.log("Ports still in use, waiting longer for cleanup...");
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 10000));
|
|
||||||
// Try one more time
|
|
||||||
await docker.createContainer(settings);
|
|
||||||
const newContainer = docker.getContainer(containerName);
|
|
||||||
await newContainer.start();
|
|
||||||
console.log("Traefik container started successfully after retry");
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to initialize Traefik:", error);
|
await docker.createContainer(settings);
|
||||||
|
console.error("Error in initializeStandaloneTraefik", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const initializeTraefikService = async ({
|
||||||
|
env,
|
||||||
|
additionalPorts = [],
|
||||||
|
serverId,
|
||||||
|
}: TraefikOptions) => {
|
||||||
|
const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths(!!serverId);
|
||||||
|
const imageName = `traefik:v${TRAEFIK_VERSION}`;
|
||||||
|
const appName = "dokploy-traefik";
|
||||||
|
|
||||||
|
const settings: CreateServiceOptions = {
|
||||||
|
Name: appName,
|
||||||
|
TaskTemplate: {
|
||||||
|
ContainerSpec: {
|
||||||
|
Image: imageName,
|
||||||
|
Env: env,
|
||||||
|
Mounts: [
|
||||||
|
{
|
||||||
|
Type: "bind",
|
||||||
|
Source: `${MAIN_TRAEFIK_PATH}/traefik.yml`,
|
||||||
|
Target: "/etc/traefik/traefik.yml",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "bind",
|
||||||
|
Source: DYNAMIC_TRAEFIK_PATH,
|
||||||
|
Target: "/etc/dokploy/traefik/dynamic",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "bind",
|
||||||
|
Source: "/var/run/docker.sock",
|
||||||
|
Target: "/var/run/docker.sock",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Networks: [{ Target: "dokploy-network" }],
|
||||||
|
Placement: {
|
||||||
|
Constraints: ["node.role==manager"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Mode: {
|
||||||
|
Replicated: {
|
||||||
|
Replicas: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
EndpointSpec: {
|
||||||
|
Ports: [
|
||||||
|
{
|
||||||
|
TargetPort: 443,
|
||||||
|
PublishedPort: TRAEFIK_SSL_PORT,
|
||||||
|
PublishMode: "host",
|
||||||
|
Protocol: "tcp",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TargetPort: 443,
|
||||||
|
PublishedPort: TRAEFIK_SSL_PORT,
|
||||||
|
PublishMode: "host",
|
||||||
|
Protocol: "udp",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TargetPort: 80,
|
||||||
|
PublishedPort: TRAEFIK_PORT,
|
||||||
|
PublishMode: "host",
|
||||||
|
Protocol: "tcp",
|
||||||
|
},
|
||||||
|
|
||||||
|
...additionalPorts.map((port) => ({
|
||||||
|
TargetPort: port.targetPort,
|
||||||
|
PublishedPort: port.publishedPort,
|
||||||
|
Protocol: port.protocol as "tcp" | "udp" | "sctp" | undefined,
|
||||||
|
PublishMode: "host" as const,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const docker = await getRemoteDocker(serverId);
|
||||||
|
try {
|
||||||
|
const service = docker.getService(appName);
|
||||||
|
const inspect = await service.inspect();
|
||||||
|
|
||||||
|
await service.update({
|
||||||
|
version: Number.parseInt(inspect.Version.Index),
|
||||||
|
...settings,
|
||||||
|
TaskTemplate: {
|
||||||
|
...settings.TaskTemplate,
|
||||||
|
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log("Traefik Updated ✅");
|
||||||
|
} catch {
|
||||||
|
await docker.createService(settings);
|
||||||
|
console.log("Traefik Started ✅");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const createDefaultServerTraefikConfig = () => {
|
export const createDefaultServerTraefikConfig = () => {
|
||||||
const { DYNAMIC_TRAEFIK_PATH } = paths();
|
const { DYNAMIC_TRAEFIK_PATH } = paths();
|
||||||
const configFilePath = path.join(DYNAMIC_TRAEFIK_PATH, "dokploy.yml");
|
const configFilePath = path.join(DYNAMIC_TRAEFIK_PATH, "dokploy.yml");
|
||||||
|
|||||||
Reference in New Issue
Block a user