33d9d7a482
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>
285 lines
8.1 KiB
Markdown
285 lines
8.1 KiB
Markdown
# 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@<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:
|
|
|
|
```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=<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:
|
|
|
|
```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.
|