Skip to content

Commit e47263a

Browse files
authored
Merge pull request #4033 from Dokploy/feat/improve-update-process-to-validate-dokploy-services
feat: enhance web server update process with health checks
2 parents d0c92d8 + b139d6f commit e47263a

3 files changed

Lines changed: 369 additions & 30 deletions

File tree

apps/dokploy/components/dashboard/settings/web-server/update-webserver.tsx

Lines changed: 174 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { HardDriveDownload, Loader2 } from "lucide-react";
1+
import {
2+
AlertTriangle,
3+
CheckCircle2,
4+
HardDriveDownload,
5+
Loader2,
6+
RefreshCw,
7+
XCircle,
8+
} from "lucide-react";
29
import { useState } from "react";
310
import { toast } from "sonner";
411
import {
@@ -15,11 +22,70 @@ import {
1522
import { Button } from "@/components/ui/button";
1623
import { api } from "@/utils/api";
1724

25+
type ServiceStatus = {
26+
status: "healthy" | "unhealthy";
27+
message?: string;
28+
};
29+
30+
type HealthResult = {
31+
postgres: ServiceStatus;
32+
redis: ServiceStatus;
33+
traefik: ServiceStatus;
34+
};
35+
36+
type ModalState = "idle" | "checking" | "results" | "updating";
37+
38+
const ServiceStatusItem = ({
39+
name,
40+
service,
41+
}: {
42+
name: string;
43+
service: ServiceStatus;
44+
}) => (
45+
<div className="flex items-center gap-2">
46+
{service.status === "healthy" ? (
47+
<CheckCircle2 className="h-4 w-4 text-green-500" />
48+
) : (
49+
<XCircle className="h-4 w-4 text-red-500" />
50+
)}
51+
<span className="text-sm font-medium">{name}</span>
52+
{service.status === "unhealthy" && service.message && (
53+
<span className="text-xs text-muted-foreground">{service.message}</span>
54+
)}
55+
</div>
56+
);
57+
1858
export const UpdateWebServer = () => {
19-
const [updating, setUpdating] = useState(false);
59+
const [modalState, setModalState] = useState<ModalState>("idle");
2060
const [open, setOpen] = useState(false);
61+
const [healthResult, setHealthResult] = useState<HealthResult | null>(null);
2162

2263
const { mutateAsync: updateServer } = api.settings.updateServer.useMutation();
64+
const { refetch: checkHealth } =
65+
api.settings.checkInfrastructureHealth.useQuery(undefined, {
66+
enabled: false,
67+
});
68+
69+
const handleVerify = async () => {
70+
setModalState("checking");
71+
setHealthResult(null);
72+
73+
try {
74+
const result = await checkHealth();
75+
if (result.data) {
76+
setHealthResult(result.data);
77+
}
78+
} catch {
79+
// checkHealth failed entirely
80+
}
81+
setModalState("results");
82+
};
83+
84+
const allHealthy =
85+
healthResult &&
86+
healthResult.postgres.status === "healthy" &&
87+
healthResult.redis.status === "healthy" &&
88+
healthResult.traefik.status === "healthy";
2389

2490
const checkIsUpdateFinished = async () => {
2591
try {
@@ -33,35 +99,39 @@ export const UpdateWebServer = () => {
3399
);
34100

35101
setTimeout(() => {
36-
// Allow seeing the toast before reloading
37102
window.location.reload();
38103
}, 2000);
39104
} catch {
40-
// Delay each request
41105
await new Promise((resolve) => setTimeout(resolve, 2000));
42-
// Keep running until it returns 200
43106
void checkIsUpdateFinished();
44107
}
45108
};
46109

47110
const handleConfirm = async () => {
48111
try {
49-
setUpdating(true);
112+
setModalState("updating");
50113
await updateServer();
51114

52-
// Give some time for docker service restart before starting to check status
53115
await new Promise((resolve) => setTimeout(resolve, 8000));
54116

55117
await checkIsUpdateFinished();
56118
} catch (error) {
57-
setUpdating(false);
119+
setModalState("results");
58120
console.error("Error updating server:", error);
59121
toast.error(
60122
"An error occurred while updating the server, please try again.",
61123
);
62124
}
63125
};
64126

127+
const handleClose = () => {
128+
if (modalState !== "updating") {
129+
setOpen(false);
130+
setModalState("idle");
131+
setHealthResult(null);
132+
}
133+
};
134+
65135
return (
66136
<AlertDialog open={open}>
67137
<AlertDialogTrigger asChild>
@@ -81,36 +151,111 @@ export const UpdateWebServer = () => {
81151
<AlertDialogContent>
82152
<AlertDialogHeader>
83153
<AlertDialogTitle>
84-
{updating
85-
? "Server update in progress"
86-
: "Are you absolutely sure?"}
154+
{modalState === "idle" && "Are you absolutely sure?"}
155+
{modalState === "checking" && "Verifying Services..."}
156+
{modalState === "results" &&
157+
(allHealthy ? "Ready to Update" : "Service Issues Detected")}
158+
{modalState === "updating" && "Server update in progress"}
87159
</AlertDialogTitle>
88-
<AlertDialogDescription>
89-
{updating ? (
90-
<span className="flex items-center gap-1">
91-
<Loader2 className="animate-spin" />
92-
The server is being updated, please wait...
93-
</span>
94-
) : (
95-
<>
96-
This action cannot be undone. This will update the web server to
97-
the new version. You will not be able to use the panel during
98-
the update process. The page will be reloaded once the update is
99-
finished.
100-
</>
101-
)}
160+
<AlertDialogDescription asChild>
161+
<div>
162+
{modalState === "idle" && (
163+
<span>
164+
This will update the web server to the new version. You will
165+
not be able to use the panel during the update process. The
166+
page will be reloaded once the update is finished.
167+
<br />
168+
<br />
169+
We recommend verifying that all services are running before
170+
updating.
171+
</span>
172+
)}
173+
174+
{modalState === "checking" && (
175+
<span className="flex items-center gap-2">
176+
<Loader2 className="animate-spin h-4 w-4" />
177+
Checking PostgreSQL, Redis and Traefik...
178+
</span>
179+
)}
180+
181+
{modalState === "results" && healthResult && (
182+
<div className="flex flex-col gap-3">
183+
<div className="flex flex-col gap-2">
184+
<ServiceStatusItem
185+
name="PostgreSQL"
186+
service={healthResult.postgres}
187+
/>
188+
<ServiceStatusItem
189+
name="Redis"
190+
service={healthResult.redis}
191+
/>
192+
<ServiceStatusItem
193+
name="Traefik"
194+
service={healthResult.traefik}
195+
/>
196+
</div>
197+
198+
{!allHealthy && (
199+
<div className="flex items-start gap-2 rounded-md border border-yellow-500/30 bg-yellow-500/10 p-3">
200+
<AlertTriangle className="h-4 w-4 text-yellow-500 mt-0.5 shrink-0" />
201+
<span className="text-sm text-yellow-600 dark:text-yellow-400">
202+
Some services are not healthy. You can still proceed
203+
with the update.
204+
</span>
205+
</div>
206+
)}
207+
208+
{allHealthy && (
209+
<span className="text-sm text-muted-foreground">
210+
All services are running. You can proceed with the update.
211+
</span>
212+
)}
213+
</div>
214+
)}
215+
216+
{modalState === "results" && !healthResult && (
217+
<div className="flex items-start gap-2 rounded-md border border-yellow-500/30 bg-yellow-500/10 p-3">
218+
<AlertTriangle className="h-4 w-4 text-yellow-500 mt-0.5 shrink-0" />
219+
<span className="text-sm text-yellow-600 dark:text-yellow-400">
220+
Could not verify services. You can still proceed with the
221+
update.
222+
</span>
223+
</div>
224+
)}
225+
226+
{modalState === "updating" && (
227+
<span className="flex items-center gap-2">
228+
<Loader2 className="animate-spin h-4 w-4" />
229+
The server is being updated, please wait...
230+
</span>
231+
)}
232+
</div>
102233
</AlertDialogDescription>
103234
</AlertDialogHeader>
104-
{!updating && (
235+
{modalState === "idle" && (
105236
<AlertDialogFooter>
106-
<AlertDialogCancel onClick={() => setOpen(false)}>
107-
Cancel
108-
</AlertDialogCancel>
237+
<AlertDialogCancel onClick={handleClose}>Cancel</AlertDialogCancel>
238+
<Button variant="secondary" onClick={handleVerify}>
239+
<RefreshCw className="h-4 w-4" />
240+
Verify Status
241+
</Button>
109242
<AlertDialogAction onClick={handleConfirm}>
110243
Confirm
111244
</AlertDialogAction>
112245
</AlertDialogFooter>
113246
)}
247+
{modalState === "results" && (
248+
<AlertDialogFooter>
249+
<AlertDialogCancel onClick={handleClose}>Cancel</AlertDialogCancel>
250+
<Button variant="secondary" onClick={handleVerify}>
251+
<RefreshCw className="h-4 w-4" />
252+
Re-check
253+
</Button>
254+
<AlertDialogAction onClick={handleConfirm}>
255+
{allHealthy ? "Confirm" : "Confirm Anyway"}
256+
</AlertDialogAction>
257+
</AlertDialogFooter>
258+
)}
114259
</AlertDialogContent>
115260
</AlertDialog>
116261
);

apps/dokploy/server/api/routers/settings.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import {
22
CLEANUP_CRON_JOB,
33
checkGPUStatus,
44
checkPortInUse,
5+
checkPostgresHealth,
6+
checkRedisHealth,
7+
checkTraefikHealth,
58
cleanupAll,
69
cleanupAllBackground,
710
cleanupBuilders,
@@ -44,8 +47,8 @@ import {
4447
writeTraefikConfigInPath,
4548
writeTraefikSetup,
4649
} from "@dokploy/server";
47-
import { checkPermission } from "@dokploy/server/services/permission";
4850
import { db } from "@dokploy/server/db";
51+
import { checkPermission } from "@dokploy/server/services/permission";
4952
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
5053
import { TRPCError } from "@trpc/server";
5154
import { eq, sql } from "drizzle-orm";
@@ -864,6 +867,23 @@ export const settingsRouter = createTRPCRouter({
864867
throw error;
865868
}
866869
}),
870+
checkInfrastructureHealth: adminProcedure.query(async () => {
871+
if (IS_CLOUD) {
872+
return {
873+
postgres: { status: "healthy" as const },
874+
redis: { status: "healthy" as const },
875+
traefik: { status: "healthy" as const },
876+
};
877+
}
878+
879+
const [postgres, redis, traefik] = await Promise.all([
880+
checkPostgresHealth(),
881+
checkRedisHealth(),
882+
checkTraefikHealth(),
883+
]);
884+
885+
return { postgres, redis, traefik };
886+
}),
867887
setupGPU: adminProcedure
868888
.input(
869889
z.object({

0 commit comments

Comments
 (0)