Some checks failed
Gitea CI / test-build (push) Has been cancelled
Make prisma migrate deploy failure non-fatal in start-with-migrate.js. Previously, migration failure caused process.exit() which killed the container, triggering an infinite restart loop. Now logs a warning and starts the Next.js server anyway (app has DB fallbacks). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
157 lines
4.6 KiB
JavaScript
157 lines
4.6 KiB
JavaScript
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
/**
|
|
* Container entrypoint: wait for DB, apply Prisma migrations, then start Next server.
|
|
*
|
|
* Why:
|
|
* - In real deployments you want schema changes applied automatically per deploy.
|
|
* - `prisma migrate deploy` is safe to run multiple times (idempotent).
|
|
*
|
|
* Controls:
|
|
* - Set `SKIP_PRISMA_MIGRATE=true` to skip migrations (emergency / debugging).
|
|
* - DB_WAIT_RETRIES (default 15) and DB_WAIT_INTERVAL_MS (default 2000) control
|
|
* how long to wait for the database before giving up.
|
|
*/
|
|
const { spawnSync } = require("node:child_process");
|
|
const { createConnection } = require("node:net");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
function run(cmd, args, opts = {}) {
|
|
const res = spawnSync(cmd, args, {
|
|
stdio: "inherit",
|
|
env: process.env,
|
|
...opts,
|
|
});
|
|
|
|
if (res.error) {
|
|
throw res.error;
|
|
}
|
|
if (typeof res.status === "number" && res.status !== 0) {
|
|
process.exit(res.status);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wait for a TCP port to be reachable.
|
|
* Parses DATABASE_URL to extract host and port.
|
|
*/
|
|
function waitForDb() {
|
|
const dbUrl = process.env.DATABASE_URL;
|
|
if (!dbUrl) {
|
|
console.log("[startup] No DATABASE_URL set, skipping DB wait.");
|
|
return Promise.resolve();
|
|
}
|
|
|
|
let host, port;
|
|
try {
|
|
// postgresql://user:pass@host:port/db?schema=public
|
|
const match = dbUrl.match(/@([^:/?]+):(\d+)/);
|
|
if (!match) throw new Error("Could not parse host:port from DATABASE_URL");
|
|
host = match[1];
|
|
port = parseInt(match[2], 10);
|
|
} catch (_err) {
|
|
console.log("[startup] Could not parse DATABASE_URL, skipping DB wait.");
|
|
return Promise.resolve();
|
|
}
|
|
|
|
const maxRetries = parseInt(process.env.DB_WAIT_RETRIES || "15", 10);
|
|
const intervalMs = parseInt(process.env.DB_WAIT_INTERVAL_MS || "2000", 10);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
let attempt = 0;
|
|
|
|
function tryConnect() {
|
|
attempt++;
|
|
const sock = createConnection({ host, port }, () => {
|
|
sock.destroy();
|
|
console.log(`[startup] Database at ${host}:${port} is reachable.`);
|
|
resolve();
|
|
});
|
|
sock.setTimeout(1500);
|
|
sock.on("error", () => {
|
|
sock.destroy();
|
|
if (attempt >= maxRetries) {
|
|
console.error(
|
|
`[startup] Database at ${host}:${port} not reachable after ${maxRetries} attempts. Proceeding anyway...`
|
|
);
|
|
resolve(); // still try migration - prisma will give a clear error
|
|
} else {
|
|
console.log(
|
|
`[startup] Waiting for database at ${host}:${port}... (${attempt}/${maxRetries})`
|
|
);
|
|
setTimeout(tryConnect, intervalMs);
|
|
}
|
|
});
|
|
sock.on("timeout", () => {
|
|
sock.destroy();
|
|
if (attempt >= maxRetries) {
|
|
console.error(
|
|
`[startup] Database at ${host}:${port} timed out after ${maxRetries} attempts. Proceeding anyway...`
|
|
);
|
|
resolve();
|
|
} else {
|
|
console.log(
|
|
`[startup] Waiting for database at ${host}:${port}... (${attempt}/${maxRetries})`
|
|
);
|
|
setTimeout(tryConnect, intervalMs);
|
|
}
|
|
});
|
|
}
|
|
|
|
tryConnect();
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
// 1. Wait for database to be reachable
|
|
await waitForDb();
|
|
|
|
// 2. Run migrations
|
|
const skip =
|
|
String(process.env.SKIP_PRISMA_MIGRATE || "").toLowerCase() === "true";
|
|
if (!skip) {
|
|
const autoBaseline =
|
|
String(process.env.PRISMA_AUTO_BASELINE || "").toLowerCase() === "true";
|
|
|
|
if (autoBaseline) {
|
|
try {
|
|
const migrationsDir = path.join(process.cwd(), "prisma", "migrations");
|
|
const entries = fs
|
|
.readdirSync(migrationsDir, { withFileTypes: true })
|
|
.filter((d) => d.isDirectory())
|
|
.map((d) => d.name);
|
|
const initMigration = entries.find((n) => n.endsWith("_init"));
|
|
if (initMigration) {
|
|
run("node", [
|
|
"node_modules/prisma/build/index.js",
|
|
"migrate",
|
|
"resolve",
|
|
"--applied",
|
|
initMigration,
|
|
]);
|
|
}
|
|
} catch (_err) {
|
|
// If baseline fails we continue to migrate deploy, which will surface the real issue.
|
|
}
|
|
}
|
|
|
|
const migrateResult = spawnSync(
|
|
"node",
|
|
["node_modules/prisma/build/index.js", "migrate", "deploy"],
|
|
{ stdio: "inherit", env: process.env }
|
|
);
|
|
if (migrateResult.status !== 0) {
|
|
console.error(
|
|
`[startup] prisma migrate deploy failed (exit ${migrateResult.status}). Starting server anyway...`
|
|
);
|
|
}
|
|
} else {
|
|
console.log("SKIP_PRISMA_MIGRATE=true -> skipping prisma migrate deploy");
|
|
}
|
|
|
|
// 3. Start the server
|
|
run("node", ["server.js"]);
|
|
}
|
|
|
|
main();
|