Fix stale deployment docs found during a pre-launch review

DEPLOYMENT.md: two spots (§7 backups, §9 known gaps) said "images" when
they should say "images and file attachments" -- generic file attachments
(#13) share the exact same no-backup-coverage and orphaned-upload gaps as
images, but the wording was never updated when that feature shipped.

ARCHITECTURE.md §9.2 and the tech-stack table described a materially
different, outdated architecture: nginx running on the app server,
reverse-proxying to gunicorn over a Unix socket. The actual setup (which
DEPLOYMENT.md already correctly documents) has no nginx on the app server
at all -- gunicorn binds a TCP port directly, and TLS/reverse-proxying is
handled by an external, pre-existing Nginx Proxy Manager instance. Rewrote
both to match reality.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 22:10:39 -06:00
co-authored by Claude Sonnet 5
parent afdd573182
commit e20916bca9
2 changed files with 32 additions and 51 deletions
+20 -39
View File
@@ -26,7 +26,7 @@ database and Redis; the other runs the application and serves the frontend.
| Cross-instance broadcast | Redis (pub/sub) | Lets multiple app server processes fan out messages to all connected clients | | Cross-instance broadcast | Redis (pub/sub) | Lets multiple app server processes fan out messages to all connected clients |
| Push notifications | pywebpush + VAPID | Standard Web Push, works on Android and iOS 16.4+ (PWA must be installed to home screen on iOS) | | Push notifications | pywebpush + VAPID | Standard Web Push, works on Android and iOS 16.4+ (PWA must be installed to home screen on iOS) |
| Frontend | React + Vite, vite-plugin-pwa | Generates the manifest and service worker for install + push | | Frontend | React + Vite, vite-plugin-pwa | Generates the manifest and service worker for install + push |
| Reverse proxy / TLS | Nginx + Let's Encrypt (certbot) | Terminates TLS, serves static assets, proxies REST + WebSocket traffic | | Reverse proxy / TLS | External Nginx Proxy Manager (pre-existing infra, not deployed by this project) | Terminates TLS, forwards REST + WebSocket traffic to the app server's one port. The app itself serves the built static frontend directly — no separate static-asset server needed |
| Process management | systemd | No Docker — native to the target Linux distro, no extra install | | Process management | systemd | No Docker — native to the target Linux distro, no extra install |
| Auth | Session cookies (httpOnly, secure) | Simplest to carry through a WebSocket handshake automatically | | Auth | Session cookies (httpOnly, secure) | Simplest to carry through a WebSocket handshake automatically |
@@ -38,7 +38,7 @@ database and Redis; the other runs the application and serves the frontend.
PWA client (browser + service worker) PWA client (browser + service worker)
| REST + WebSocket | REST + WebSocket
v v
FastAPI app server (N instances behind Nginx) FastAPI app server (behind an external reverse proxy / TLS terminator)
| | | | | |
v v v v v v
PostgreSQL Redis pub/sub Push service (pywebpush) PostgreSQL Redis pub/sub Push service (pywebpush)
@@ -198,67 +198,48 @@ Runs PostgreSQL and Redis.
### 9.2 App server ### 9.2 App server
Runs the FastAPI app and Nginx; serves the built PWA static files. Runs the FastAPI app, which also serves the built PWA static files directly
(`backend/app/main.py` mounts `frontend/dist` alongside `/api` and `/ws`) —
no separate web server runs on this box.
- Python virtualenv, application installed via `pip install -e .` or similar. - Python virtualenv, application installed via `pip install -e .` or similar.
- App run via Gunicorn with Uvicorn workers, one process per CPU core as a - App run via Gunicorn with Uvicorn workers, one process per CPU core as a
starting point, managed by a systemd unit: starting point, managed by a systemd unit binding a plain TCP port:
```ini ```ini
# /etc/systemd/system/ds-chat.service # /etc/systemd/system/ds-chat.service
[Unit] [Unit]
Description=Chat service app server Description=DS Chat app server
After=network.target After=network.target
[Service] [Service]
User=ds-chat User=ds-chat
WorkingDirectory=/srv/ds-chat WorkingDirectory=/srv/ds-chat/backend
EnvironmentFile=/etc/ds-chat/env EnvironmentFile=/etc/ds-chat/env
ExecStart=/srv/ds-chat/venv/bin/gunicorn app.main:app \ ExecStart=/srv/ds-chat/backend/.venv/bin/gunicorn app.main:app \
-k uvicorn.workers.UvicornWorker \ -k uvicorn.workers.UvicornWorker \
--workers 4 \ --workers 4 \
--bind unix:/run/ds-chat/ds-chat.sock --bind 0.0.0.0:8000
Restart=on-failure Restart=on-failure
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
``` ```
- Nginx terminates TLS (certbot-managed certificate), serves the built frontend - TLS termination and public-facing reverse proxying are **not** handled on
assets directly, and reverse-proxies API and WebSocket traffic to the Unix this box — they're handled by an existing, separate Nginx Proxy Manager
socket: (NPM) instance elsewhere in the infrastructure, configured to forward
`chat.example.com` to this server's port 8000 with WebSocket support
```nginx enabled (required — without it `/ws/chat` can't upgrade). A firewall rule
server { (`ufw`) restricts port 8000 to NPM's address only — see `DEPLOYMENT.md`
listen 443 ssl; §3g/§4 for the exact commands and NPM configuration steps.
server_name chat.example.com;
root /srv/ds-chat/frontend/dist;
try_files $uri /index.html;
location /api/ {
proxy_pass http://unix:/run/ds-chat/ds-chat.sock;
proxy_set_header Host $host;
}
location /ws/ {
proxy_pass http://unix:/run/ds-chat/ds-chat.sock;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
```
- Secrets (database URL pointing at the data server's private IP, Redis URL, - Secrets (database URL pointing at the data server's private IP, Redis URL,
VAPID keys, session secret) live in `/etc/ds-chat/env`, loaded via VAPID keys, session secret) live in `/etc/ds-chat/env`, loaded via
`EnvironmentFile=`, never committed to the repository. `EnvironmentFile=`, never committed to the repository.
- Deploy process: `git pull`, install/update dependencies, `alembic upgrade - Deploy process: `git pull`, install/update dependencies, `alembic upgrade
head`, build the frontend, `systemctl restart ds-chat`, `nginx -s reload` if head`, build the frontend, `systemctl restart ds-chat` — automated end to
the Nginx config changed. end by `deploy/upgrade.sh`.
- Logs: `journalctl -u ds-chat`, rotated by systemd/journald defaults; add - Logs: `journalctl -u ds-chat`, rotated by systemd/journald defaults.
`logrotate` if the app also writes its own log files.
## 10. Security considerations ## 10. Security considerations
+12 -12
View File
@@ -285,12 +285,12 @@ producing a gzipped `pg_dump` in `/var/backups/ds-chat/` with 14-day local
rotation. Off-box shipping is a placeholder in that script (commented-out rotation. Off-box shipping is a placeholder in that script (commented-out
rsync/S3 examples) — decide where those need to go and fill it in. rsync/S3 examples) — decide where those need to go and fill it in.
That script covers Postgres only. Uploaded chat images live on the **app** That script covers Postgres only. Uploaded chat images and file attachments
server's disk (`/srv/ds-chat/uploads`, created in §3a) — a separate machine both live on the **app** server's disk (`/srv/ds-chat/uploads`, created in
from the data server this script runs on — and currently have no backup §3a) — a separate machine from the data server this script runs on — and
mechanism at all. Whatever off-box destination you pick above, include currently have no backup mechanism at all. Whatever off-box destination you
`/srv/ds-chat/uploads` in it too (e.g. a second `rsync` line run from the pick above, include `/srv/ds-chat/uploads` in it too (e.g. a second `rsync`
app server). line run from the app server).
**Test a restore** (against a scratch database, never directly onto **Test a restore** (against a scratch database, never directly onto
`ds_chat`): `ds_chat`):
@@ -334,12 +334,12 @@ scope decisions" for the full detail on each):
re-validated per delivery (DNS-rebinding gap). re-validated per delivery (DNS-rebinding gap).
- Backup off-box shipping is a placeholder — decide a destination and fill - Backup off-box shipping is a placeholder — decide a destination and fill
in `deploy/backup-postgres.sh`. in `deploy/backup-postgres.sh`.
- Uploaded chat images (`/srv/ds-chat/uploads` on the app server) have no - Uploaded chat images and file attachments (`/srv/ds-chat/uploads` on the
backup coverage at all yet, on-box or off — see §7. app server) have no backup coverage at all yet, on-box or off — see §7.
- Uploaded-but-never-sent images (a user attaches a file, then never hits - Uploaded-but-never-sent images or files (a user attaches one, then never
Send) leak an orphaned file on disk — no cleanup job for this yet. Not a hits Send) leak an orphaned file on disk — no cleanup job for this yet.
security issue (still gated by room membership to view), just an eventual Not a security issue (still gated by room membership to view), just an
disk-space housekeeping item. eventual disk-space housekeeping item.
None of these are new to this phase — deploying doesn't change any of them, None of these are new to this phase — deploying doesn't change any of them,
just makes them reachable from the internet instead of localhost, which is just makes them reachable from the internet instead of localhost, which is