feat: add nginx reverse proxy

This commit is contained in:
bisco
2026-06-24 08:59:35 +02:00
parent 8155d94fee
commit b36cd2a754
14 changed files with 228 additions and 37 deletions
+56
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.