tooling/issue-letsencrypt.sh: manual script, issues one cert per name (apex + LE_SUBDOMAINS) with acme.sh + OVH DNS-01 (no inbound ports), installs to certs/<name>/ and reloads nginx. nginx/ulicraft.conf.tmpl: TLS vhosts for apex/pack (static) + auth (proxy to drasl). OVH creds and LE_EMAIL in .env.example; certs/ gitignored. plan/15-letsencrypt.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3.2 KiB
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
- Install acme.sh:
curl https://get.acme.sh | sh -s email=you@example.com - OVH API credentials →
.env(see.env.exampleOVH_*,LE_EMAIL,LE_SUBDOMAINS). Create a token at https://eu.api.ovh.com/createToken/ withGET/POST/PUT/DELETEon/domain/zone/*, or leaveOVH_CKblank and run the script once — acme.sh prints an authorization URL; visit it, then paste the consumer key intoOVH_CKand re-run.
Issue
tooling/issue-letsencrypt.sh # real certs
LE_STAGING=1 tooling/issue-letsencrypt.sh # dry run against the LE staging CA
Certs install to certs/<name>/{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
- Stack up with the nginx override (publishes drasl on
127.0.0.1:25585):docker compose -f docker-compose.yml -f docker-compose.nginx.yml up -d - Render + install the vhost (
nginx/ulicraft.conf.tmpl):nginx then: apex +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 nginxpack.*as static files (www,pack,custom),auth.*→ drasl. Ensurewww-datacan 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).