generated from bisco/codex-bootstrap
feat: add nginx reverse proxy
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user