Files
rfc-app/deploy/DEPLOY.md
T
Ben Stull 33d9d7a482 Add deploy/ — nginx vhost, systemd unit, runbook
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>
2026-05-24 05:18:28 -07:00

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.org over HTTPS.
  • DNS: an A record for rfc.wiggleverse.org pointing at the same IP as git.wiggleverse.org.
  • Python 3.11+ available system-wide. Node 20+ available (for npm run build once; 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:repository
  • write:user
  • write:admin (needed because the bot creates per-RFC repos on graduation — scope down to repo+org if 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:

  1. The landing page renders. (Slice 1 placeholder; Slice 7 polishes.)
  2. Click Sign in with Gitea → OAuth round-trip → you land on the catalog. (Empty on first visit — no RFCs yet.)
  3. Click + Propose New RFC, fill in title/slug/pitch, submit. The pending-ideas disclosure shows the new PR.
  4. Click the PR row, then Merge proposal. The catalog refreshes with the super-draft.
  5. 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-app shows RuntimeError: Required environment variable ... is not set. The .env is missing a value, or EnvironmentFile= in the systemd unit isn't finding it. Confirm /opt/rfc-app/backend/.env exists and is mode 0600 owned by rfc-app.
  • OAuth callback returns "Invalid state". The redirect URI in Gitea must match APP_URL/auth/callback exactly. Confirm it's https://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 Gateway on /api/ or /auth/*.* uvicorn isn't running or isn't bound to 127.0.0.1:8000. systemctl status rfc-app.
  • 403 from 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 users row was created with role contributor; only OWNER_GITEA_LOGIN's login gets owner on first sign-in. Confirm .env has the right value and you signed in with that account.