production is a static build behind nginx, not a process
The site's own build script says it outright: "The public site is not served by Bun in production — it is a directory of files behind nginx. Output: dist/." So production has no port, nothing to restart, and nothing that can crash at three in the morning. It is built or stale, and served or not served. Showing it as a pm2 process was showing something that does not exist, and its Start button could only ever have failed. It now has Build, Serve and Stop, and the two states are reported separately — a build can succeed while nothing serves it, and a container can serve a directory built weeks ago. They fail independently and are fixed differently. The container exists because nginx could not see the files. NPM mounts only its own /data and /etc/letsencrypt, and there is no other nginx on the box. The alternatives were putting site files inside NPM's own volume, or editing NPM's compose and restarting it — which takes every site on this machine down for a moment. So: one nginx:alpine, dist mounted READ-ONLY, on NPM's own network with NO published port. NPM reaches it by name, and nothing about the site is exposed except through the proxy. Recreated rather than restarted, for the same reason pm2 is: a container keeps the mounts it was created with, so a root location changed here would be saved, displayed, and still serving the old directory. Domains follows. Production's forward target is a container NAME, not the docker gateway that reaches a host process — and matching what already points at it uses the full target rather than the port, since every other site behind this NPM on port 80 would otherwise be listed as fofos's. The compiler found the dead branches this left in startServer once production could no longer reach it. Removed rather than left to read as though the case were still possible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+27
-6
@@ -7,7 +7,17 @@ import { encryptSecret, decryptSecret } from 'officerdb/crypto';
|
||||
import { readDatabases } from './databases';
|
||||
|
||||
/**
|
||||
* The site's two servers — development and production — run under pm2.
|
||||
* The site's DEVELOPMENT server, under pm2.
|
||||
*
|
||||
* ── Production is not here, and that is the point ──
|
||||
*
|
||||
* The site's own build script says the public site is a directory of files behind nginx, not a Bun
|
||||
* process. So production is a build and a web server with no application in it — see `static-site.ts`.
|
||||
* This module is development only: `bun --hot`, which is exactly the case pm2 is right for.
|
||||
*
|
||||
* The two names remain, because a caller asking about `production` should get a real answer rather than
|
||||
* a 400, and because the panel still shows two rows. `production` here reports the pm2 process that no
|
||||
* longer exists — which is correct, and is why `startServer` refuses it.
|
||||
*
|
||||
* ── pm2 for BOTH, including development ──
|
||||
*
|
||||
@@ -74,7 +84,7 @@ const PURPOSE = 'fofos-admin';
|
||||
* Regenerating on every start would be worse still — it would invalidate every session and every token
|
||||
* anybody had written down.
|
||||
*/
|
||||
async function siteSecrets(): Promise<{ apiToken: string; sessionSecret: string }> {
|
||||
export async function siteSecrets(): Promise<{ apiToken: string; sessionSecret: string }> {
|
||||
const row = await readRow();
|
||||
const existing = {
|
||||
apiToken: row?.siteApiToken ? decryptSecret(PURPOSE, row.siteApiToken) : null,
|
||||
@@ -226,15 +236,25 @@ export async function savePorts(dev: unknown, prod: unknown): Promise<ServerStat
|
||||
* override what somebody chose.
|
||||
*/
|
||||
export async function startServer(which: ServerName): Promise<{ ok: boolean; output: string }> {
|
||||
/*
|
||||
Production has no process to start.
|
||||
|
||||
Refused here rather than in the route, so there is one place that knows it — a check in the panel
|
||||
alone would be a rule the API did not have, and the API is what an agent reaches.
|
||||
*/
|
||||
if (which === 'production') {
|
||||
throw new Error('Production is a static build served by nginx, not a process. Build it instead.');
|
||||
}
|
||||
|
||||
const row = await readRow();
|
||||
if (!row?.rootPath) throw new Error('No root location is set.');
|
||||
|
||||
const port = which === 'development' ? row.devPort : row.prodPort;
|
||||
const port = row.devPort;
|
||||
if (!port) throw new Error(`No ${which} port is set.`);
|
||||
|
||||
const websiteDir = join(row.rootPath, 'website');
|
||||
const databases = await readDatabases();
|
||||
const target = which === 'development' ? databases.development : databases.production;
|
||||
const target = databases.development;
|
||||
if (!target.name || !target.exists) throw new Error(`The ${which} database does not exist yet.`);
|
||||
|
||||
const raw = process.env.OFFICERDEV_POSTGRES_URL ?? process.env.POSTGRES_URL;
|
||||
@@ -265,7 +285,8 @@ export async function startServer(which: ServerName): Promise<{ ok: boolean; out
|
||||
// to whatever the repository shipped with. Same reasoning as the schema push.
|
||||
...(databases.production.name ? { FOFOS_DB_PRODUCTION: databases.production.name } : {}),
|
||||
...(databases.development.name ? { FOFOS_DB_DEVELOPMENT: databases.development.name } : {}),
|
||||
...(which === 'production' ? { POSTGRES_URL: url.toString() } : { POSTGRES_URL_DEV: url.toString() }),
|
||||
// Only development reaches here — production is refused above — so this is always the dev URL.
|
||||
POSTGRES_URL_DEV: url.toString(),
|
||||
};
|
||||
|
||||
// Removed first so a stale process cannot survive with the old environment. Failure is expected and
|
||||
@@ -283,7 +304,7 @@ export async function startServer(which: ServerName): Promise<{ ok: boolean; out
|
||||
websiteDir,
|
||||
'--',
|
||||
'run',
|
||||
which === 'development' ? 'dev' : 'start',
|
||||
'dev',
|
||||
],
|
||||
120_000,
|
||||
env,
|
||||
|
||||
Reference in New Issue
Block a user