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
+89
View File
@@ -0,0 +1,89 @@
# RustDesk Server — VPS (ROOTLESS PODMAN) port of the home server's ~/Server/rustdesk,
# migrated 2026-08-12.
#
# ⚠️ THIS SERVICE CANNOT GO BEHIND CLOUDFLARE'S PROXY.
# RustDesk's ID/relay protocol is raw TCP/UDP on 21115-21119. Cloudflare's proxy only
# carries HTTP/HTTPS; raw TCP needs Spectrum, which is not on this plan. So
# rustdesk.gbrown.org MUST be a grey-cloud (DNS-only) record pointing at
# 178.156.197.55, and these ports must be open in firewalld's public zone.
#
# Consequence, accepted deliberately: this publishes the VPS's real IP. That is
# tolerable because the firewall still restricts 80/443 to Cloudflare ranges, so
# knowing the IP does not grant access to any of the web services.
#
# The server keypair in ./volumes/server/id_ed25519 was carried over from the home
# server. Its public half matches RUSTDESK_API_RUSTDESK_KEY below
# (WLgvHhau6aa5nDPQutTHeQBpIrOEb8aPXByVBWQwkKc=), which is what lets existing clients
# reconnect without being re-paired. NEVER regenerate it.
#
# Rootless adaptations:
# - :Z on both bind mounts (SELinux Enforcing); each is exclusive to this container.
# - All published ports are >1024 so no privileged-port handling is needed.
# - The image starts as root under s6 and drops privileges internally, so no
# userns_mode is required.
#
# PORT REFERENCE (all must be open in the firewall):
# 21114/tcp web UI + API 21117/tcp hbbr relay
# 21115/tcp hbbs NAT type test 21118/tcp websocket (web client)
# 21116/tcp hbbs ID registration 21119/tcp websocket relay
# 21116/udp hbbs heartbeat <-- UDP, easy to forget
networks:
nginx-bridge:
external: true
services:
rustdesk-server:
image: docker.io/lejianwen/rustdesk-server-s6:latest
container_name: rustdesk-server
hostname: rustdesk-server
domainname: rustdesk.gbrown.org
networks: ['nginx-bridge']
ports:
- 21114:21114
- 21115:21115
- 21116:21116
- 21116:21116/udp
- 21117:21117
- 21118:21118
- 21119:21119
environment:
- MUST_LOGIN=Y
- TZ=America/New_York
# SPLIT HOSTNAMES — this is deliberate, do not "simplify" it back to one name.
# relay.gbrown.org DNS-only (grey cloud) -> 178.156.197.55
# Carries the raw TCP/UDP protocol on 21115-21119.
# Cloudflare's proxy only handles HTTP/HTTPS, so this
# hostname MUST bypass it. That publishes the VPS IP,
# which is acceptable: the firewall still restricts
# 80/443 to Cloudflare ranges, so knowing the IP grants
# no access to any web service.
# rustdesk.gbrown.org Proxied -> NPM -> rustdesk-server:21114
# The web console and API, over TLS via Cloudflare.
# Keeping the API on the proxied name is what avoids sending login
# credentials over plain HTTP.
# RELAY is read by the s6 run script as `hbbs -r $RELAY`. It is what hbbs
# hands back to clients when P2P hole-punching fails, so it MUST be set here:
# the RUSTDESK_API_* vars below only configure the web console/API, not hbbs.
# Left unset, the image defaults to `relay.example.com`, which resolves to
# nothing -- so every connection needing a relay (i.e. every off-LAN,
# off-WireGuard client behind CGNAT) silently fails.
- RELAY=relay.gbrown.org
- RUSTDESK_API_RUSTDESK_ID_SERVER=relay.gbrown.org
- RUSTDESK_API_RUSTDESK_RELAY_SERVER=relay.gbrown.org
- RUSTDESK_API_RUSTDESK_API_SERVER=https://rustdesk.gbrown.org
- RUSTDESK_API_RUSTDESK_KEY=WLgvHhau6aa5nDPQutTHeQBpIrOEb8aPXByVBWQwkKc=
- RUSTDESK_API_RUSTDESK_JWT_KEY=${RUSTDESK_API_RUSTDESK_JWT_KEY}
- RUSTDESK_API_LANG=en
- RUSTDESK_API_APP_DISABLE_PWD_LOGIN=true
- RUSTDESK_API_ADMIN_TITLE=Gib's Rustdesk
- RUSTDESK_API_ADMIN_HELLO=<h1>Welcome to Gib's Rustdesk<h1>
- ENCRYPTED_ONLY=1
- RUSTDESK_API_RUSTDESK_WEBCLIENT_MAGIC_QUERYONLINE=1
labels:
com.centurylinklabs.watchtower.enable: "true"
volumes:
- ./volumes/server:/data:Z
- ./volumes/api:/app/data:Z
restart: unless-stopped
tty: true