#!/usr/bin/env node /** * Container entrypoint supervisor. * * Runs PocketBase and the TanStack Start node server side by side: * 1. starts PocketBase (data in /data, migrations from ./pb_migrations) * 2. waits for its health endpoint * 3. bootstraps the superuser account (idempotent upsert) * 4. seeds initial content (idempotent, create-if-missing) * 5. starts the site server on PORT (default 3000) * * If either process dies the container exits non-zero so Docker restarts it. */ import { spawn, spawnSync } from 'node:child_process' const PORT = process.env.PORT ?? '3000' const PB_HTTP = process.env.PB_HTTP ?? '0.0.0.0:8090' // PB_DATA_DIR is the canonical variable (set in the deploy .env); PB_DATA kept // as a legacy alias, /data as the image default. const PB_DATA = process.env.PB_DATA_DIR ?? process.env.PB_DATA ?? '/data' let stopping = false const children = [] function start(name, command, args, extraEnv = {}) { const child = spawn(command, args, { stdio: 'inherit', env: { ...process.env, ...extraEnv }, }) child.name = name children.push(child) child.on('exit', (code) => { if (stopping) return console.error(`[supervisor] ${name} exited with code ${code} — shutting down`) shutdown(code ?? 1) }) return child } function run(name, command, args, extraEnv = {}) { const result = spawnSync(command, args, { stdio: 'inherit', env: { ...process.env, ...extraEnv }, }) if (result.status !== 0) { console.warn(`[supervisor] ${name} finished with status ${result.status}`) } return result.status ?? 0 } function shutdown(exitCode = 0) { if (stopping) return stopping = true for (const child of children) { if (child.exitCode === null && child.signalCode === null) child.kill('SIGTERM') } setTimeout(() => process.exit(exitCode), 2000) } process.on('SIGINT', () => shutdown(0)) process.on('SIGTERM', () => shutdown(0)) async function waitForHealth(url, attempts = 120) { for (let i = 0; i < attempts; i++) { try { const res = await fetch(url) if (res.ok) return } catch { // not up yet } await new Promise((resolve) => setTimeout(resolve, 500)) } throw new Error(`health check at ${url} timed out`) } async function main() { console.log('[supervisor] starting PocketBase…') start('pocketbase', './pocketbase', [ 'serve', `--http=${PB_HTTP}`, `--dir=${PB_DATA}`, '--migrationsDir=./pb_migrations', '--publicDir=./pb_public', ]) await waitForHealth('http://127.0.0.1:8090/api/health') console.log('[supervisor] PocketBase is healthy') if (process.env.PB_SUPERUSER_EMAIL && process.env.PB_SUPERUSER_PASSWORD) { run('superuser upsert', './pocketbase', [ 'superuser', 'upsert', process.env.PB_SUPERUSER_EMAIL, process.env.PB_SUPERUSER_PASSWORD, `--dir=${PB_DATA}`, '--migrationsDir=./pb_migrations', ]) console.log('[supervisor] superuser ensured') run('seed', 'node', ['scripts/seed.mjs']) } else { console.warn('[supervisor] PB_SUPERUSER_EMAIL/PASSWORD not set — skipping superuser + seed') } console.log(`[supervisor] starting site server on :${PORT}…`) start('site', 'node', ['.output/server/index.mjs'], { PORT, PB_URL: process.env.PB_URL ?? 'http://127.0.0.1:8090', NODE_ENV: 'production', }) } main().catch((error) => { console.error(`[supervisor] fatal: ${error.message}`) shutdown(1) })