Files
Panama/server
Gabriel Brown f33da41cc6 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
2026-08-25 23:11:49 -04:00
..

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

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).