Replace Cloudflare DNS challenge with standard Let's Encrypt HTTP-01

Caddy's built-in ACME support handles TLS automatically — no CF_API_TOKEN,
no Cloudflare account, no DNS plugin needed. Requires ports 80+443 forwarded
to the Pi and ACME_EMAIL set in infra/.env.
This commit is contained in:
Claude 2026-03-20 11:41:40 +00:00
parent 3794f4cf36
commit dc59293c5e
No known key found for this signature in database
4 changed files with 46 additions and 83 deletions

View file

@ -140,29 +140,24 @@ docker run --rm hello-world # verify
---
## 6. Domain and DNS (Cloudflare)
## 6. Domain and DNS
You need a domain whose DNS is managed by Cloudflare (the free plan is fine).
You need a domain pointing to your Pi's public IP. Any DNS provider works.
1. In Cloudflare, add two DNS records pointing to your **public home IP**:
1. Add DNS A records pointing to your **public home IP**:
| Type | Name | Content | Proxy |
|---|---|---|---|
| A | `hiy` | `<your-public-IP>` | Proxied (orange cloud) |
| A | `*` | `<your-public-IP>` | Proxied (orange cloud) |
| Type | Name | Content |
|---|---|---|
| A | `hiy` | `<your-public-IP>` |
| A | `*` | `<your-public-IP>` |
The wildcard `*` record routes `<appname>.yourdomain.com` to the Pi.
2. In **Cloudflare → SSL/TLS → Overview**, set mode to **Full (strict)**.
3. Create a **Cloudflare API token** for Caddy's ACME DNS challenge:
- Go to My Profile → API Tokens → Create Token
- Use the **Edit zone DNS** template, scope it to your zone
- Copy the token — you will need it in step 8
### Port forwarding
Forward the following ports on your router to the Pi's static IP:
Forward the following ports on your router to the Pi's static IP.
**Both ports are required** — Caddy uses port 80 to prove domain ownership
to Let's Encrypt before issuing the HTTPS certificate.
| External | Internal |
|---|---|
@ -195,63 +190,20 @@ Minimum required variables:
# Your domain (apps will be at <name>.yourdomain.com)
DOMAIN_SUFFIX=yourdomain.com
# Cloudflare API token for ACME DNS-01 challenge
CF_API_TOKEN=your_cloudflare_api_token
# Email for Let's Encrypt expiry notices
ACME_EMAIL=you@example.com
```
---
## 9. Caddy — automatic HTTPS
Caddy handles TLS via Cloudflare's DNS challenge (no port-80 HTTP challenge
needed, works even behind CGNAT).
Caddy obtains a Let's Encrypt certificate automatically via the HTTP-01
challenge. No DNS API token or Cloudflare account required.
Edit `proxy/caddy.json` and replace the top-level config with:
```json
{
"admin": { "listen": "0.0.0.0:2019" },
"apps": {
"tls": {
"automation": {
"policies": [{
"subjects": ["*.yourdomain.com", "yourdomain.com"],
"issuers": [{
"module": "acme",
"challenges": {
"dns": {
"provider": {
"name": "cloudflare",
"api_token": "{env.CF_API_TOKEN}"
}
}
}
}]
}]
}
},
"http": {
"servers": {
"hiy": {
"listen": [":80", ":443"],
"routes": [
{
"handle": [{
"handler": "reverse_proxy",
"upstreams": [{"dial": "server:3000"}]
}]
}
]
}
}
}
}
}
```
> **Note:** The Caddy image in `docker-compose.yml` needs the Cloudflare DNS
> plugin. Use `caddy:2-alpine` with `xcaddy` or replace the image with
> `ghcr.io/caddybuilds/caddy-cloudflare:latest`.
The `proxy/Caddyfile` is already configured — nothing to edit here.
Just make sure `DOMAIN_SUFFIX` and `ACME_EMAIL` are set in `infra/.env`
and that ports 80 and 443 are forwarded to the Pi (see step 6).
---
@ -427,7 +379,7 @@ Caddy never restarts.
| Symptom | Likely cause | Fix |
|---|---|---|
| Dashboard not reachable | Compose not started / port 443 not forwarded | `docker compose logs server` |
| TLS certificate error | CF_API_TOKEN wrong or DNS not propagated | Check Caddy logs: `docker compose logs caddy` |
| TLS certificate error | Port 80/443 not forwarded or DNS not propagated yet | Check Caddy logs: `docker compose logs caddy` |
| Build fails — "docker not found" | Docker socket not mounted | Verify `docker-proxy` service is up |
| Build fails — "pack not found" | `pack` not installed | See step 11 |
| Webhook returns 401 | Secret mismatch | Regenerate app, re-copy secret to GitHub |

5
infra/.env.example Normal file
View file

@ -0,0 +1,5 @@
# Your domain — apps will be served at <name>.yourdomain.com
DOMAIN_SUFFIX=yourdomain.com
# Email address for Let's Encrypt expiry notices (required for HTTPS)
ACME_EMAIL=you@example.com

View file

@ -53,11 +53,14 @@ services:
- "80:80"
- "443:443"
- "2019:2019" # admin API
environment:
DOMAIN_SUFFIX: ${DOMAIN_SUFFIX:-localhost}
ACME_EMAIL: ${ACME_EMAIL:-}
volumes:
- ../proxy/caddy.json:/etc/caddy/caddy.json:ro
- ../proxy/Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
command: caddy run --config /etc/caddy/caddy.json
command: caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
networks:
- hiy-net
- default

View file

@ -1,29 +1,32 @@
# HIY — Caddyfile
#
# Local development: auto-HTTPS is disabled; everything runs on :80.
# Production (Pi): remove the `auto_https off` line, set your real email,
# and Caddy will obtain Let's Encrypt certificates automatically.
# Caddy automatically obtains a Let's Encrypt certificate for every domain it
# serves (HTTP-01 challenge). No Cloudflare or DNS API token required.
#
# Wildcard TLS on the Pi (recommended):
# tls your@email.com {
# dns cloudflare {env.CLOUDFLARE_API_TOKEN}
# }
# Requirements:
# - Ports 80 and 443 must be publicly reachable (router port-forward to Pi)
# - DNS A record for {$DOMAIN_SUFFIX} must point to your public IP
# - Set ACME_EMAIL in infra/.env (Let's Encrypt needs a contact address)
#
# Local dev: set DOMAIN_SUFFIX=localhost in infra/.env — Caddy will use a
# self-signed cert automatically for localhost.
{
# Admin API — used by build.sh to update routes dynamically.
# Admin API — used by hiy-server to add/remove app routes dynamically.
admin 0.0.0.0:2019
# Comment this out on the Pi with a real domain.
auto_https off
# Contact email for Let's Encrypt expiry notices.
email {$ACME_EMAIL}
}
# Fallback: serves the HIY dashboard itself.
:80 {
# HIY dashboard — served at your root domain.
{$DOMAIN_SUFFIX} {
reverse_proxy server:3000
}
# Example of what the build script adds via the Caddy API:
# Deployed apps are added here dynamically by hiy-server via the Caddy API.
# Each entry looks like:
#
# myapp.yourdomain.com {
# reverse_proxy <container-ip>:3000
# myapp.{$DOMAIN_SUFFIX} {
# reverse_proxy <container-ip>:<port>
# }