- playbook.yml (hosts: monoco) + vars/default.yml, requirements.yml - roles: network (shared docker network), greatbear (registry pull, bind mount /var/lib/greatbear/pb_data, .env upload, health wait), caddy (blockinfile snippet into the shared Caddyfile + docker exec reload) - image honors PB_DATA_DIR and runs as root to match the bind-mount pattern - frontend on www.greatbear.in (+apex) with /api/* and /_* proxied to PocketBase; admin.greatbear.in redirects to the admin UI
117 lines
3.4 KiB
JavaScript
Executable File
117 lines
3.4 KiB
JavaScript
Executable File
#!/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)
|
|
})
|