Containarium automatically provisions TLS certificates for domains when adding proxy routes. This ensures all routes have HTTPS enabled without manual certificate management.
When you add a new proxy route (e.g., myapp.example.com), Containarium:
- Adds the reverse proxy route to Caddy
- Adds the domain to Caddy's TLS automation policy
- Caddy automatically obtains a certificate via ACME (Let's Encrypt or ZeroSSL)
When you add a route via the API or Web UI:
# Via CLI (future)
containarium route add myapp.example.com --target 10.0.3.100:8080
# Via REST API
curl -X POST https://<cluster>.example.com/v1/network/routes \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"domain": "myapp.example.com",
"target_ip": "10.0.3.100",
"target_port": 8080
}'Containarium will:
- Add the route to Caddy's HTTP server configuration
- Provision TLS by adding the domain to Caddy's automation policy
Caddy uses an automation policy to manage certificates. When a domain is added, Containarium updates the policy:
{
"subjects": ["myapp.example.com", "other.example.com"],
"issuers": [
{"module": "acme"},
{"module": "acme", "ca": "https://acme.zerossl.com/v2/DV90"}
]
}This configures:
- Let's Encrypt as the primary certificate authority
- ZeroSSL as a fallback CA
- Domain Added: Route is created, domain added to TLS policy
- ACME Challenge: Caddy performs HTTP-01 or TLS-ALPN-01 challenge
- Certificate Issued: Certificate is obtained and stored
- Auto-Renewal: Caddy automatically renews before expiration
The domain must resolve to the Caddy server's IP address:
# Example: point myapp.example.com to your server
myapp.example.com. IN A 35.xxx.xxx.xxxPorts 80 and 443 must be accessible for ACME challenges:
# GCP firewall rule example
gcloud compute firewall-rules create allow-http-https \
--allow tcp:80,tcp:443 \
--target-tags containariumThe Caddy Admin API must be accessible (default: http://localhost:2019):
# Verify Caddy Admin API is running
curl http://localhost:2019/config/The Network tab in the Web UI provides route management with automatic TLS:
- Navigate to Network tab
- Click Add Route
- Select or enter a domain (e.g.,
myapp.example.com) - Select target container and port
- Click Add Route
The certificate is provisioned automatically in the background.
For wildcard certificates (e.g., *.example.com), use DNS-01 challenge:
- Configure Caddy with a DNS provider plugin (Cloudflare, Route53, etc.)
- Set up DNS API credentials
- Configure the Caddyfile with DNS challenge:
*.example.com {
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
@subdomain host *.example.com
handle @subdomain {
reverse_proxy {http.request.host.labels.2}:8080
}
}See CADDY-SETUP.md for detailed DNS provider configuration.
The Caddyfile above is illustrative; the daemon manages its bundled core-caddy
entirely over the admin API (no Caddyfile), so a multi-region control plane
that serves each node at a per-region hostname (region-a.example.com,
region-b.example.com, …) is configured via env + flags:
- Serve the hostnames. The daemon fronts every domain passed via the
repeatable
--public-base-domainflag (in addition to--base-domain); each gets a route to the daemon's REST endpoint (#213). - Enable DNS-01. Set
CONTAINARIUM_ACME_DNS_PROVIDER=cloudflare(plus the provider token, e.g.CF_API_TOKEN). The daemon buildscore-caddywith the matchingcaddy-dnsmodule and configures the ACME/ZeroSSL issuers to solve DNS-01 (#378). - Wildcard is auto-provisioned. With DNS-01 configured, the daemon adds a
single
*.<base-domain>wildcard subject to TLS automation at edge startup (and re-adds it after a Caddy reload). One DNS-01 issuance then covers every current and future per-region subdomain — no per-hostname issuance, no HTTP-01 (#389).
This is the supported path for per-region / wildcard HTTPS.
Without a DNS provider, each additional hostname falls back to per-hostname
HTTP-01. HTTP-01 is fragile for this edge: the ACME validator fetches
http://<host>/.well-known/acme-challenge/<token> on :80 and must not be
redirected to HTTPS (RFC 8555 §8.3), but auto-HTTPS / route ordering on the
shared :80/:443 server can shadow the challenge, and issuance then fails with
tls: internal error. A hardened HTTP-01 carve-out for the multi-hostname edge
is not shipped — it needs a live Let's Encrypt round-trip to verify safely,
and a wrong edge-TLS change risks every hostname. For per-region / multiple
hostnames, use DNS-01 (above). The HTTP-01 carve-out is tracked in #389.
Check Caddy logs for ACME errors:
# If Caddy runs in a container
incus exec containarium-core-caddy -- journalctl -u caddy -f
# Or check Caddy's stderr
incus exec containarium-core-caddy -- tail -f /var/log/caddy/error.logCommon issues:
- DNS not pointing to server
- Ports 80/443 blocked
- Rate limits (Let's Encrypt limits to 50 certs/domain/week)
Verify the TLS automation policy:
curl -s http://localhost:2019/config/apps/tls/automation/policies | jqIf a domain already has a certificate (e.g., wildcard), the provisioning gracefully skips:
Warning: Failed to provision TLS for myapp.example.com: domain already in policy
This is expected behavior - the existing certificate will be used.
The ProxyManager.ProvisionTLS(domain string) method:
// ProvisionTLS provisions a TLS certificate for the given domain
// via Caddy's TLS automation policy with ACME issuers
func (p *ProxyManager) ProvisionTLS(domain string) errorParameters:
domain: Full domain name (e.g.,myapp.example.com)
Returns:
nilon success- Error if TLS provisioning fails (route is still added)
| Endpoint | Method | Purpose |
|---|---|---|
/config/apps/tls/automation/policies |
GET | Get existing TLS policies |
/config/apps/tls/automation/policies |
PATCH | Update policy with new domain |
/config/apps/tls/automation/policies |
POST | Create new policy if none exists |
- Certificate Storage: Certificates are stored in Caddy's data directory
- Private Keys: Never leave the server; managed entirely by Caddy
- ACME Account: Automatically created and managed by Caddy
- Renewal: Automatic, no manual intervention required