# 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. ```sh 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. ```sh 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 ```sh 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: ```sh # On your laptop: cd frontend && npm install && npm run build rsync -a dist/ ben.stull@:/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: ```sh cd /opt/rfc-app/frontend && sudo -u rfc-app npm install sudo -u rfc-app npm run build ``` ### 3.3 Write `.env` ```sh 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: ```ini GITEA_URL=https://git.wiggleverse.org GITEA_BOT_USER=rfc-bot GITEA_BOT_TOKEN= GITEA_ORG=wiggleverse META_REPO=meta OAUTH_CLIENT_ID= OAUTH_CLIENT_SECRET= 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= 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: ```sh 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`. ```sh 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 ```sh 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: ```sh 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 ```sh 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 ```sh 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: ```sh 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](../README.md#seeding-an-active-rfc-for-8-testing)). If anything misfires, the troubleshooting section below covers the common failure modes. --- ## 7. Updating after a push ```sh 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.