Extract API, migrations and portable NAS deployment
This commit is contained in:
commit
673255fa4f
57 files changed
+3799
No files matched your search
@@ -0,0 +1,71 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
const env = { ...process.env, API_HOST: '127.0.0.1', API_PORT: '3001' };
|
||||
function origin(name) {
|
||||
const value = env[name];
|
||||
if (!value) return undefined;
|
||||
const url = new URL(value);
|
||||
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== '/') {
|
||||
throw new Error(`${name} must be an HTTP(S) origin without a path`);
|
||||
}
|
||||
if (env.COOKIE_SECURE !== 'false' && url.protocol !== 'https:') throw new Error(`${name} requires HTTPS with secure cookies`);
|
||||
return url.origin;
|
||||
}
|
||||
|
||||
const children = new Set();
|
||||
let stopping = false;
|
||||
let exitCode = 0;
|
||||
let killTimer;
|
||||
function stop(code) {
|
||||
if (stopping) return;
|
||||
stopping = true;
|
||||
exitCode = code;
|
||||
process.exitCode = code;
|
||||
for (const child of children) child.kill('SIGTERM');
|
||||
if (children.size) {
|
||||
killTimer = setTimeout(() => { for (const child of children) child.kill('SIGKILL'); }, 10000);
|
||||
killTimer.unref();
|
||||
}
|
||||
}
|
||||
process.on('SIGTERM', () => stop(0));
|
||||
process.on('SIGINT', () => stop(0));
|
||||
|
||||
function run(command, args, service = false) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (stopping) { resolve(); return; }
|
||||
const child = spawn(command, args, { env, stdio: 'inherit' });
|
||||
children.add(child);
|
||||
child.once('error', () => { stop(1); reject(new Error(`Could not start ${command}`)); });
|
||||
child.once('close', code => {
|
||||
children.delete(child);
|
||||
if (service && !stopping) {
|
||||
console.error(`${command} exited unexpectedly; stopping the container`);
|
||||
stop(1);
|
||||
}
|
||||
if (!children.size && killTimer) clearTimeout(killTimer);
|
||||
if (!stopping && code !== 0) reject(new Error(`${command} failed with exit code ${code}`));
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const appUrl = origin('APP_URL');
|
||||
const adminUrl = origin('ADMIN_URL');
|
||||
if (!env.APP_ORIGINS && !appUrl) throw new Error('APP_URL (or APP_ORIGINS) is required');
|
||||
env.APP_ORIGINS ||= [...new Set([appUrl, adminUrl].filter(Boolean))].join(',');
|
||||
if (appUrl) env.OIDC_REDIRECT_URI ||= `${appUrl}/api/auth/callback`;
|
||||
if (!env.DB_HOST || !env.DB_PASSWORD) throw new Error('DB_HOST and DB_PASSWORD are required');
|
||||
if (!env.TOKEN_ENCRYPTION_KEY || Buffer.from(env.TOKEN_ENCRYPTION_KEY, 'base64').length !== 32) throw new Error('TOKEN_ENCRYPTION_KEY must contain 32 random bytes encoded as base64');
|
||||
await run('node', ['dist/migrate.js']);
|
||||
await run('node', ['dist/seed.js']);
|
||||
if (!stopping) await Promise.all([
|
||||
run('node', ['dist/server.js'], true),
|
||||
run('nginx', ['-g', 'daemon off;'], true),
|
||||
]);
|
||||
} catch (error) {
|
||||
// Configuration values, passwords, and tokens are never printed here.
|
||||
console.error(error instanceof Error ? error.message : 'Container startup failed');
|
||||
stop(1);
|
||||
}
|
||||
process.exitCode = exitCode;
|
||||
Reference in new issue
Block a user