# nginx-proxy The single web ingress for cloud + edge. Reverse-proxies HTTPS UIs and stream-proxies MQTT-TLS to RabbitMQ. TLS certificates managed by a certbot sidecar (Let's Encrypt, HTTP-01 webroot challenge). - **Image**: stock `nginx:1.27-alpine` (we don't use `nginxproxy/nginx-proxy` because we need the `stream {}` context for MQTT-TLS, which that image doesn't expose cleanly) - **Sidecar**: `certbot/certbot:latest` — renews every 12h, shared `nginx-certs` + `nginx-acme-challenge` volumes - **Networks**: `edge` (the only port-publisher) + `app` (talks to upstream services) - **Host ports**: `tcp/80`, `tcp/443`, `tcp/8883` ## Config layout ``` config/ ├── nginx.conf # base config — must include `stream {}` directive ├── conf.d/ # HTTP vhosts (one per upstream UI) │ ├── grafana.conf │ ├── node-red.conf │ ├── gitea.conf │ └── ... └── stream.d/ └── mqtt.conf # MQTT-TLS stream block, SNI route to rabbitmq:1883 ``` Volumes: - `nginx-certs` — Let's Encrypt cert chains (`/etc/letsencrypt`), read-only mounted into nginx, writable from certbot - `nginx-acme-challenge` — webroot for HTTP-01 challenges (`/var/www/certbot`) ## Initial cert issuance 1. Start with HTTP-only nginx config (serving `/.well-known/acme-challenge/`). 2. Issue: ```bash docker compose run --rm certbot certonly \ --webroot -w /var/www/certbot \ --email "$LETSENCRYPT_EMAIL" --agree-tos --no-eff-email \ -d gitea.example.com -d grafana.example.com -d nodered.example.com ``` 3. Drop HTTPS vhost configs into `config/conf.d/` and reload nginx. The sidecar then renews automatically. ## TODO - Write base `config/nginx.conf` (`http` + `stream` contexts) - Per-upstream vhost templates with OIDC `auth_request` to Keycloak - Decide internal PKI vs Let's Encrypt for cloud-internal hostnames not reachable from the public internet - Edge-side variant: bind to plant-LAN IP only, internal CA for plant.local hostnames