Replace Traefik with host-webserver reverse proxy in prod deployment
CI / lint (push) Canceled after 0s
CI / typecheck (push) Canceled after 0s
CI / unit-tests (push) Canceled after 0s
CI / integration-tests (push) Canceled after 0s
CI / container-build (push) Canceled after 0s
CI / dependency-audit (push) Canceled after 0s

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 <[email protected]>
This commit is contained in:
NixReaper
2026-09-05 20:49:55 -05:00
co-authored by Claude Sonnet 5
parent ec9b1c28e3
commit 905cd202eb
4 changed files with 59 additions and 47 deletions
+30 -18
View File
@@ -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
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).
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
```
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
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
+12 -16
View File
@@ -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:
+4 -2
View File
@@ -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: <one of AURORA_ADMIN_API_KEYS>`. 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 `<Location>`/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 |
|---|---|---|---|
+10 -8
View File
@@ -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.