generated from bisco/codex-bootstrap
feat: add nginx reverse proxy
This commit is contained in:
@@ -78,6 +78,7 @@ Configure the canonical test command for this repository:
|
||||
docker compose run --rm backend python manage.py test
|
||||
docker compose run --rm frontend npm run check
|
||||
docker compose run --rm frontend npm run build
|
||||
docker compose run --rm nginx nginx -t
|
||||
docker compose config --quiet
|
||||
```
|
||||
|
||||
|
||||
+4
-2
@@ -5,6 +5,8 @@ POSTGRES_PASSWORD=replace-with-a-local-password
|
||||
DATABASE_URL=postgresql://theatre_lab:replace-with-a-local-password@postgres:5432/theatre_lab
|
||||
DJANGO_SECRET_KEY=replace-with-a-long-random-development-key
|
||||
DJANGO_DEBUG=true
|
||||
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,backend
|
||||
WAGTAILADMIN_BASE_URL=http://localhost:8000
|
||||
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,backend,azionelab.org
|
||||
WAGTAILADMIN_BASE_URL=http://azionelab.org:8080
|
||||
PUBLIC_CMS_API_URL=http://backend:8000
|
||||
NGINX_BIND_ADDRESS=127.0.0.1
|
||||
NGINX_HTTP_PORT=8080
|
||||
|
||||
@@ -10,16 +10,20 @@ stores content, and Docker Compose supplies a reproducible local environment.
|
||||
aggregate read-only API at `GET /api/site/home/`.
|
||||
- `frontend/`: Astro components, responsive design system, typed API adapter, and
|
||||
Italian fallback content for development when the CMS is unavailable.
|
||||
- `nginx/`: reverse-proxy virtual host for `azionelab.org`, routing public requests to
|
||||
Astro or Wagtail.
|
||||
- `postgres`: persistent CMS database.
|
||||
- Docker volumes: `postgres_data` for the database and `media_data` for uploads.
|
||||
|
||||
See [the architecture document](docs/architecture.md) and
|
||||
[ADR-0001](docs/adr/0001-headless-wagtail-astro.md) for the rationale.
|
||||
See [the architecture document](docs/architecture.md),
|
||||
[ADR-0001](docs/adr/0001-headless-wagtail-astro.md), and
|
||||
[ADR-0002](docs/adr/0002-nginx-reverse-proxy.md) for the rationale.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker Engine with Docker Compose v2.
|
||||
- Ports `4321` and `8000` available on localhost.
|
||||
- Ports `8080`, `4321`, and `8000` available on localhost.
|
||||
- A local hosts-file entry mapping `azionelab.org` to `127.0.0.1`.
|
||||
|
||||
No host Python or Node.js installation is required.
|
||||
|
||||
@@ -32,15 +36,24 @@ docker compose ps
|
||||
docker compose exec backend python manage.py seed_demo
|
||||
```
|
||||
|
||||
For local domain resolution, add this line to `/etc/hosts` using your normal system
|
||||
administration workflow:
|
||||
|
||||
```text
|
||||
127.0.0.1 azionelab.org
|
||||
```
|
||||
|
||||
Open:
|
||||
|
||||
- public site: <http://localhost:4321>
|
||||
- Wagtail admin: <http://localhost:8000/admin/>
|
||||
- aggregate API: <http://localhost:8000/api/site/home/>
|
||||
- public site: <http://azionelab.org:8080>
|
||||
- Wagtail admin: <http://azionelab.org:8080/admin/>
|
||||
- aggregate API: <http://azionelab.org:8080/api/site/home/>
|
||||
|
||||
Direct loopback ports `4321` and `8000` remain available for local diagnostics only.
|
||||
|
||||
The frontend remains usable with curated fallback content if the API is unavailable.
|
||||
On first startup the backend applies migrations and collects static files before
|
||||
starting Gunicorn. Wait until `postgres`, `backend`, and `frontend` report healthy.
|
||||
starting Gunicorn. Wait until all four services report healthy.
|
||||
|
||||
## Create an editor account
|
||||
|
||||
@@ -48,7 +61,7 @@ starting Gunicorn. Wait until `postgres`, `backend`, and `frontend` report healt
|
||||
docker compose exec backend python manage.py createsuperuser
|
||||
```
|
||||
|
||||
Sign in to Wagtail at <http://localhost:8000/admin/>. The public site has no user
|
||||
Sign in to Wagtail at <http://azionelab.org:8080/admin/>. The public site has no user
|
||||
accounts or authentication.
|
||||
|
||||
## Demo content
|
||||
@@ -75,14 +88,14 @@ after seeding may be overwritten for records managed by the command.
|
||||
|
||||
The frontend reads `PUBLIC_CMS_API_URL`; inside Compose it defaults to
|
||||
`http://backend:8000`. Media URLs use `WAGTAILADMIN_BASE_URL` so browsers receive the
|
||||
public `http://localhost:8000` address. Change both values for another environment.
|
||||
public reverse-proxy address. Change both values for another environment.
|
||||
The standard published-page endpoint is also available at `/api/v2/pages/`.
|
||||
|
||||
## Useful commands
|
||||
|
||||
```bash
|
||||
# Follow application logs
|
||||
docker compose logs -f postgres backend frontend
|
||||
docker compose logs -f postgres backend frontend nginx
|
||||
|
||||
# Run database migrations
|
||||
docker compose exec backend python manage.py migrate
|
||||
@@ -91,6 +104,7 @@ docker compose exec backend python manage.py migrate
|
||||
docker compose run --rm backend python manage.py test
|
||||
docker compose run --rm frontend npm run check
|
||||
docker compose run --rm frontend npm run build
|
||||
docker compose run --rm nginx nginx -t
|
||||
docker compose config --quiet
|
||||
|
||||
# Stop the stack without deleting content
|
||||
@@ -109,6 +123,7 @@ local environment; run the seed command again after the next startup.
|
||||
.
|
||||
├── backend/ # Wagtail/Django CMS and API
|
||||
├── frontend/ # Astro public website
|
||||
├── nginx/ # Reverse-proxy virtual host
|
||||
├── docs/ # Architecture, operations, security, and ADRs
|
||||
├── docker-compose.yml
|
||||
├── .env.example
|
||||
@@ -134,5 +149,5 @@ cautions. Do not commit database dumps, `.env`, or uploaded media.
|
||||
## Production TODOs
|
||||
|
||||
This repository deliberately targets a simple, working local deployment. Before a
|
||||
public production launch, add a TLS reverse proxy, production-grade static/media
|
||||
public production launch, add TLS certificates, production-grade static/media
|
||||
serving, off-host backups, monitoring, and a deployment-specific secret manager.
|
||||
|
||||
@@ -24,6 +24,8 @@ ALLOWED_HOSTS = [
|
||||
for host in os.getenv("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
|
||||
if host.strip()
|
||||
]
|
||||
if "azionelab.org" not in ALLOWED_HOSTS:
|
||||
ALLOWED_HOSTS.append("azionelab.org")
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"home",
|
||||
|
||||
+28
-2
@@ -27,8 +27,8 @@ services:
|
||||
DATABASE_URL: ${DATABASE_URL:-postgresql://theatre_lab:replace-with-a-local-password@postgres:5432/theatre_lab}
|
||||
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:-replace-with-a-long-random-development-key}
|
||||
DJANGO_DEBUG: ${DJANGO_DEBUG:-true}
|
||||
DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS:-localhost,127.0.0.1,backend}
|
||||
WAGTAILADMIN_BASE_URL: ${WAGTAILADMIN_BASE_URL:-http://localhost:8000}
|
||||
DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS:-localhost,127.0.0.1,backend,azionelab.org}
|
||||
WAGTAILADMIN_BASE_URL: ${WAGTAILADMIN_BASE_URL:-http://azionelab.org:8080}
|
||||
volumes:
|
||||
- media_data:/app/media
|
||||
ports:
|
||||
@@ -72,6 +72,32 @@ services:
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
nginx:
|
||||
image: nginx:1.30.0-alpine
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
volumes:
|
||||
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
ports:
|
||||
- "${NGINX_BIND_ADDRESS:-127.0.0.1}:${NGINX_HTTP_PORT:-8080}:80"
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
frontend:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"wget -q -O /dev/null --header='Host: azionelab.org' http://127.0.0.1/health/ || exit 1",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
media_data:
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# ADR-0002: NGINX reverse proxy for the public virtual host
|
||||
|
||||
Date: 2026-06-24
|
||||
|
||||
Status: Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The Compose stack exposes Astro and Wagtail on separate local ports. Azione!Lab needs
|
||||
one domain-aware HTTP entry point for `azionelab.org` while keeping application routing
|
||||
and service discovery inside Docker Compose.
|
||||
|
||||
## Decision
|
||||
|
||||
Add a pinned NGINX Alpine container. Its `azionelab.org` virtual host routes Wagtail
|
||||
admin, API, documents, health, static files, and media to Django; all other paths go to
|
||||
Astro. An explicit default virtual host returns 404 for unknown host names.
|
||||
|
||||
For local development NGINX binds to `127.0.0.1:8080`, configurable through
|
||||
`NGINX_BIND_ADDRESS` and `NGINX_HTTP_PORT`. Existing loopback-only application ports
|
||||
remain available for diagnostics. PostgreSQL remains private to the Compose network.
|
||||
TLS termination is outside this local implementation.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The preferred local URL becomes `http://azionelab.org:8080` after a hosts-file entry.
|
||||
- Wagtail receives the original host and standard forwarded client/protocol headers.
|
||||
- Media URLs must use the reverse-proxy address through `WAGTAILADMIN_BASE_URL`.
|
||||
- Production deployment still requires TLS, trusted proxy policy, and removal or
|
||||
firewalling of diagnostic application ports.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- Routing only the frontend was rejected because Wagtail admin, API, and media should
|
||||
share the public domain.
|
||||
- Replacing Astro or Django with static files served directly by NGINX was rejected as
|
||||
unnecessary for the requested local stack.
|
||||
- Adding automatic certificates was deferred because DNS and certificate ownership are
|
||||
not part of this task.
|
||||
|
||||
## Security impact
|
||||
|
||||
Unknown host names are rejected. NGINX adds basic content-type and referrer headers,
|
||||
runs without added capabilities or privileged mode, and is bound to loopback by default.
|
||||
No TLS guarantee is made by this local configuration.
|
||||
|
||||
## Operational impact
|
||||
|
||||
NGINX depends on healthy frontend and backend services and has its own health check.
|
||||
Operators must configure DNS/hosts, the public Wagtail base URL, and the published port.
|
||||
|
||||
## Rollback
|
||||
|
||||
Remove the NGINX service and configuration, restore direct application URLs, and revert
|
||||
the related allowed-host and public-base-URL values. Database and media data are not
|
||||
changed.
|
||||
+10
-5
@@ -1,11 +1,15 @@
|
||||
# Architecture
|
||||
|
||||
The system has three runtime components on the Docker Compose default network:
|
||||
The system has four runtime components on the Docker Compose default network:
|
||||
|
||||
1. Astro renders the public single page and requests one aggregate JSON document.
|
||||
2. Wagtail/Django manages editorial content, serves media in development, and exposes
|
||||
1. NGINX accepts requests for `azionelab.org` and routes them by path.
|
||||
2. Astro renders the public single page and requests one aggregate JSON document.
|
||||
3. Wagtail/Django manages editorial content, serves media in development, and exposes
|
||||
the read-only `/api/site/home/` endpoint.
|
||||
3. PostgreSQL persists Wagtail content and metadata.
|
||||
4. PostgreSQL persists Wagtail content and metadata.
|
||||
|
||||
NGINX sends `/admin`, `/api`, `/documents`, `/health`, `/media`, and `/static` paths to
|
||||
Wagtail. All remaining paths go to Astro. Unknown virtual hosts receive a 404 response.
|
||||
|
||||
The aggregate response contains `settings`, `homepage`, `feature_cards`, `teacher`,
|
||||
`lesson_info`, `shows`, and `gallery_items`. Image values contain browser-facing URLs
|
||||
@@ -18,4 +22,5 @@ has no database access and no public write API. Uploaded media is stored in a de
|
||||
Compose volume.
|
||||
|
||||
The deployment topology and content model are recorded in
|
||||
[ADR-0001](adr/0001-headless-wagtail-astro.md).
|
||||
[ADR-0001](adr/0001-headless-wagtail-astro.md) and
|
||||
[ADR-0002](adr/0002-nginx-reverse-proxy.md).
|
||||
|
||||
+15
-8
@@ -14,22 +14,29 @@ docker compose exec backend python manage.py seed_demo
|
||||
The backend applies migrations and collects static files before starting Gunicorn.
|
||||
All services have health checks; wait for healthy status before opening the site.
|
||||
|
||||
Compose exposes the Astro site on loopback port `4321` and Django/Wagtail on loopback
|
||||
port `8000`. PostgreSQL is available only on the Compose network. `postgres_data` and
|
||||
`media_data` are persistent named volumes.
|
||||
Add `127.0.0.1 azionelab.org` to the local hosts file, then use
|
||||
`http://azionelab.org:8080`. NGINX binds to loopback port `8080` by default and routes
|
||||
the domain to Astro or Wagtail. Their direct loopback ports `4321` and `8000` remain
|
||||
available for diagnostics. PostgreSQL is available only on the Compose network.
|
||||
`postgres_data` and `media_data` are persistent named volumes.
|
||||
|
||||
The stack uses explicit PostgreSQL 16.9, Python 3.12.12, and Node.js 22.20 image
|
||||
versions. Containers are not privileged and use `no-new-privileges`.
|
||||
The stack uses explicit PostgreSQL 16.9, Python 3.12.12, Node.js 22.20, and NGINX 1.30.0
|
||||
image versions. Containers are not privileged and use `no-new-privileges`.
|
||||
|
||||
Required runtime variables are `DATABASE_URL`, `DJANGO_SECRET_KEY`, `DJANGO_DEBUG`,
|
||||
`DJANGO_ALLOWED_HOSTS`, `WAGTAILADMIN_BASE_URL`, and `PUBLIC_CMS_API_URL`. PostgreSQL
|
||||
bootstrap variables are also documented in `.env.example`. Do not use the example
|
||||
credentials outside local development.
|
||||
`DJANGO_ALLOWED_HOSTS`, `WAGTAILADMIN_BASE_URL`, `PUBLIC_CMS_API_URL`,
|
||||
`NGINX_BIND_ADDRESS`, and `NGINX_HTTP_PORT`. PostgreSQL bootstrap variables are also
|
||||
documented in `.env.example`. Do not use the example credentials outside local
|
||||
development.
|
||||
|
||||
`WAGTAILADMIN_BASE_URL` must be browser-reachable because it is used for media URLs.
|
||||
`PUBLIC_CMS_API_URL` must be reachable by Astro; within Compose it is
|
||||
`http://backend:8000`.
|
||||
|
||||
The local virtual host is HTTP-only. Production must publish NGINX through the intended
|
||||
network interface, configure DNS for `azionelab.org`, terminate TLS, and prevent direct
|
||||
external access to application ports.
|
||||
|
||||
## Production boundary
|
||||
|
||||
The Compose stack is a local development deployment. A public environment still needs
|
||||
|
||||
+11
-4
@@ -5,13 +5,19 @@
|
||||
```bash
|
||||
docker compose up --build -d
|
||||
docker compose ps
|
||||
docker compose logs -f backend frontend postgres
|
||||
docker compose logs -f nginx backend frontend postgres
|
||||
docker compose down
|
||||
```
|
||||
|
||||
Compose health checks PostgreSQL and Django. The public health endpoint is
|
||||
`http://localhost:8000/health/`; the aggregate content endpoint is
|
||||
`http://localhost:8000/api/site/home/`.
|
||||
Compose health checks all services. Through the virtual host, the public health endpoint
|
||||
is `http://azionelab.org:8080/health/`; the aggregate content endpoint is
|
||||
`http://azionelab.org:8080/api/site/home/`.
|
||||
|
||||
Test routing without changing the hosts file:
|
||||
|
||||
```bash
|
||||
curl --resolve azionelab.org:8080:127.0.0.1 http://azionelab.org:8080/health/
|
||||
```
|
||||
|
||||
Apply schema changes with `docker compose exec backend python manage.py migrate`.
|
||||
Create editors with `createsuperuser`; use Wagtail permissions for later non-superuser
|
||||
@@ -42,3 +48,4 @@ Keep real backups encrypted and off-host with a defined retention policy.
|
||||
- Frontend fallback content can mask a CMS outage, so monitor backend health directly.
|
||||
- Local named volumes are not off-host backups.
|
||||
- Development media serving is unsuitable for production traffic.
|
||||
- The local reverse proxy provides HTTP only; it does not manage certificates.
|
||||
|
||||
+10
-1
@@ -3,7 +3,7 @@
|
||||
## Site shows fallback content
|
||||
|
||||
1. Check `docker compose ps` and the backend health status.
|
||||
2. Request `http://localhost:8000/api/site/home/` directly.
|
||||
2. Request `http://azionelab.org:8080/api/site/home/` through NGINX.
|
||||
3. Inspect `docker compose logs backend postgres`.
|
||||
4. Verify `PUBLIC_CMS_API_URL` resolves from the frontend container.
|
||||
5. Restart the frontend after recovery so its static page is rebuilt from CMS data.
|
||||
@@ -29,6 +29,15 @@
|
||||
4. For the frontend, request port `4321`, then check the API separately because
|
||||
fallback content can mask a CMS outage.
|
||||
|
||||
## NGINX returns 404 or 502
|
||||
|
||||
1. Confirm the request host is exactly `azionelab.org`; unknown hosts intentionally
|
||||
receive 404.
|
||||
2. Run `docker compose exec nginx nginx -t`.
|
||||
3. Check `docker compose ps` and verify both backend and frontend are healthy.
|
||||
4. Inspect `docker compose logs --tail=200 nginx backend frontend`.
|
||||
5. Verify local DNS or `/etc/hosts` maps `azionelab.org` to the proxy address.
|
||||
|
||||
## Rollback
|
||||
|
||||
Revert the application commit and rebuild containers. Preserve database/media volumes.
|
||||
|
||||
+5
-1
@@ -4,7 +4,8 @@
|
||||
and password hashing. The public site does not add accounts or write endpoints.
|
||||
- The aggregate API is intentionally unauthenticated and returns published editorial
|
||||
content only; secrets and unpublished drafts must never be serialized.
|
||||
- PostgreSQL has no host-published port. Web ports bind to loopback for local use.
|
||||
- PostgreSQL has no host-published port. NGINX and diagnostic application ports bind to
|
||||
loopback for local use. Unknown NGINX virtual hosts are rejected with 404.
|
||||
- Containers are unprivileged at the Compose level: no privileged mode, host network,
|
||||
Docker socket, or added capabilities are used. `no-new-privileges` is enabled and
|
||||
the application images run as non-root users.
|
||||
@@ -18,6 +19,9 @@
|
||||
legitimately exposes only contact details approved for publication.
|
||||
- Dependency and image versions are explicit. Operators remain responsible for patch
|
||||
upgrades, vulnerability scans, and production digest pinning.
|
||||
- NGINX forwards the original host and standard client/protocol headers. The local
|
||||
configuration is HTTP-only; production must add TLS and ensure application ports are
|
||||
not externally reachable.
|
||||
|
||||
Manual production hardening remains required for reverse proxy headers, TLS, media
|
||||
storage, backup retention, monitoring, and admin network policy.
|
||||
|
||||
+6
-3
@@ -8,6 +8,7 @@ All tests should run inside Docker containers.
|
||||
docker compose run --rm backend python manage.py test
|
||||
docker compose run --rm frontend npm run check
|
||||
docker compose run --rm frontend npm run build
|
||||
docker compose run --rm nginx nginx -t
|
||||
docker compose config --quiet
|
||||
```
|
||||
|
||||
@@ -18,11 +19,13 @@ The test suite covers:
|
||||
- Django model and API integration tests;
|
||||
- Astro static type and template validation;
|
||||
- production frontend build;
|
||||
- NGINX syntax and upstream configuration validation;
|
||||
- Docker Compose configuration validation.
|
||||
|
||||
All commands run in containers. The backend test container starts PostgreSQL through
|
||||
Compose; the frontend checks use the checked-in lockfile for reproducible installs.
|
||||
|
||||
After seeding, smoke-test `/health/`, `/api/site/home/`, and the public page. Basic
|
||||
keyboard navigation, responsive layout, and accessible names require a manual browser
|
||||
pass until browser automation is added.
|
||||
After seeding, smoke-test `/health/`, `/api/site/home/`, and the public page through
|
||||
NGINX using the `azionelab.org` host. Also verify that an unknown host returns 404.
|
||||
Basic keyboard navigation, responsive layout, and accessible names require a manual
|
||||
browser pass until browser automation is added.
|
||||
|
||||
@@ -6,4 +6,9 @@ export default defineConfig({
|
||||
host: true,
|
||||
port: 4321,
|
||||
},
|
||||
vite: {
|
||||
server: {
|
||||
allowedHosts: ["azionelab.org"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
"" close;
|
||||
}
|
||||
|
||||
server_tokens off;
|
||||
|
||||
upstream wagtail_backend {
|
||||
server backend:8000;
|
||||
keepalive 16;
|
||||
}
|
||||
|
||||
upstream astro_frontend {
|
||||
server frontend:4321;
|
||||
keepalive 16;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
|
||||
return 404;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name azionelab.org;
|
||||
|
||||
client_max_body_size 10m;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
location ~ ^/(admin|api|documents|health|media|static)(/|$) {
|
||||
proxy_pass http://wagtail_backend;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://astro_frontend;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user