diff --git a/apps/dokploy/server/server.ts b/apps/dokploy/server/server.ts index 5fe048c922..069eb88114 100644 --- a/apps/dokploy/server/server.ts +++ b/apps/dokploy/server/server.ts @@ -28,53 +28,106 @@ const PORT = Number.parseInt(process.env.PORT || "3000", 10); const HOST = process.env.HOST || "0.0.0.0"; const dev = process.env.NODE_ENV !== "production"; +// Tracks whether the HTTP server has reached the "listening" state. Until then, +// any fatal error means the process can never serve requests and MUST exit(1) so +// the orchestrator restarts it cleanly. Without this, a startup failure (e.g. a +// rejected `app.prepare()`) leaves background handles — the Redis reconnect loop, +// open sockets — keeping the event loop alive, so the process never exits: it +// spins at ~100% CPU, the healthcheck never passes, and Docker Swarm crash-loops +// the container with only an opaque "ELIFECYCLE Command failed." See issue #4253. +let isServerListening = false; + +// Node terminates the process on an uncaught exception by default; preserve that +// but log the cause first so the failure point is never silent. +process.on("uncaughtException", (error) => { + console.error("Uncaught exception:", error); + process.exit(1); +}); + +// A stray unhandled rejection BEFORE the server is listening means startup has +// failed — exit so we don't spin forever (see above). Once we are serving +// requests we only log, to avoid crashing an otherwise-healthy instance. +process.on("unhandledRejection", (reason) => { + console.error("Unhandled rejection:", reason); + if (!isServerListening) { + process.exit(1); + } +}); + // Initialize critical directories and Traefik config BEFORE Next.js starts // This prevents race conditions with the install script if (process.env.NODE_ENV === "production" && !IS_CLOUD) { - setupDirectories(); - createDefaultTraefikConfig(); - createDefaultServerTraefikConfig(); - console.log("✅ initialization complete"); + try { + setupDirectories(); + createDefaultTraefikConfig(); + createDefaultServerTraefikConfig(); + console.log("✅ initialization complete"); + } catch (error) { + console.error("Failed to initialize directories/Traefik config:", error); + process.exit(1); + } } const app = next({ dev, turbopack: process.env.TURBOPACK === "1" }); const handle = app.getRequestHandler(); -void app.prepare().then(async () => { - try { - console.log("Running DokployVersion: ", packageInfo.version); - const server = http.createServer((req, res) => { - handle(req, res); - }); +void app + .prepare() + .then(async () => { + try { + console.log("Running DokployVersion: ", packageInfo.version); + const server = http.createServer((req, res) => { + handle(req, res); + }); - // WEBSOCKET - setupDrawerLogsWebSocketServer(server); - setupDeploymentLogsWebSocketServer(server); - setupDockerContainerLogsWebSocketServer(server); - setupDockerContainerTerminalWebSocketServer(server); - setupTerminalWebSocketServer(server); - if (!IS_CLOUD) { - setupDockerStatsMonitoringSocketServer(server); - } + // WEBSOCKET + setupDrawerLogsWebSocketServer(server); + setupDeploymentLogsWebSocketServer(server); + setupDockerContainerLogsWebSocketServer(server); + setupDockerContainerTerminalWebSocketServer(server); + setupTerminalWebSocketServer(server); + if (!IS_CLOUD) { + setupDockerStatsMonitoringSocketServer(server); + } - server.listen(PORT, HOST); - console.log(`Server Started on: http://${HOST}:${PORT}`); - if (process.env.NODE_ENV === "production" && !IS_CLOUD) { - createDefaultMiddlewares(); - await initializeNetwork(); - await initCronJobs(); - await initSchedules(); - await initCancelDeployments(); - await initVolumeBackupsCronJobs(); - await sendDokployRestartNotifications(); - } - await initEnterpriseBackupCronJobs(); + // Wait for the bind to succeed (or fail, e.g. EADDRINUSE) before + // continuing, so a listen failure exits cleanly instead of spinning. + await new Promise((resolve, reject) => { + const onError = (error: Error) => reject(error); + server.once("error", onError); + server.listen(PORT, HOST, () => { + server.removeListener("error", onError); + resolve(); + }); + }); + isServerListening = true; + console.log(`Server Started on: http://${HOST}:${PORT}`); - if (!IS_CLOUD) { - console.log("Starting Deployment Worker"); - const { startDeploymentWorker } = await import("./queues/queueSetup"); - await startDeploymentWorker(); + if (process.env.NODE_ENV === "production" && !IS_CLOUD) { + createDefaultMiddlewares(); + await initializeNetwork(); + await initCronJobs(); + await initSchedules(); + await initCancelDeployments(); + await initVolumeBackupsCronJobs(); + await sendDokployRestartNotifications(); + } + await initEnterpriseBackupCronJobs(); + + if (!IS_CLOUD) { + console.log("Starting Deployment Worker"); + const { startDeploymentWorker } = await import("./queues/queueSetup"); + await startDeploymentWorker(); + } + } catch (e) { + console.error("Main Server Error", e); + // If we failed before binding, the process can never serve traffic — + // exit so the orchestrator restarts us instead of leaving it spinning. + if (!isServerListening) { + process.exit(1); + } } - } catch (e) { - console.error("Main Server Error", e); - } -}); + }) + .catch((error) => { + console.error("Failed to prepare Next.js app server:", error); + process.exit(1); + });