Private
Public Access
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:
+20
-39
@@ -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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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)
|
||||
| REST + WebSocket
|
||||
v
|
||||
FastAPI app server (N instances behind Nginx)
|
||||
FastAPI app server (behind an external reverse proxy / TLS terminator)
|
||||
| | |
|
||||
v v v
|
||||
PostgreSQL Redis pub/sub Push service (pywebpush)
|
||||
@@ -198,67 +198,48 @@ Runs PostgreSQL and Redis.
|
||||
|
||||
### 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.
|
||||
- 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
|
||||
# /etc/systemd/system/ds-chat.service
|
||||
[Unit]
|
||||
Description=Chat service app server
|
||||
Description=DS Chat app server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=ds-chat
|
||||
WorkingDirectory=/srv/ds-chat
|
||||
WorkingDirectory=/srv/ds-chat/backend
|
||||
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 \
|
||||
--workers 4 \
|
||||
--bind unix:/run/ds-chat/ds-chat.sock
|
||||
--bind 0.0.0.0:8000
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
- Nginx terminates TLS (certbot-managed certificate), serves the built frontend
|
||||
assets directly, and reverse-proxies API and WebSocket traffic to the Unix
|
||||
socket:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- TLS termination and public-facing reverse proxying are **not** handled on
|
||||
this box — they're handled by an existing, separate Nginx Proxy Manager
|
||||
(NPM) instance elsewhere in the infrastructure, configured to forward
|
||||
`chat.example.com` to this server's port 8000 with WebSocket support
|
||||
enabled (required — without it `/ws/chat` can't upgrade). A firewall rule
|
||||
(`ufw`) restricts port 8000 to NPM's address only — see `DEPLOYMENT.md`
|
||||
§3g/§4 for the exact commands and NPM configuration steps.
|
||||
- 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
|
||||
`EnvironmentFile=`, never committed to the repository.
|
||||
- Deploy process: `git pull`, install/update dependencies, `alembic upgrade
|
||||
head`, build the frontend, `systemctl restart ds-chat`, `nginx -s reload` if
|
||||
the Nginx config changed.
|
||||
- Logs: `journalctl -u ds-chat`, rotated by systemd/journald defaults; add
|
||||
`logrotate` if the app also writes its own log files.
|
||||
head`, build the frontend, `systemctl restart ds-chat` — automated end to
|
||||
end by `deploy/upgrade.sh`.
|
||||
- Logs: `journalctl -u ds-chat`, rotated by systemd/journald defaults.
|
||||
|
||||
## 10. Security considerations
|
||||
|
||||
|
||||
+12
-12
@@ -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
|
||||
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**
|
||||
server's disk (`/srv/ds-chat/uploads`, created in §3a) — a separate machine
|
||||
from the data server this script runs on — and currently have no backup
|
||||
mechanism at all. Whatever off-box destination you pick above, include
|
||||
`/srv/ds-chat/uploads` in it too (e.g. a second `rsync` line run from the
|
||||
app server).
|
||||
That script covers Postgres only. Uploaded chat images and file attachments
|
||||
both live on the **app** server's disk (`/srv/ds-chat/uploads`, created in
|
||||
§3a) — a separate machine from the data server this script runs on — and
|
||||
currently have no backup mechanism at all. Whatever off-box destination you
|
||||
pick above, include `/srv/ds-chat/uploads` in it too (e.g. a second `rsync`
|
||||
line run from the app server).
|
||||
|
||||
**Test a restore** (against a scratch database, never directly onto
|
||||
`ds_chat`):
|
||||
@@ -334,12 +334,12 @@ scope decisions" for the full detail on each):
|
||||
re-validated per delivery (DNS-rebinding gap).
|
||||
- Backup off-box shipping is a placeholder — decide a destination and fill
|
||||
in `deploy/backup-postgres.sh`.
|
||||
- Uploaded chat images (`/srv/ds-chat/uploads` on the 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
|
||||
Send) leak an orphaned file on disk — no cleanup job for this yet. Not a
|
||||
security issue (still gated by room membership to view), just an eventual
|
||||
disk-space housekeeping item.
|
||||
- Uploaded chat images and file attachments (`/srv/ds-chat/uploads` on the
|
||||
app server) have no backup coverage at all yet, on-box or off — see §7.
|
||||
- Uploaded-but-never-sent images or files (a user attaches one, then never
|
||||
hits Send) leak an orphaned file on disk — no cleanup job for this yet.
|
||||
Not a security issue (still gated by room membership to view), just an
|
||||
eventual disk-space housekeeping item.
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user