# Deployment ## Local environment Copy `.env.example` to `.env`, replace the development placeholders, then run: ```bash cp .env.example .env docker compose up --build -d docker compose ps 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. 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. Local HTTPS is disabled by default; `letsencrypt_data` and `certbot_challenges` remain empty unless used. 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`, `PUBLIC_CMS_API_URL`, `NGINX_BIND_ADDRESS`, `NGINX_HTTP_PORT`, and `NGINX_HTTPS_PORT`. The optional certificate variables and PostgreSQL bootstrap variables are 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 PostgreSQL Compose service is `db`, so new `DATABASE_URL` values use `db:5432`. The internal `postgres` alias is retained only for compatibility with existing local `.env` files and should not be used in new configuration. ## TLS deployment modes When an external load balancer terminates TLS, leave `LETSENCRYPT_ENABLED=0`. The `certbot` service then has zero replicas and NGINX serves HTTP to the trusted internal network. Configure the load balancer to set `X-Forwarded-Proto: https`, keep application ports private, and restrict proxy access to the load balancer network. For direct exposure, the HTTP-01 challenge requires public DNS for `LETSENCRYPT_DOMAIN` to resolve to this host and inbound TCP ports 80 and 443 to reach NGINX. Use a real operator address and production-safe Django settings: ```dotenv LETSENCRYPT_ENABLED=1 LETSENCRYPT_DOMAIN=azionelab.org LETSENCRYPT_EMAIL=operator@example.org LETSENCRYPT_STAGING=1 NGINX_BIND_ADDRESS=0.0.0.0 NGINX_HTTP_PORT=80 NGINX_HTTPS_PORT=443 WAGTAILADMIN_BASE_URL=https://azionelab.org DJANGO_DEBUG=false DJANGO_ALLOWED_HOSTS=azionelab.org ``` Then run `docker compose up --build -d` and inspect `docker compose logs certbot proxy`. NGINX serves HTTP until a certificate exists, then reloads and redirects normal HTTP requests to HTTPS. The ACME path remains available over HTTP for renewal. Use the Let's Encrypt staging CA first. Before switching to the production CA, stop the stack and remove only the staging certificate volume after checking its exact Compose project name: ```bash docker compose down docker volume ls --filter label=com.docker.compose.volume=letsencrypt_data docker volume rm PROJECT_letsencrypt_data ``` Set `LETSENCRYPT_STAGING=0`, restart, and verify the certificate issuer in a browser or TLS inspection tool. Never use `docker compose down --volumes` on an environment whose database, media, or certificates must be retained. ## Production boundary The Compose stack remains a minimal deployment base. A public environment still needs production static/media serving, restricted admin access, managed secrets, off-host backups, monitoring, firewall rules, and an explicit domain/allowed-host policy. ## Rollback Set `LETSENCRYPT_ENABLED=0` to disable the certificate service without deleting certificates, or terminate TLS at the load balancer. Revert the application commit and rebuild images for a full rollback. Keep database, media, and certificate volumes unless deletion is intentional. Schema rollback must be evaluated per Django migration; take coordinated database and media backups first.