Caddy HTTPS Reverse Proxy (Domain-First)
Use this skill to deploy or repair a production-style HTTPS reverse proxy on
a remote Linux server where backend API is only HTTP (for example
127.0.0.1:39080).
Default policy:
- Prefer user-owned domain first (for example
api.example.com). - Use DuckDNS only as fallback when no usable own-domain is available.
When To Use
Use this skill when the user asks to:
- Add trusted HTTPS in front of an existing HTTP backend.
- Configure Caddy without Docker.
- Use their own domain with Let's Encrypt automatic certificate issuance.
- Fallback to DuckDNS DNS-01 when own-domain path is unavailable.
- Prepare a backend endpoint consumable by browser frontends (for example GitHub Pages).
Required User Inputs
Collect and confirm these values before execution:
ssh_host: target SSH host (IP or domain).ssh_user: remote SSH user.domain: target HTTPS domain (prefer user-owned domain, for exampleapi.ackingliu.top).backend_upstream: backend HTTP upstream (default127.0.0.1:39080).contact_email: ACME email (use a real mailbox when possible).
Optional but useful:
public_ip: expected public IP of target server.ssh_port: default22.use_local_download_upload:truewhen remote network is unstable.disable_http3:trueto force onlyh1/h2.
DuckDNS fallback only:
duckdns_domain: DuckDNS hostname (for examplesfapi.duckdns.org).duckdns_token: DuckDNS token for DNS challenge.
Preconditions
domainresolves to the target public IP.- Inbound
80/tcpand443/tcpare reachable. - Backend service will bind loopback (
127.0.0.1) instead of0.0.0.0. - Remote user has
sudoprivilege.
Security Rules
- Never hardcode token directly in
Caddyfile. - If DuckDNS mode is used, store token in root-only env file:
/etc/caddy/caddy.envwith mode600.
- Do not run Caddy with
--environin systemd ExecStart, to avoid leaking env vars into logs. - Do not expose backend upstream port publicly.
- If switching from DuckDNS to own domain, remove stale DuckDNS env/drop-in.
Deployment Workflow
Step 1: Select deployment mode
Choose one mode before execution:
domain-first(default and preferred):- Use user-owned domain.
- Use standard Caddy + Let's Encrypt (
http-01). - No DNS plugin and no token file needed.
duckdns-fallback:- Use only when user has no usable own domain or explicitly requests it.
- Use Let's Encrypt DNS-01 via
dns.providers.duckdns.
Step 2: Preflight checks
Run on remote:
hostname && whoami && datesudo -n true(or explicit sudo check)dig +short A <domain>sudo ss -lntup | grep -E '(:80|:443|:39080|:22)' || true
If public_ip is provided and DNS does not match it, stop and ask user to fix
DNS first.
Step 3: Install baseline packages
On remote:
sudo apt-get update -ysudo apt-get install -y caddy curl ca-certificates
Step 4: Configure Caddyfile (domain-first preferred)
Create /etc/caddy/Caddyfile:
{
email admin@example.com
servers {
protocols h1 h2
}
}
api.example.com {
@health path /_caddy_health
respond @health "ok" 200
reverse_proxy 127.0.0.1:39080
}
Replace:
- email with
contact_email - site address with
domain - upstream with
backend_upstream - if
disable_http3isfalse, you may removeservers { protocols h1 h2 }
Step 5: DuckDNS fallback path (only when needed)
Requirement: module dns.providers.duckdns must exist.
Preferred download URL:
https://caddyserver.com/api/download?os=linux&arch=amd64&p=github.com/caddy-dns/duckdns
Two supported methods:
- Remote direct download:
- download to
/tmp/caddy-custom
- download to
- Local download + upload fallback (recommended when remote bandwidth is poor):
- download locally
scpupload to/tmp/caddy-customon remote
Then on remote:
chmod +x /tmp/caddy-custom/tmp/caddy-custom list-modules | grep dns.providers.duckdns- backup existing binary:
sudo cp /usr/bin/caddy /usr/bin/caddy.bak.<timestamp>
- replace:
sudo install -m 755 /tmp/caddy-custom /usr/bin/caddy
- verify:
caddy versioncaddy list-modules | grep dns.providers.duckdns
Then configure token and systemd environment: Create env file:
/etc/caddy/caddy.env- content:
DUCKDNS_TOKEN=<duckdns_token>
- permission:
chmod 600 /etc/caddy/caddy.envchown root:root /etc/caddy/caddy.env
Create/overwrite drop-in:
/etc/systemd/system/caddy.service.d/env.conf- content:
[Service]EnvironmentFile=/etc/caddy/caddy.envExecStart=ExecStart=/usr/bin/caddy run --config /etc/caddy/Caddyfile
DuckDNS mode Caddyfile template:
{
email admin@example.com
servers {
protocols h1 h2
}
}
sfapi.duckdns.org {
tls {
dns duckdns {env.DUCKDNS_TOKEN}
resolvers 1.1.1.1 8.8.8.8
}
@health path /_caddy_health
respond @health "ok" 200
reverse_proxy 127.0.0.1:39080
}
Replace:
- email with
contact_email - site address with
domain - upstream with
backend_upstream
Step 6: Validate and restart
On remote:
sudo caddy validate --config /etc/caddy/Caddyfilesudo systemctl daemon-reloadsudo systemctl restart caddysudo systemctl status caddy --no-pager -lsudo journalctl -u caddy -n 120 --no-pager -l
Expected success signal:
- log contains challenge flow matching selected mode:
- domain-first: usually
challenge_type":"http-01"(ortls-alpn-01) - duckdns-fallback:
challenge_type":"dns-01"
- domain-first: usually
- log contains certificate obtained message
- listeners include
:80and:443
Step 7: End-to-end verification
Verify from remote and local client:
curl -I http://<domain>/_caddy_health->308redirect to HTTPScurl -I https://<domain>/_caddy_health->200- certificate check:
echo | openssl s_client -connect <domain>:443 -servername <domain> 2>/dev/null | openssl x509 -noout -issuer -subject -dates
If backend is not started yet, /_caddy_health must still return 200 by Caddy.
Step 8: Cleanup stale fallback config (when domain-first is selected)
If machine previously used DuckDNS mode:
- remove
/etc/systemd/system/caddy.service.d/env.confif present. - remove
/etc/caddy/caddy.envif present. sudo systemctl daemon-reloadsudo systemctl restart caddy
Firewall and Cloud-Network Notes
If certificate issuance fails:
- In domain-first mode, confirm inbound
80/443are reachable from internet. - Check cloud security group allows inbound 80/443 from
0.0.0.0/0. - Check host firewall order so explicit allow for 80/443 is effective.
- Recheck domain A/AAAA record accuracy.
- If network path for HTTP challenge is unstable, switch to DuckDNS DNS-01.
Common Failure Patterns
- Browser still warns:
- stale cert; verify with
openssl s_clientand clear CDN/proxy assumptions.
- stale cert; verify with
502from Caddy:- backend not running on configured
backend_upstream.
- backend not running on configured
dns.providers.duckdnsmissing in DuckDNS mode:- wrong Caddy binary, reinstall plugin-enabled binary.
- DNS challenge fails in DuckDNS mode:
- invalid token, wrong domain, or DNS propagation delay.
- TLS internal errors from some clients:
- verify client-side proxy/VPN interference and compare direct network path.
Rollback
If deployment must be reverted:
- restore old binary:
sudo cp /usr/bin/caddy.bak.<timestamp> /usr/bin/caddy
- restore prior
/etc/caddy/Caddyfilefrom backup. - remove drop-in/env only if needed:
/etc/systemd/system/caddy.service.d/env.conf/etc/caddy/caddy.env
sudo systemctl daemon-reload && sudo systemctl restart caddy
Output Contract
When finishing this skill, report:
- selected mode:
domain-firstorduckdns-fallback. - final public HTTPS URL.
- cert issuer + validity dates.
- upstream target (
backend_upstream) configured. - Caddy version and whether DuckDNS module is required/installed.
- files created/changed:
/etc/caddy/Caddyfile/etc/caddy/caddy.env(DuckDNS mode only)/etc/systemd/system/caddy.service.d/env.conf(DuckDNS mode only)/usr/bin/caddy(plus backup path)
