# 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-.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 ` creates `~/Server//` 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//` 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 ` — 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//` with the three files, modelled on any existing service. The unit is the standard oneshot `podman compose up -d` shape (`WorkingDirectory=%h/Server/`); 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).