From 905cd202eb793996a863dfd01911f96921451d05 Mon Sep 17 00:00:00 2001 From: NixReaper Date: Sat, 5 Sep 2026 20:49:55 -0500 Subject: [PATCH] Replace Traefik with host-webserver reverse proxy in prod deployment The production host runs cPanel, which already owns ports 80/443 for other sites on the box -- there's no Traefik there. docker-compose.prod.yml now binds the api container to 127.0.0.1:8000 only; the host's own Apache/LiteSpeed (via cPanel) reverse-proxies api.amdbs.net to it and terminates TLS itself (AutoSSL). Dropped the external traefik-public network requirement and Traefik labels entirely. Also corrected a separate latent inaccuracy while touching this: a bare `docker compose up` (not `docker stack deploy`) never actually honored deploy.replicas -- docs now say so and point at `--scale` instead of implying free multi-replica scaling that was never really happening. README's Production deployment section rewritten with concrete steps: Cloudflare DNS record, cPanel subdomain + reverse proxy, then compose up. Co-Authored-By: Claude Sonnet 5 --- README.md | 54 +++++++++++++++++++++++++---------------- docker-compose.prod.yml | 28 +++++++++------------ docs/api.md | 6 +++-- docs/architecture.md | 18 ++++++++------ 4 files changed, 59 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 849b374..7cfaa9b 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ build specification this repository implements, and ## Architecture at a glance -A modular monolith (not microservices): a stateless FastAPI API behind -Traefik, PostgreSQL as the sole source of truth, Redis for disposable +A modular monolith (not microservices): a stateless FastAPI API behind a +reverse proxy, PostgreSQL as the sole source of truth, Redis for disposable caching/rate-limiting/queueing, and arq workers for async metadata ingestion. Full diagram and reasoning in [`docs/architecture.md`](docs/architecture.md). @@ -170,28 +170,40 @@ example. Concurrency per worker process is controlled by ## Production deployment -```bash -docker compose -f docker-compose.prod.yml up --build -d -``` +The production host runs cPanel, which already owns ports 80/443 for other +sites on the box — there's no Traefik here. `docker-compose.prod.yml` binds +the API container to `127.0.0.1:8000` only; the host's own webserver +(Apache or LiteSpeed, via cPanel) reverse-proxies `api.amdbs.net` to it and +terminates TLS itself (cPanel's AutoSSL can issue the Let's Encrypt cert +once the subdomain exists and DNS resolves). -Traefik-ready (labels on the `api` service), PgBouncer in front of -PostgreSQL (transaction pooling — `DATABASE_BEHIND_TRANSACTION_POOLER=true` -disables asyncpg's server-side prepared statements, which don't survive -transaction-mode pooling), horizontally-scaled API and worker replicas. See +1. **DNS** (Cloudflare, where `amdbs.net` is managed): add an A record for + `api.amdbs.net` pointing at the server's IP. +2. **cPanel**: create the `api.amdbs.net` subdomain so AutoSSL picks it up + and issues a cert; then reverse-proxy that vhost to `127.0.0.1:8000` — + either cPanel/WHM's Application Manager if available, or a custom + Apache/LiteSpeed vhost include (`ProxyPass`/`ProxyPassReverse` for + Apache; LiteSpeed's External App + rewrite rule for LiteSpeed). +3. **`.env`** on the server (never committed), beyond the base vars in + `.env.example`: + + ```bash + CORS_ORIGINS=https://www.amdbs.net # must include the frontend's real origin + ``` + +4. **Deploy**: + + ```bash + docker compose -f docker-compose.prod.yml up --build -d + ``` + +PgBouncer sits in front of PostgreSQL (transaction pooling — +`DATABASE_BEHIND_TRANSACTION_POOLER=true` disables asyncpg's server-side +prepared statements, which don't survive transaction-mode pooling). See [`docs/architecture.md`](docs/architecture.md). -Requires an external Traefik instance already running with a -`traefik-public` Docker network and a `letsencrypt` cert resolver -configured (`docker-compose.prod.yml` attaches to it, it doesn't run -Traefik itself). Aurora Core's production domain split is `api.amdbs.net` -(this backend) / `www.amdbs.net` (frontend, separate project) — the -production `.env` (never committed) needs, beyond the base vars in -`.env.example`: - -```bash -AURORA_API_HOST=api.amdbs.net # Traefik router rule (docker-compose.prod.yml only, not read by the app) -CORS_ORIGINS=https://www.amdbs.net # must include the frontend's real origin -``` +Aurora Core's production domain split is `api.amdbs.net` (this backend) / +`www.amdbs.net` (frontend, a separate project). ## Known limitations diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 688a3c9..977aa9a 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,12 +1,12 @@ -# Production-oriented topology (section 1/28): Traefik -> N stateless API -# replicas -> PgBouncer -> PostgreSQL, plus Redis and independently-scaled -# workers. Requires a pre-existing external Traefik network named -# `traefik-public` and a populated `.env` (never commit real secrets). +# Production-oriented topology (section 1/28): the host's own webserver +# (cPanel/Apache or LiteSpeed -- already holding ports 80/443 for other +# sites on the box) reverse-proxies to the API container on 127.0.0.1, +# terminating TLS itself; there's no Traefik here. PgBouncer sits in front +# of PostgreSQL, Redis and the worker are independently scaled. Requires a +# populated `.env` (never commit real secrets). name: aurora-core networks: - traefik-public: - external: true aurora-internal: driver: bridge @@ -97,19 +97,15 @@ services: condition: service_healthy migrate: condition: service_completed_successfully - networks: [traefik-public, aurora-internal] + networks: [aurora-internal] + # Bound to localhost only -- the host's own webserver (cPanel/Apache or + # LiteSpeed, already holding 80/443 for other sites on the box) terminates + # TLS for api.amdbs.net and reverse-proxies here. No Traefik on this box. + ports: + - "127.0.0.1:${AURORA_API_PORT:-8000}:8000" deploy: - replicas: 3 resources: limits: { cpus: "1", memory: 1G } - labels: - - "traefik.enable=true" - - "traefik.docker.network=traefik-public" - - "traefik.http.routers.aurora-core-api.rule=Host(`${AURORA_API_HOST:-api.aurora.local}`)" - - "traefik.http.routers.aurora-core-api.entrypoints=websecure" - - "traefik.http.routers.aurora-core-api.tls.certresolver=letsencrypt" - - "traefik.http.services.aurora-core-api.loadbalancer.server.port=8000" - - "traefik.http.services.aurora-core-api.loadbalancer.healthcheck.path=/health" worker: build: diff --git a/docs/api.md b/docs/api.md index d17a290..ae91d82 100644 --- a/docs/api.md +++ b/docs/api.md @@ -198,8 +198,10 @@ of actually ingesting that movie or series, so there's no separate ## Administrative (section 17: "separately protected") Require header `X-API-Key: `. Never exposed to -public clients — mount behind a separate Traefik router/ACL in production if -you want network-level isolation too. +public clients — add a rule in the host webserver's vhost config for +`api.amdbs.net` (e.g. an Apache ``/LiteSpeed rewrite blocking +`/api/v1/admin/*` from outside a trusted IP range) in production if you +want network-level isolation on top of the API key. | Method | Path | Body | Notes | |---|---|---|---| diff --git a/docs/architecture.md b/docs/architecture.md index 4682731..538fcfc 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -9,7 +9,7 @@ Aurora applications are consumers of it, never dependencies of it. ``` Internet | -Traefik +Host webserver (cPanel-managed Apache/LiteSpeed — TLS termination + reverse proxy) | Aurora Core API replicas (stateless) | @@ -191,8 +191,8 @@ development requires the observability stack to be running. `GET /health` — liveness only (process is up). `GET /ready` — checks PostgreSQL connectivity (always required) and Redis (required only if `REDIS_REQUIRED=true`, since Redis is disposable — see `docs/database.md`). -Both are mounted twice: unprefixed (`/health`, `/ready`, for -Docker/Traefik health checks) and under `/api/v1` (per section 17's endpoint +Both are mounted twice: unprefixed (`/health`, `/ready`, for Docker/host +webserver health checks) and under `/api/v1` (per section 17's endpoint list). ## Statelessness & horizontal scaling (section 1/2) @@ -200,8 +200,10 @@ list). The API holds no in-process state across requests — no server-side sessions, no in-memory caches keyed by request. Every dependency (`get_db`, `get_redis`, `get_queue`) is request-scoped and backed by a -shared pool. This is what makes `docker-compose.prod.yml`'s `api: replicas: -3` behind Traefik correct without sticky sessions. Workers -(`docker-compose.prod.yml`'s `worker: replicas: 2`) scale independently of -the API — arq's Redis-backed queue ensures each job is claimed by exactly -one worker. +shared pool, so it's correct to run more than one API instance behind a +reverse proxy without sticky sessions. Note that a bare `docker compose up` +(as opposed to `docker stack deploy` under Swarm) does not itself honor a +service's `deploy.replicas` — actually running multiple instances needs +`docker compose -f docker-compose.prod.yml up -d --scale api=N`. Workers +scale independently of the API the same way (`--scale worker=N`) — arq's +Redis-backed queue ensures each job is claimed by exactly one worker.