Files
ds-chat/deploy/chatapp.env.example
T
ksmithandClaude Sonnet 5 7f579bb508 Phase 8: Production deployment (Debian 13, Nginx Proxy Manager)
Deployment artifacts for the two-server architecture from ARCHITECTURE.md
§9, grounded in verified Debian 13 (trixie) package facts (Python 3.13,
PostgreSQL 17, Node.js 20, redis-server 8.0, certbot 4.0, ufw --
confirmed rather than guessed) rather than a generic "modern Linux" guide:
deploy/systemd/chatapp.service, deploy/chatapp.env.example,
deploy/backup-postgres.sh, deploy/upgrade.sh, and DEPLOYMENT.md as the
actual numbered runbook.

Revised mid-implementation once the user clarified the app sits behind an
existing, separate Nginx Proxy Manager rather than local Nginx+certbot:
dropped the local Nginx config entirely, gunicorn now binds a TCP port
instead of a Unix socket, and app/main.py gained a static-file mount + SPA
fallback route so gunicorn alone serves the built frontend, /api, and /ws
on one port -- what lets NPM's simple one-upstream-per-domain mode work
with zero custom path routing. Path-traversal-guarded (full_path comes
straight from the URL) and cache-header-differentiated (far-future
immutable on Vite's content-hashed assets, no-cache on index.html/sw.js/
manifest so a deploy actually propagates instead of leaving clients on a
stale service worker) -- verified locally against a real gunicorn process
serving a real frontend build, not just eyeballed.

Two real gaps found and fixed alongside the docs, not just noted: gunicorn
wasn't a dependency anywhere despite being the whole app-server design, and
there was no WebSocket reconnect logic on the client -- a reverse proxy's
idle-connection timeout (NPM's or otherwise) would have silently killed a
quiet chat connection with nothing to recover it. Added exponential-backoff
reconnect to useChatSocket.ts, verified by hand (killed and restarted the
local dev backend mid-session, confirmed auto-reconnect and that a message
sends successfully afterward with no page reload).

Every command in DEPLOYMENT.md that could be verified locally, was: the
exact systemd ExecStart line run against local dev Postgres/Redis with
clean SIGTERM shutdown, the static-file serving behavior against a real
build, both shell scripts syntax-checked. What couldn't be verified from
this sandbox (actual Debian 13 hardware, Nginx Proxy Manager itself) is
flagged explicitly in the plan rather than claimed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 11:30:55 -06:00

38 lines
1.5 KiB
Bash

# /etc/chatapp/env (production)
#
# This file is loaded by systemd's EnvironmentFile= (see
# deploy/systemd/chatapp.service) directly into the app process's
# environment -- it is NOT a dotenv file Python reads from a working
# directory, and it must never be committed to the repository.
#
# Install:
# sudo mkdir -p /etc/chatapp
# sudo cp deploy/chatapp.env.example /etc/chatapp/env
# sudo chown root:chatapp /etc/chatapp/env
# sudo chmod 0640 /etc/chatapp/env
# # then edit in the real values below
#
# See ../DEPLOYMENT.md for how each value is generated.
# Points at the data server's PRIVATE address -- never the public one.
# The role/password here are whatever you created on the data server in
# DEPLOYMENT.md step 2.
DATABASE_URL=postgresql+asyncpg://chatapp:REPLACE_ME@<DATA_SERVER_PRIVATE_IP>:5432/chatapp
# Generate with: python3 -c "import secrets; print(secrets.token_urlsafe(32))"
SESSION_SECRET=REPLACE_ME
# true in production -- cookies are only sent over HTTPS. The local dev
# default (backend/.env.example) is false because dev runs over plain HTTP.
SESSION_HTTPS_ONLY=true
# Matches the requirepass set in /etc/redis/redis.conf on the data server
# (see DEPLOYMENT.md step 2). Same private-address rule as DATABASE_URL.
REDIS_URL=redis://:REPLACE_ME@<DATA_SERVER_PRIVATE_IP>:6379/0
# Optional: push notifications are silently skipped if these are unset.
# Generate with: .venv/bin/python -m app.cli generate-vapid-keys
VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=
VAPID_SUBJECT=mailto:you@example.com