Panama learns what a server is: from a root login to running containers

A machine's role is now the interview's first question and the one answer
Panama records. Servers get the same shell minus the screen: core packages,
nvm, Bun, Claude Code and Codex (desktops get Codex too), linger, rootless
ports from 80, firewalld, the nginx-bridge network, and a nightly image
updater that replaced watchtower for cause.

server/containers/ carries junior's 23 compose services -- secrets moved to
per-machine .env files that never enter this public repo, every transformed
compose proven to render byte-identical to what is live. 'panama server'
enables, disables and relinks them; nothing here restarts a running service.
'boot --server' walks a fresh VPS from its root login to a normal install.

Five new contracts pin the secrets rule, the catalog's shape, panama-server's
behavior, the role plumbing, and the dotfile classification.

Claude-Session: https://claude.ai/code/session_01NU5JGiN3JfzqrLQB6wmJ1E
This commit is contained in:
Gabriel Brown
2026-08-25 23:11:49 -04:00
parent 9b338608ef
commit f33da41cc6
93 changed files with 4735 additions and 247 deletions
+82
View File
@@ -0,0 +1,82 @@
# The server role
What a Panama machine runs when the interview answered `server`: the same
shell environment as every other machine, minus everything that needs a
screen, plus rootless podman compose services managed as systemd user units.
```
server/
containers/ One directory per service: compose.yml, the
podman-<name>.service user unit, and an .env.example
naming what the service needs told
scripts/ update-containers, the nightly image updater
systemd/ The units behind it, linked by setup-server
```
## The contract with ~/Server
The repository carries the definitions; the machine carries the state.
`panama server enable <Name>` creates `~/Server/<Name>/` as a **real
directory** and symlinks only `compose.yml` into it; the unit symlinks into
`~/.config/systemd/user/`. The `.env` (seeded once from `.env.example`, then
yours) and the bind-mounted `data/` live in `~/Server/<Name>/` and are never
inside the checkout — so nothing git can do, `clean -fdx` included, can reach
a database.
Because of that split, compose files must reference secrets as `${VAR}`
interpolations resolved from the `.env` beside them, never inline.
`tests/server/compose-secrets-contract` fails the suite when a tracked file
under `server/` carries anything that looks like a secret: this repository is
public, and the gitignore is a seatbelt, not the brakes.
Placeholders in `.env.example` are spelled `CHANGE_ME`, exactly — that token
is what `panama server enable` refuses to start on.
## Conventions
- Service directories are TitleCase, matching `~/Server` as it has always
looked: `server/containers/Gitea` becomes `~/Server/Gitea`.
- Bind mounts live under `./data/` in new services. Imported services keep
the volume path they were running with until their cutover, because the
repo copy must render identically to the live one — renames happen with
the service stopped, not in git.
- Only 80, 443 and 81 are open (setup-server's doing). Everything else is
reverse-proxied over the `nginx-bridge` network by container name, with no
published ports of its own. A service that truly needs another host port
says so in a README in its directory, and the port is opened by hand.
- Every compose joins `nginx-bridge` (external; created by setup-server).
## Enabling, updating, cutting over
```sh
panama server list # the catalog, and what this machine runs
panama server enable Gitea # link, seed .env, enable --now
panama server status # is what is enabled actually up
panama server disable Gitea # stop and unlink; data and .env stay
```
`panama update` relinks and reloads but **never restarts a running
service** — it names the ones whose definitions changed and leaves the
restart to you.
Moving a service that already runs from hand-managed files onto the repo's
(the cutover): stop the unit, standardize the volume directory to `data/`
(updating the repo compose to match), then `panama server enable <Name>`
it backs up the hand-written files as `*.pre-panama` and links the tracked
ones — and verify it came back up. One service at a time, quiet hours.
## Adding a service
Add `server/containers/<Name>/` with the three files, modelled on any
existing service. The unit is the standard oneshot `podman compose up -d`
shape (`WorkingDirectory=%h/Server/<Name>`); the shape contract pins the
details. Secrets go in `.env.example` as `KEY=CHANGE_ME` lines.
## Nightly updates
`server/scripts/update-containers` pulls images and restarts changed
services through their units, nightly at midnight via `podman-update.timer`.
It replaced watchtower after watchtower recreated a container outside its
compose pod and took Gitea down for three days — the script's header carries
the full story, plus the SKIP list for services that must only ever be
updated by hand (postgresql, authentik).