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:
2026-08-29 19:36:29 +01:00
co-authored by Claude Opus 5
parent 752c06d24a
commit 27f93d6e14
6 changed files with 472 additions and 22 deletions
+33 -1
View File
@@ -11,7 +11,8 @@ import { reconcile } from './derive';
import { createDatabases, pushSchema, readDatabases } from './databases';
import { readBackups, restoreBackup, runBackup, saveBackups, schedule, testDestination } from './backups';
import { watchMailbox } from './messages';
import { readServers, savePorts, startServer, stopServer } from './servers';
import { readServers, savePorts, siteSecrets, startServer, stopServer } from './servers';
import { buildSite, readStatic, startStatic, stopStatic } from './static-site';
import { join } from 'node:path';
import { fofosRequest, FofosError, probe, type FofosResponse } from './fofos-client';
@@ -422,6 +423,37 @@ router.put('/_config/servers', async (ctx) => {
}
});
/**
* Production — a static build, and the nginx that serves it.
*
* Separate routes from the servers above because it is a different kind of thing: there is no process,
* no port and nothing to restart. A build either succeeded or did not, and a container is either
* serving the directory or is not.
*/
router.get('/_config/static', async (ctx) => ctx.json(await readStatic()));
router.post('/_config/static/build', async (ctx) => {
try {
const result = await buildSite(await siteSecrets());
return ctx.json({ ...result, status: await readStatic() }, result.ok ? 200 : 400);
} catch (error) {
return ctx.json({ error: (error as Error).message }, 400);
}
});
router.post('/_config/static/:action', async (ctx) => {
const action = ctx.req.param('action');
if (action !== 'start' && action !== 'stop') {
return ctx.json({ error: 'Action must be start or stop.' }, 400);
}
try {
const result = action === 'start' ? await startStatic() : await stopStatic();
return ctx.json({ ...result, status: await readStatic() }, result.ok ? 200 : 400);
} catch (error) {
return ctx.json({ error: (error as Error).message }, 400);
}
});
router.post('/_config/servers/:which/:action', async (ctx) => {
const which = ctx.req.param('which');
const action = ctx.req.param('action');