# nginx-proxy The single web ingress for cloud + edge. Reverse-proxies HTTPS UIs and stream-proxies MQTT-TLS to RabbitMQ. TLS certs managed by a certbot sidecar (Let's Encrypt). - **Image**: stock `nginx:1.27-alpine` (we don't use `nginxproxy/nginx-proxy` because we need the `stream {}` context for MQTT-TLS) - **Sidecar**: `certbot/certbot:latest` — renews every 12h via HTTP-01 webroot challenges - **Networks**: `edge` (the only port-publisher) + `app` (talks to upstream services) - **Host ports**: `tcp/80`, `tcp/443`, `tcp/8883` ## Cert strategy **Interim (Versio DNS)**: HTTP-01 SAN cert covering all subdomains, issued via `--webroot`. Requires: - Public DNS A records for each subdomain pointing at the cloud host - `tcp/80` reachable from the internet **After TransIP migration**: switch to DNS-01 wildcard (`*.wbd-rd.nl`). Swap the `certbot/certbot` image for a build that includes `certbot-dns-transip` and reissue with `--cert-name infra` so the cert path stays stable — **no vhost config changes needed**. ## Config layout ``` config/ ├── nginx.conf # base — http + stream contexts ├── conf.d/ │ ├── 00-default.conf # port 80: ACME challenge + HTTPS redirect │ ├── grafana.conf # grafana.wbd-rd.nl │ ├── gitea.conf # gitea.wbd-rd.nl │ ├── keycloak.conf # keycloak.wbd-rd.nl │ ├── nodered.conf # nodered.wbd-rd.nl │ ├── mlflow.conf # mlflow.wbd-rd.nl │ ├── jupyter.conf # jupyter.wbd-rd.nl │ ├── portainer.conf # portainer.wbd-rd.nl (HTTPS upstream) │ ├── rabbitmq.conf # rabbitmq.wbd-rd.nl (mgmt UI) │ └── jenkins.conf # jenkins.wbd-rd.nl └── stream.d/ └── mqtt.conf # mqtt.wbd-rd.nl:8883 → rabbitmq:1883 ``` Volumes: - `nginx-certs` — Let's Encrypt cert chains at `/etc/letsencrypt/`; read-only into nginx, writable from certbot - `nginx-acme-challenge` — webroot for HTTP-01 challenges at `/var/www/certbot/` All vhosts reference `/etc/letsencrypt/live/infra/fullchain.pem` and `privkey.pem` — a stable path independent of the issuance method. ## First-run bootstrap The HTTPS server blocks won't load without a cert at `/etc/letsencrypt/live/infra/`. Bootstrap procedure (one-time): ```bash cd stacks/nginx-proxy # 1. Self-signed fallback so nginx starts and serves /.well-known/acme-challenge/ docker compose run --rm --entrypoint=/bin/sh nginx -c \ "mkdir -p /etc/letsencrypt/live/infra && \ openssl req -x509 -nodes -days 1 -newkey rsa:2048 \ -keyout /etc/letsencrypt/live/infra/privkey.pem \ -out /etc/letsencrypt/live/infra/fullchain.pem \ -subj '/CN=bootstrap-infra'" # 2. Start nginx (HTTPS blocks load with the dummy cert) docker compose up -d nginx # 3. Issue the real cert via HTTP-01 docker compose run --rm certbot certonly \ --webroot -w /var/www/certbot \ --email "$LETSENCRYPT_EMAIL" --agree-tos --no-eff-email \ --cert-name infra \ -d git.wbd-rd.nl -d auth.wbd-rd.nl -d dash.wbd-rd.nl \ -d flow.wbd-rd.nl -d ml.wbd-rd.nl -d hub.wbd-rd.nl \ -d ops.wbd-rd.nl -d mq.wbd-rd.nl -d ci.wbd-rd.nl \ -d mqtt.wbd-rd.nl # 4. Reload nginx to pick up the real cert docker compose exec nginx nginx -s reload ``` The certbot sidecar then renews every 12h automatically. ## DNS prereqs (HTTP-01) Before bootstrap, ensure A records exist in Versio for the 10 new short subdomains (the canonical tool-named ones — `gitea.wbd-rd.nl`, `grafana.wbd-rd.nl`, etc. — stay pointed at the existing Versio stack during the transition): ``` git.wbd-rd.nl A # gitea (new) auth.wbd-rd.nl A # keycloak dash.wbd-rd.nl A # grafana (new) flow.wbd-rd.nl A # node-red (new) ml.wbd-rd.nl A # mlflow hub.wbd-rd.nl A # jupyterhub ops.wbd-rd.nl A # portainer mq.wbd-rd.nl A # rabbitmq mgmt UI ci.wbd-rd.nl A # jenkins mqtt.wbd-rd.nl A # MQTT-TLS broker ``` ## TODO - Wildcard cert via `certbot-dns-transip` (post TransIP migration) - OIDC `auth_request` to Keycloak in front of services without native OIDC (mlflow, portainer-CE) - Edge-side variant: bind to plant-LAN IP, internal CA for `*.local` hostnames - HSTS + security headers (`add_header Strict-Transport-Security ...`)