# TLS — Let's Encrypt (OVH DNS-01) + host nginx ingress Production serves real, publicly-trusted certs via the **host's nginx**. Caddy is no longer the production ingress — it moved to its own compose file for the LAN/air-gap path. ## Two ingress paths | Path | Ingress | Compose | TLS | |------|---------|---------|-----| | Production | host **nginx** | `docker-compose.yml` + `docker-compose.nginx.yml` | Let's Encrypt (this doc) | | LAN / air-gap | **caddy** container | `docker-compose.yml` + `docker-compose.caddy.yml` (+ static/mirror) | Caddy internal CA | `build-stack.sh` drives the caddy path (adds `docker-compose.caddy.yml` automatically). The nginx path is a manual deploy (below). This supersedes the ingress part of `plan/14-deploy.md` for production. ## Certs: one per name, via OVH DNS-01 `tooling/issue-letsencrypt.sh` issues a **separate** cert for the apex and each subdomain (`${BASE_DOMAIN}`, `auth.…`, `pack.…`) using acme.sh + the OVH DNS-01 challenge — no inbound ports needed, so it works even on a LAN-only host. ### One-time setup 1. Install acme.sh: ```bash curl https://get.acme.sh | sh -s email=you@example.com ``` 2. OVH API credentials → `.env` (see `.env.example` `OVH_*`, `LE_EMAIL`, `LE_SUBDOMAINS`). Create a token at with `GET/POST/PUT/DELETE` on `/domain/zone/*`, or leave `OVH_CK` blank and run the script once — acme.sh prints an authorization URL; visit it, then paste the consumer key into `OVH_CK` and re-run. ### Issue ```bash tooling/issue-letsencrypt.sh # real certs LE_STAGING=1 tooling/issue-letsencrypt.sh # dry run against the LE staging CA ``` Certs install to `certs//{cert.pem,key.pem}` (gitignored). acme.sh adds a cron for renewal; `--reloadcmd` reloads nginx automatically on renew. Override the reload with `RELOAD_CMD=...` if nginx isn't systemd-managed. ## nginx 1. Stack up with the nginx override (publishes drasl on `127.0.0.1:25585`): ```bash docker compose -f docker-compose.yml -f docker-compose.nginx.yml up -d ``` 2. Render + install the vhost (`nginx/ulicraft.conf.tmpl`): ```bash APP_DIR=/home/ubuntu/mc/ulicraft-server-v1 BASE_DOMAIN=ulicraft.net \ envsubst '$APP_DIR $BASE_DOMAIN' \ < nginx/ulicraft.conf.tmpl | sudo tee /etc/nginx/sites-available/ulicraft.conf sudo ln -sf /etc/nginx/sites-available/ulicraft.conf /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx ``` nginx then: apex + `pack.*` as static files (`www`, `pack`, `custom`), `auth.*` → drasl. Ensure `www-data` can read `$APP_DIR/{www,pack,custom,certs}`. ## DNS Point the public/party DNS for `${BASE_DOMAIN}`, `auth.${BASE_DOMAIN}`, `pack.${BASE_DOMAIN}` at the host. The Minecraft container reaches them via `extra_hosts: host-gateway` (set in `docker-compose.nginx.yml`) and uses HTTPS — the JVM trusts Let's Encrypt with no CA import (the big win over the internal CA). ## Why DNS-01 (not HTTP-01) `HOST_LAN_IP` shows a LAN host; HTTP-01/TLS-ALPN-01 need public inbound on 80/443. DNS-01 only needs the OVH API, so it issues regardless of public reachability — and `ulicraft.net` is a real OVH-managed domain, which LE requires (it never issues for `.lan`/`.local`).