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
+66
View File
@@ -0,0 +1,66 @@
AUTH_AUTHENTIK_ID=CHANGE_ME
AUTH_AUTHENTIK_ISSUER="https://auth.gbrown.org/application/o/spoon/"
AUTH_AUTHENTIK_SECRET=CHANGE_ME
AUTH_GITHUB_ID="Iv23liygrKjd17rba96x"
AUTH_GITHUB_SECRET=CHANGE_ME
BACKEND_CONTAINER_NAME="spoon-backend"
BACKEND_DOMAIN="convex.spoon.gbrown.org"
BACKEND_PORT="3210"
BACKEND_TAG="latest"
CONVEX_CLOUD_ORIGIN="https://api.spoon.gbrown.org"
CONVEX_SELF_HOSTED_ADMIN_KEY=CHANGE_ME
CONVEX_SELF_HOSTED_URL="https://api.spoon.gbrown.org"
CONVEX_SITE_ORIGIN="https://convex.spoon.gbrown.org"
CONVEX_SITE_URL="https://spoon.gbrown.org"
DASHBOARD_CONTAINER_NAME="spoon-dashboard"
DASHBOARD_DOMAIN="dashboard.spoon.gbrown.org"
DASHBOARD_PORT="6791"
DASHBOARD_TAG="latest"
DISABLE_BEACON="true"
DO_NOT_REQUIRE_SSL="true"
GITHUB_APP_CLIENT_ID="Iv23liygrKjd17rba96x"
GITHUB_APP_CLIENT_SECRET=CHANGE_ME
GITHUB_APP_ID="4111484"
GITHUB_APP_INSTALLATION_ID="141786602"
GITHUB_APP_OWNER="gibbyb"
GITHUB_APP_PRIVATE_KEY=CHANGE_ME
GITHUB_APP_SLUG="spoon-gbrown"
GITHUB_APP_WEBHOOK_SECRET=CHANGE_ME
INSTANCE_NAME="convex"
NETWORK="nginx-bridge"
NEXT_CONTAINER_NAME="spoon-next"
NEXT_DOMAIN="spoon.gbrown.org"
NEXT_PORT="3000"
NEXT_PUBLIC_CONVEX_URL="https://api.spoon.gbrown.org"
NEXT_PUBLIC_DEPLOYMENT_URL="https://api.spoon.gbrown.org"
NEXT_PUBLIC_PLAUSIBLE_URL="https://plausible.gbrown.org"
NEXT_PUBLIC_SENTRY_DSN="https://[email protected]/8"
NEXT_PUBLIC_SENTRY_ORG="sentry"
NEXT_PUBLIC_SENTRY_PROJECT_NAME="spoon-nextjs"
NEXT_PUBLIC_SENTRY_URL="https://sentry.gbrown.org"
NEXT_PUBLIC_SITE_URL="https://spoon.gbrown.org"
NODE_ENV="production"
POSTGRES_CONTAINER_NAME="spoon-postgres"
POSTGRES_DB="spoon_convex"
POSTGRES_PASSWORD=CHANGE_ME
POSTGRES_PORT="5432"
POSTGRES_URL=CHANGE_ME
POSTGRES_USER="spoon"
REDACT_LOGS_TO_CLIENT="true"
SENTRY_AUTH_TOKEN=CHANGE_ME
SITE_PROXY_PORT="3211"
SPOON_ENCRYPTION_KEY=CHANGE_ME
SPOON_WORKER_TOKEN=CHANGE_ME
USESEND_API_KEY=CHANGE_ME
USESEND_FROM_EMAIL="Spoon Admin <[email protected]>"
USESEND_URL="https://usesend.gbrown.org"
SPOON_AGENT_JOB_IMAGE=git.gbrown.org/gib/spoon-agent-job:latest
SPOON_AGENT_JOB_TIMEOUT_MS="1800000"
SPOON_AGENT_MAX_CONCURRENT_JOBS="1"
SPOON_AGENT_NETWORK=nginx-bridge
SPOON_AGENT_RUNTIME=docker
SPOON_AGENT_WORKDIR=/var/lib/spoon-agent/work
SPOON_AGENT_WORKER_ID=production-worker
SPOON_AGENT_WORKER_HTTP_PORT="3921"
SPOON_AGENT_WORKER_INTERNAL_TOKEN=CHANGE_ME
SPOON_AGENT_WORKER_URL=http://spoon-agent-worker:3921
+114
View File
@@ -0,0 +1,114 @@
networks:
nginx-bridge:
external: true
services:
spoon-next:
image: git.gbrown.org/gib/${NEXT_CONTAINER_NAME}:latest
container_name: ${NEXT_CONTAINER_NAME}
hostname: ${NEXT_CONTAINER_NAME}
domainname: ${NEXT_DOMAIN}
networks:
- ${NETWORK:-nginx-bridge}
pull_policy: missing
environment:
- NODE_ENV=${NODE_ENV}
- SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN}
- NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL:-http://localhost:${NEXT_PORT:-3000}}
- NEXT_PUBLIC_CONVEX_URL=${NEXT_PUBLIC_CONVEX_URL:-http://${BACKEND_CONTAINER_NAME:-spoon-backend}:${BACKEND_PORT:-3210}}
- NEXT_PUBLIC_PLAUSIBLE_URL=${NEXT_PUBLIC_PLAUSIBLE_URL:-https://plausible.gbrown.org}
- NEXT_PUBLIC_SENTRY_DSN=${NEXT_PUBLIC_SENTRY_DSN}
- NEXT_PUBLIC_SENTRY_URL=${NEXT_PUBLIC_SENTRY_URL}
- NEXT_PUBLIC_SENTRY_ORG=${NEXT_PUBLIC_SENTRY_ORG:-sentry}
- NEXT_PUBLIC_SENTRY_PROJECT_NAME=${NEXT_PUBLIC_SENTRY_PROJECT_NAME}
- SPOON_AGENT_WORKER_URL=${SPOON_AGENT_WORKER_URL:-http://spoon-agent-worker:3921}
- SPOON_AGENT_WORKER_INTERNAL_TOKEN=${SPOON_AGENT_WORKER_INTERNAL_TOKEN}
- SPOON_WORKER_TOKEN=${SPOON_WORKER_TOKEN}
depends_on:
- spoon-backend
labels:
- com.centurylinklabs.watchtower.enable=true
tty: true
stdin_open: true
restart: unless-stopped
spoon-agent-worker:
image: git.gbrown.org/gib/spoon-agent-worker:latest
container_name: spoon-agent-worker
hostname: spoon-agent-worker
domainname: worker.${NEXT_DOMAIN:-spoon.gbrown.org}
networks:
- ${NETWORK:-nginx-bridge}
pull_policy: missing
environment:
- GITHUB_APP_ID=${GITHUB_APP_ID}
- GITHUB_APP_PRIVATE_KEY=${GITHUB_APP_PRIVATE_KEY}
- NEXT_PUBLIC_CONVEX_URL=https://api.spoon.gbrown.org
- SPOON_AGENT_WORKER_ID=${SPOON_AGENT_WORKER_ID:-production-worker}
- SPOON_AGENT_JOB_IMAGE=${SPOON_AGENT_JOB_IMAGE:-git.gbrown.org/gib/spoon-agent-job:latest}
- SPOON_AGENT_RUNTIME=docker
- SPOON_AGENT_NETWORK=${NETWORK:-nginx-bridge}
- SPOON_AGENT_WORKDIR=/var/lib/spoon-agent/work
- SPOON_AGENT_HOST_WORKDIR=/var/lib/spoon-agent/work
- SPOON_AGENT_WORKER_HTTP_PORT=${SPOON_AGENT_WORKER_HTTP_PORT:-3921}
- SPOON_AGENT_WORKER_INTERNAL_TOKEN=${SPOON_AGENT_WORKER_INTERNAL_TOKEN}
- SPOON_AGENT_MAX_CONCURRENT_JOBS=${SPOON_AGENT_MAX_CONCURRENT_JOBS:-1}
- SPOON_AGENT_JOB_TIMEOUT_MS=${SPOON_AGENT_JOB_TIMEOUT_MS:-1800000}
- SPOON_WORKER_TOKEN=${SPOON_WORKER_TOKEN}
volumes:
- /run/user/1000/podman/podman.sock:/var/run/docker.sock
- ./volumes/agent-work:/var/lib/spoon-agent/work:z
labels:
- com.centurylinklabs.watchtower.enable=true
tty: true
stdin_open: true
restart: unless-stopped
security_opt:
- label:disable
spoon-backend:
image: ghcr.io/get-convex/convex-backend:${BACKEND_TAG:-latest}
container_name: ${BACKEND_CONTAINER_NAME:-spoon-backend}
hostname: ${BACKEND_CONTAINER_NAME:-spoon-backend}
domainname: ${BACKEND_DOMAIN:-convex.spoon.gbrown.org}
networks:
- ${NETWORK:-nginx-bridge}
volumes:
- ./volumes/convex:/convex/data:z
pull_policy: missing
environment:
- INSTANCE_NAME=${INSTANCE_NAME}
- CONVEX_CLOUD_ORIGIN=${CONVEX_CLOUD_ORIGIN:-http://${BACKEND_CONTAINER_NAME:-spoon-backend}:${BACKEND_PORT:-3210}}
- CONVEX_SITE_ORIGIN=${CONVEX_SITE_ORIGIN:-http://${BACKEND_CONTAINER_NAME:-spoon-backend}:${SITE_PROXY_PORT:-3211}}
- DISABLE_BEACON=${DISABLE_BEACON:-true}
- REDACT_LOGS_TO_CLIENT=${REDACT_LOGS_TO_CLIENT:-true}
- DO_NOT_REQUIRE_SSL=${DO_NOT_REQUIRE_SSL:-false}
- POSTGRES_URL=${POSTGRES_URL}
labels:
- com.centurylinklabs.watchtower.enable=true
stdin_open: true
tty: true
restart: unless-stopped
healthcheck:
test: curl -f http://localhost:3210/version
interval: 5s
start_period: 10s
stop_grace_period: 10s
stop_signal: SIGINT
spoon-dashboard:
image: ghcr.io/get-convex/convex-dashboard:${DASHBOARD_TAG:-latest}
container_name: ${DASHBOARD_CONTAINER_NAME:-spoon-dashboard}
hostname: ${DASHBOARD_CONTAINER_NAME:-spoon-dashboard}
domainname: ${DASHBOARD_DOMAIN:-dashboard.${BACKEND_DOMAIN:-spoon.gbrown.org}}
networks:
- ${NETWORK:-nginx-bridge}
pull_policy: missing
environment:
- NEXT_PUBLIC_DEPLOYMENT_URL=${NEXT_PUBLIC_DEPLOYMENT_URL:-http://${BACKEND_CONTAINER_NAME:-spoon-backend}:${PORT:-3210}}
depends_on:
spoon-backend:
condition: service_healthy
labels:
- com.centurylinklabs.watchtower.enable=true
stdin_open: true
tty: true
restart: unless-stopped
stop_grace_period: 10s
stop_signal: SIGINT
@@ -0,0 +1,21 @@
[Unit]
RequiresMountsFor=/home/gib/Media
Description=Podman Compose: Spoon
After=network-online.target podman.socket
Wants=network-online.target
StartLimitIntervalSec=300
StartLimitBurst=3
[Service]
Type=oneshot
WorkingDirectory=%h/Server/Spoon
ExecStart=/usr/bin/podman compose up -d
ExecStop=/usr/bin/podman compose down
RemainAfterExit=yes
TimeoutStartSec=900
TimeoutStopSec=90
Restart=on-failure
RestartSec=30
[Install]
WantedBy=default.target