My new favorite way to solve this is with Caddy (valid certificates for private HTTPS services).
Caddy consolidates the features of NGINX and certbot (or acme.sh) and works great as a container.
I need three files for this: compose.yml and Dockerfile and Caddyfile.
The Dockerfile builds the Caddy image with the DNS plugin for whichever provider I’m using, e.g., CloudFlare, DigitalOcean, AWS, etc.
The Compose file ties it all together, mounting my Caddyfile on the Caddy container, and using the Dockerfile to build the Caddy image.
Here’s an example of each.
# Use the official Caddy image as a parent image
FROM caddy:2-builder AS builder
RUN xcaddy build \
--with github.com/caddy-dns/cloudflare # add more like "--with github.com/org/repo"
# Use the official Caddy image to create the final image
FROM caddy:2
# Copy the custom Caddy build into the final image
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
Caddyfile:
{
email me@example.com # use a real email for EFF/LetsEncrypt
acme_ca https://acme-v02.api.letsencrypt.org/directory
# you can use the staging service to make sure it's working before burning quota
#acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}
# get a wildcard cert and handle any matching HTTPS requests, this is why you use a REAL domain name that you control for Ziti service addresses!
*.ziti.example.com {
tls {
dns cloudflare {env.CF_API_TOKEN}
resolvers 1.1.1.1
}
log {
output stdout
format console
level INFO
#level DEBUG
}
# optionally mount some static files on the container as the doc root
root * /mnt
# Caddy has many options, just one example of forwarding the request to a port on the Docker host. You can also forward requests to separate containers in the same isolated Docker network.
reverse_proxy /* host.docker.internal:8096
}
Finally, the compose.yml:
services:
caddy:
build:
context: .
restart: unless-stopped
environment:
CF_API_TOKEN:
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- /my/web/files/:/mnt/
- caddy_data:/data
- caddy_config:/config
user: "$PUID:$PGID"
extra_hosts:
- "host.docker.internal:host-gateway"
init:
image: busybox
command: >
chown -R "$PUID:$PGID" /data /config;
chmod -R ug=rwX,o-rwx /data /config;
volumes:
- caddy_data:/data
- caddy_config:/config
volumes:
caddy_data:
caddy_config:
This part is then run with:
docker compose up --detach
Now add some Ziti! The Ziti service addresses must match your Caddyfile. Caddy automatically provides the certificate and auto-renewal as long as the DNS provider token is valid.
An example of a matching Ziti service address would be www.ziti.example.com:443. You can have separate Caddyfile blocks for each service to handle each domain name or use a wildcard, depending on the requirements of the destination app you’re providing.