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
90 lines
4.2 KiB
YAML
90 lines
4.2 KiB
YAML
# 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
|