Single-host deployment of the app at rfc.wiggleverse.org alongside the existing Gitea instance. nginx reverse-proxies /api/* and /auth/* to a single uvicorn process on 127.0.0.1:8000 and serves the Vite build output as static files; certbot adds the TLS cert in place; systemd supervises the process per §4.2's single-process-with-WAL-SQLite contract (one worker; raising --workers would break the invariant). deploy/DEPLOY.md is the step-by-step runbook covering host prep, Gitea bot + OAuth setup, .env shape, meta-repo seed, nginx + certbot, systemd, smoke test, and the update/rollback shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.1 KiB
Deployment runbook
Single-host deployment of the RFC app at rfc.wiggleverse.org,
sharing infrastructure with git.wiggleverse.org (same Gitea
instance, same nginx, same Let's Encrypt). The shape matches §4.2:
one process, one SQLite file, no separate worker.
Bring-up order: host prep → Gitea side (bot, OAuth, meta repo) → app side (code, venv, build, .env) → web server side (nginx, certbot) → systemd → smoke-test.
Every step is idempotent or no-op-on-rerun, so re-running this runbook to recover from a partial install is safe.
Prereqs
- Ubuntu/Debian-style host with nginx and certbot already serving
git.wiggleverse.orgover HTTPS. - DNS: an
Arecord forrfc.wiggleverse.orgpointing at the same IP asgit.wiggleverse.org. - Python 3.11+ available system-wide. Node 20+ available (for
npm run buildonce; the build output is what runs in production — Node isn't needed at runtime).
1. Host prep
Create the system user and the install directory.
sudo useradd --system --shell /usr/sbin/nologin --home-dir /opt/rfc-app rfc-app
sudo mkdir -p /opt/rfc-app
sudo chown rfc-app:rfc-app /opt/rfc-app
Clone the repo. Use HTTPS since we don't need to push from the server.
sudo -u rfc-app git clone https://git.wiggleverse.org/ben.stull/rfc-app.git /opt/rfc-app
2. Gitea side
2.1 Create the bot service account
In the Gitea web UI, signed in as a Gitea admin:
- Site Administration → User Accounts → Create User Account
- Username:
rfc-bot(or whatever you want) - Email: anything sensible (e.g.
rfc-bot@wiggleverse.org) - Password: random, you won't use it interactively
- Send email confirmation: off
Then sign in as the bot, open Settings → Applications → Generate
New Token, name it rfc-app, grant scopes:
write:repositorywrite:userwrite:admin(needed because the bot creates per-RFC repos on graduation — scope down torepo+orgif you want to defer admin until Slice 5)
Copy the token. It goes into .env as GITEA_BOT_TOKEN.
2.2 Create the org and add the bot
The meta repo lives inside an org. In Gitea: Create Organization → wiggleverse. Then Members → Invite → rfc-bot → Owner.
2.3 Register the OAuth2 application
Site Administration → Integrations → OAuth2 Applications → Create Application:
- Name:
RFC App - Redirect URI:
https://rfc.wiggleverse.org/auth/callback
Copy the client ID and client secret. They go into .env.
3. App side
3.1 Python venv + deps
sudo -u rfc-app python3 -m venv /opt/rfc-app/backend/.venv
sudo -u rfc-app /opt/rfc-app/backend/.venv/bin/pip install \
-r /opt/rfc-app/backend/requirements.txt
3.2 Build the frontend
Build locally if you don't want Node on the server; copy dist/
across:
# On your laptop:
cd frontend && npm install && npm run build
rsync -a dist/ ben.stull@<host>:/tmp/rfc-app-dist/
# On the host:
sudo -u rfc-app mkdir -p /opt/rfc-app/frontend/dist
sudo cp -r /tmp/rfc-app-dist/. /opt/rfc-app/frontend/dist/
sudo chown -R rfc-app:rfc-app /opt/rfc-app/frontend/dist
Or build on the host directly if Node is available:
cd /opt/rfc-app/frontend && sudo -u rfc-app npm install
sudo -u rfc-app npm run build
3.3 Write .env
sudo -u rfc-app cp /opt/rfc-app/backend/.env.example /opt/rfc-app/backend/.env
sudoedit /opt/rfc-app/backend/.env # set every value
Required values for production:
GITEA_URL=https://git.wiggleverse.org
GITEA_BOT_USER=rfc-bot
GITEA_BOT_TOKEN=<the token from step 2.1>
GITEA_ORG=wiggleverse
META_REPO=meta
OAUTH_CLIENT_ID=<from step 2.3>
OAUTH_CLIENT_SECRET=<from step 2.3>
APP_URL=https://rfc.wiggleverse.org
SECRET_KEY=<32+ random chars; generate with `openssl rand -hex 32`>
OWNER_GITEA_LOGIN=ben.stull
GITEA_WEBHOOK_SECRET=<random; generate with `openssl rand -hex 32`>
DATABASE_PATH=/opt/rfc-app/backend/data/rfc-app.db
# Optional — chat is disabled until at least one is set:
ENABLED_MODELS=claude
ANTHROPIC_API_KEY=
GOOGLE_API_KEY=
OPENAI_API_KEY=
Lock the file down — it carries secrets:
sudo chmod 600 /opt/rfc-app/backend/.env
sudo chown rfc-app:rfc-app /opt/rfc-app/backend/.env
3.4 Seed the meta repo
This creates wiggleverse/meta on Gitea, populates the hand-authored
files, and registers the webhook against APP_URL/api/webhooks/gitea.
sudo -u rfc-app -H bash -c \
'cd /opt/rfc-app/backend && .venv/bin/python ../scripts/seed_meta_repo.py'
Re-running is safe; every step is upsert-shaped.
4. Web server side
4.1 nginx vhost
sudo cp /opt/rfc-app/deploy/nginx/rfc.wiggleverse.org.conf \
/etc/nginx/sites-available/rfc.wiggleverse.org
sudo ln -s /etc/nginx/sites-available/rfc.wiggleverse.org \
/etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Make sure the nginx user can read /opt/rfc-app/frontend/dist. The
simplest path: add the nginx user (usually www-data) to the
rfc-app group and chmod the dist tree group-readable:
sudo usermod -a -G rfc-app www-data
sudo chmod -R g+rX /opt/rfc-app/frontend/dist
sudo systemctl reload nginx
4.2 Let's Encrypt cert
sudo certbot --nginx -d rfc.wiggleverse.org
Certbot rewrites the vhost in place to add the 443 listener and
certificate directives. After it finishes, https://rfc.wiggleverse.org
serves but the backend isn't up yet — the next step starts it.
5. systemd
sudo cp /opt/rfc-app/deploy/systemd/rfc-app.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now rfc-app
sudo systemctl status rfc-app
Watch the logs:
sudo journalctl -u rfc-app -f
Expected startup line: RFC app started — meta repo wiggleverse/meta.
6. Smoke test
In a browser at https://rfc.wiggleverse.org:
- The landing page renders. (Slice 1 placeholder; Slice 7 polishes.)
- Click Sign in with Gitea → OAuth round-trip → you land on the catalog. (Empty on first visit — no RFCs yet.)
- Click + Propose New RFC, fill in title/slug/pitch, submit. The pending-ideas disclosure shows the new PR.
- Click the PR row, then Merge proposal. The catalog refreshes with the super-draft.
- Optional: seed an active RFC for §8 testing (see README.md).
If anything misfires, the troubleshooting section below covers the common failure modes.
7. Updating after a push
sudo -u rfc-app git -C /opt/rfc-app pull
sudo -u rfc-app /opt/rfc-app/backend/.venv/bin/pip install \
-r /opt/rfc-app/backend/requirements.txt
# Rebuild the frontend locally and rsync dist/ as in step 3.2.
sudo systemctl restart rfc-app
The §5 schema migrations run on startup and are append-only, so a restart is the entire deploy. No reverse-migration story for now — add one when the schema starts breaking back-compat (deferred until Slice 8 hardening).
Troubleshooting
systemctl status rfc-appshowsRuntimeError: Required environment variable ... is not set. The.envis missing a value, orEnvironmentFile=in the systemd unit isn't finding it. Confirm/opt/rfc-app/backend/.envexists and is mode 0600 owned byrfc-app.- OAuth callback returns "Invalid state". The redirect URI in
Gitea must match
APP_URL/auth/callbackexactly. Confirm it'shttps://rfc.wiggleverse.org/auth/callback. - The catalog stays empty after a merge. Check the webhook:
journalctl -u rfc-app | grep webhook. Gitea's Settings → Webhooks → Recent Deliveries on the meta repo shows the delivery status; the reconciler will catch up within 5 minutes anyway. 502 Bad Gatewayon /api/ or /auth/*.* uvicorn isn't running or isn't bound to127.0.0.1:8000.systemctl status rfc-app.403from nginx on static assets. The nginx user can't read/opt/rfc-app/frontend/dist. Apply the chmod from step 4.1.- OAuth works, but the user can't propose. The
usersrow was created with rolecontributor; onlyOWNER_GITEA_LOGIN's login getsowneron first sign-in. Confirm.envhas the right value and you signed in with that account.