refactor(traefik): update Traefik initialization to support standalone and service modes, enhance port handling with protocol specification

This commit is contained in:
Mauricio Siu
2025-08-03 01:18:18 -06:00
parent 42629e83a1
commit 607c505c4b
6 changed files with 447 additions and 186 deletions

View File

@@ -1,5 +1,6 @@
import { useTranslation } from "next-i18next";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
@@ -10,8 +11,6 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { api } from "@/utils/api";
import { useTranslation } from "next-i18next";
import { toast } from "sonner";
import { EditTraefikEnv } from "../../web-server/edit-traefik-env";
import { ManageTraefikPorts } from "../../web-server/manage-traefik-ports";
import { ShowModalLogs } from "../../web-server/show-modal-logs";

View File

@@ -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 { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
@@ -19,15 +27,15 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
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 {
children: React.ReactNode;
@@ -37,6 +45,7 @@ interface Props {
const PortSchema = z.object({
targetPort: z.number().min(1, "Target port is required"),
publishedPort: z.number().min(1, "Published port is required"),
protocol: z.enum(["tcp", "udp", "sctp"]),
});
const TraefikPortsSchema = z.object({
@@ -75,12 +84,17 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
useEffect(() => {
if (currentPorts) {
form.reset({ ports: currentPorts });
form.reset({
ports: currentPorts.map((port) => ({
...port,
protocol: port.protocol as "tcp" | "udp" | "sctp",
})),
});
}
}, [currentPorts, form]);
const handleAddPort = () => {
append({ targetPort: 0, publishedPort: 0 });
append({ targetPort: 0, publishedPort: 0, protocol: "tcp" });
};
const onSubmit = async (data: TraefikPortsForm) => {
@@ -96,7 +110,9 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
return (
<>
<div onClick={() => setOpen(true)}>{children}</div>
<button type="button" onClick={() => setOpen(true)}>
{children}
</button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-3xl">
<DialogHeader>
@@ -143,8 +159,8 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
<ScrollArea className="h-[400px] pr-4">
<div className="grid gap-4">
{fields.map((field, index) => (
<Card key={field.id}>
<CardContent className="grid grid-cols-[1fr_1fr_auto] gap-4 p-4 transparent">
<Card key={field.id} className="bg-transparent">
<CardContent className="grid grid-cols-[1fr_1fr_1fr_auto] gap-4 p-4 transparent">
<FormField
control={form.control}
name={`ports.${index}.targetPort`}
@@ -168,7 +184,6 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
);
}}
value={field.value || ""}
className="w-full dark:bg-black"
placeholder="e.g. 8080"
/>
</FormControl>
@@ -200,7 +215,6 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
);
}}
value={field.value || ""}
className="w-full dark:bg-black"
placeholder="e.g. 80"
/>
</FormControl>
@@ -208,6 +222,42 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
</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">
<Button

View File

@@ -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 {
apiAssignDomain,
@@ -11,54 +63,6 @@ import {
apiUpdateDockerCleanup,
} from "@/server/db/schema";
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 { appRouter } from "../root";
import {
@@ -73,10 +77,7 @@ export const settingsRouter = createTRPCRouter({
if (IS_CLOUD) {
return true;
}
const { stdout } = await execAsync(
"docker service inspect dokploy --format '{{.ID}}'",
);
await execAsync(`docker service update --force ${stdout.trim()}`);
await reloadDockerResource("dokploy");
return true;
}),
cleanRedis: adminProcedure.mutation(async () => {
@@ -101,20 +102,15 @@ export const settingsRouter = createTRPCRouter({
if (IS_CLOUD) {
return true;
}
await reloadDockerResource("dokploy-redis");
await execAsync("docker service scale dokploy-redis=0");
await execAsync("docker service scale dokploy-redis=1");
return true;
}),
reloadTraefik: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
try {
if (input?.serverId) {
await execAsync("docker restart dokploy-traefik");
} else if (!IS_CLOUD) {
await execAsync("docker restart dokploy-traefik");
}
await reloadDockerResource("dokploy-traefik", input?.serverId);
} catch (err) {
console.error(err);
}
@@ -124,17 +120,28 @@ export const settingsRouter = createTRPCRouter({
toggleDashboard: adminProcedure
.input(apiEnableDashboard)
.mutation(async ({ input }) => {
const ports = (await getTraefikPorts(input.serverId)).filter(
(port) =>
port.targetPort !== 80 &&
port.targetPort !== 443 &&
port.targetPort !== 8080,
const ports = await readPorts("dokploy-traefik", input.serverId);
const env = await readEnvironmentVariables(
"dokploy-traefik",
input.serverId,
);
await initializeTraefik({
additionalPorts: ports,
enableDashboard: input.enableDashboard,
const preparedEnv = prepareEnvironmentVariables(env);
let newPorts = ports;
// 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,
force: true,
});
return true;
}),
@@ -551,29 +558,23 @@ export const settingsRouter = createTRPCRouter({
readTraefikEnv: adminProcedure
.input(apiServerSchema)
.query(async ({ input }) => {
const command =
"docker container inspect dokploy-traefik --format '{{json .Config.Env}}'";
let result = "";
if (input?.serverId) {
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");
const envVars = await readEnvironmentVariables(
"dokploy-traefik",
input?.serverId,
);
return envVars;
}),
writeTraefikEnv: adminProcedure
.input(z.object({ env: z.string(), serverId: z.string().optional() }))
.mutation(async ({ input }) => {
const envs = prepareEnvironmentVariables(input.env);
await initializeTraefik({
const ports = await readPorts("dokploy-traefik", input?.serverId);
await writeTraefikSetup({
env: envs,
additionalPorts: ports,
serverId: input.serverId,
force: true,
});
return true;
@@ -581,22 +582,8 @@ export const settingsRouter = createTRPCRouter({
haveTraefikDashboardPortEnabled: adminProcedure
.input(apiServerSchema)
.query(async ({ input }) => {
const command = `docker container inspect --format='{{json .NetworkSettings.Ports}}' dokploy-traefik`;
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;
});
const ports = await readPorts("dokploy-traefik", input?.serverId);
return ports.some((port) => port.targetPort === 8080);
}),
readStatsLogs: adminProcedure
@@ -793,6 +780,7 @@ export const settingsRouter = createTRPCRouter({
z.object({
targetPort: 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",
});
}
await initializeTraefik({
serverId: input.serverId,
const env = await readEnvironmentVariables(
"dokploy-traefik",
input?.serverId,
);
const preparedEnv = prepareEnvironmentVariables(env);
await writeTraefikSetup({
env: preparedEnv,
additionalPorts: input.additionalPorts,
force: true,
serverId: input.serverId,
});
return true;
} catch (error) {
@@ -825,7 +819,8 @@ export const settingsRouter = createTRPCRouter({
getTraefikPorts: adminProcedure
.input(apiServerSchema)
.query(async ({ input }) => {
return await getTraefikPorts(input?.serverId);
const ports = await readPorts("dokploy-traefik", input?.serverId);
return ports;
}),
updateLogCleanup: adminProcedure
.input(

View File

@@ -1,10 +1,3 @@
import {
createDefaultMiddlewares,
createDefaultServerTraefikConfig,
createDefaultTraefikConfig,
initializeTraefik,
} from "@dokploy/server/setup/traefik-setup";
import { execAsync } from "@dokploy/server";
import { setupDirectories } from "@dokploy/server/setup/config-paths";
import { initializePostgres } from "@dokploy/server/setup/postgres-setup";
@@ -13,6 +6,13 @@ import {
initializeNetwork,
initializeSwarm,
} from "@dokploy/server/setup/setup";
import {
createDefaultMiddlewares,
createDefaultServerTraefikConfig,
createDefaultTraefikConfig,
initializeStandaloneTraefik,
} from "@dokploy/server/setup/traefik-setup";
(async () => {
try {
setupDirectories();
@@ -22,7 +22,7 @@ import {
createDefaultTraefikConfig();
createDefaultServerTraefikConfig();
await execAsync("docker pull traefik:v3.1.2");
await initializeTraefik();
await initializeStandaloneTraefik();
await initializeRedis();
await initializePostgres();
} catch (e) {