diff --git a/Dockerfile b/Dockerfile index 5d7bb6770..262862ca6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -65,4 +65,8 @@ RUN curl -sSL https://railpack.com/install.sh | bash COPY --from=buildpacksio/pack:0.39.1 /usr/local/bin/pack /usr/local/bin/pack EXPOSE 3000 -CMD [ "pnpm", "start" ] + +HEALTHCHECK --interval=10s --timeout=3s --retries=10 \ + CMD curl -fs http://localhost:3000/api/trpc/settings.health || exit 1 + + CMD ["sh", "-c", "pnpm run wait-for-postgres && exec pnpm start"] diff --git a/apps/dokploy/esbuild.config.ts b/apps/dokploy/esbuild.config.ts index d2f434255..615b2bdc0 100644 --- a/apps/dokploy/esbuild.config.ts +++ b/apps/dokploy/esbuild.config.ts @@ -25,6 +25,7 @@ try { entryPoints: { server: "server/server.ts", migration: "migration.ts", + "wait-for-postgres": "wait-for-postgres.ts", "reset-password": "reset-password.ts", "reset-2fa": "reset-2fa.ts", }, diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 43b1c5ff1..e6aaf1bda 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -6,10 +6,12 @@ "type": "module", "scripts": { "build": "npm run build-server && npm run build-next", - "start": "node -r dotenv/config dist/migration.mjs && node -r dotenv/config dist/server.mjs", + "start": "node -r dotenv/config dist/wait-for-postgres.mjs && node -r dotenv/config dist/migration.mjs && node -r dotenv/config dist/server.mjs", "build-server": "tsx esbuild.config.ts", "build-next": "next build --webpack", "setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run", + "wait-for-postgres": "node -r dotenv/config dist/wait-for-postgres.mjs", + "wait-for-postgres-dev": "tsx -r dotenv/config wait-for-postgres.ts", "reset-password": "node -r dotenv/config dist/reset-password.mjs", "reset-2fa": "node -r dotenv/config dist/reset-2fa.mjs", "dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ", diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index 274b9ca0f..bb7269fa7 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -764,16 +764,13 @@ export const settingsRouter = createTRPCRouter({ return haveServers.length > 0 || haveProjects.length > 0; }), health: publicProcedure.query(async () => { - if (IS_CLOUD) { - try { - await db.execute(sql`SELECT 1`); - return { status: "ok" }; - } catch (error) { - console.error("Database connection error:", error); - throw error; - } + try { + await db.execute(sql`SELECT 1`); + return { status: "ok" }; + } catch (error) { + console.error("Database connection error:", error); + throw error; } - return { status: "not_cloud" }; }), setupGPU: adminProcedure .input( diff --git a/apps/dokploy/wait-for-postgres.ts b/apps/dokploy/wait-for-postgres.ts new file mode 100644 index 000000000..3460fe7c3 --- /dev/null +++ b/apps/dokploy/wait-for-postgres.ts @@ -0,0 +1,91 @@ +import net from "node:net"; +import { URL } from "node:url"; +import { dbUrl } from "@dokploy/server/db/constants"; + +const TIMEOUT_MS = Number(process.env.POSTGRES_WAIT_TIMEOUT || 120_000); +const RETRY_DELAY_MS = Number(process.env.POSTGRES_WAIT_RETRY || 2000); + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function resolvePostgresTarget(): { host: string; port: number } { + const databaseUrl = dbUrl; + + if (!databaseUrl) { + console.error("[wait-for-postgres] DATABASE_URL is not set"); + process.exit(1); + } + + try { + const url = new URL(databaseUrl); + + const host = url.hostname; + const port = Number(url.port || 5432); + + if (!host) { + throw new Error("DATABASE_URL has no hostname"); + } + + return { host, port }; + } catch (err) { + console.error("[wait-for-postgres] Invalid DATABASE_URL:", databaseUrl); + process.exit(1); + } +} + +function checkTcpConnection(host: string, port: number): Promise { + return new Promise((resolve, reject) => { + const socket = net.createConnection({ host, port }); + + socket.setTimeout(3000); + + socket.on("connect", () => { + socket.end(); + resolve(); + }); + + socket.on("timeout", () => { + socket.destroy(); + reject(new Error("Connection timeout")); + }); + + socket.on("error", reject); + }); +} + +async function waitForPostgres() { + const { host, port } = resolvePostgresTarget(); + const start = Date.now(); + + console.log( + `[wait-for-postgres] Waiting for postgres at ${host}:${port} (timeout ${TIMEOUT_MS}ms)`, + ); + + while (true) { + try { + await checkTcpConnection(host, port); + console.log("[wait-for-postgres] Postgres is reachable ✅"); + return; + } catch { + const elapsed = Date.now() - start; + + if (elapsed > TIMEOUT_MS) { + console.error( + `[wait-for-postgres] Timeout after ${elapsed}ms. Postgres not reachable ❌`, + ); + process.exit(1); + } + + console.log( + `[wait-for-postgres] Postgres not ready yet, retrying in ${RETRY_DELAY_MS}ms...`, + ); + await sleep(RETRY_DELAY_MS); + } + } +} + +waitForPostgres().catch((err) => { + console.error("[wait-for-postgres] Fatal error:", err); + process.exit(1); +});