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:
@@ -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
|
||||
Reference in New Issue
Block a user