You run ten self-hosted services and every single one needs a valid TLS certificate. Certbot needs port 80 open and stops working when Traefik is already listening there. Wildcard certificates require DNS plugins most clients do not bundle. Renewal scripts break when you migrate containers between hosts.
acme.sh solves all of this with a single shell script that supports over three hundred DNS providers, runs in Docker without runtime dependencies, and handles auto-renewal through a built-in cron mode.
This guide walks through a complete acme.sh deployment on Docker — issuing wildcard Let’s Encrypt certificates via DNS-01 challenge, automating renewal, and deploying certs to Traefik, Nginx, and Caddy on the same Docker network.
Why acme.sh Over Certbot for Homelab Certificates
Certbot is the default ACME client for most administrators, but it has structural disadvantages in a Docker-forward homelab:
- Dependency footprint: Certbot is a Python application with plugins that pull in dozens of packages. acme.sh is a single POSIX shell script with zero runtime dependencies beyond curl and openssl.
- DNS provider support: Certbot’s DNS-01 plugins are per-provider packages you install separately. acme.sh bundles support for over three hundred DNS providers in the script itself — no extra install step.
- Multiple CA endpoints: acme.sh supports Let’s Encrypt, ZeroSSL, Buypass, Google Trust Services, and custom ACME endpoints. Switching CAs is a one-flag change.
- Key types: ECDSA (prime256v1, secp384r1) and RSA (2048, 4096) are first-class options, controlled by a single
--keylengthflag. - Deploy hooks: acme.sh ships with built-in deploy hooks for nginx, apache, haproxy, cpanel, and custom commands. You write one reload command and every renewal triggers it.
If you already run Certbot and it works, keep it. If you are starting fresh or want wildcard certs without exposing port 80, acme.sh is the cleaner path.
Installing acme.sh in Docker
The official image neilpang/acme.sh bundles the script with cron support. Create a compose file that mounts persistent volumes for the acme.sh data directory and the certificate output directory:
|
|
Two volumes handle persistence:
./acme_data— stores account keys, certificate files, and configuration. This is the only volume you need for restart survival../certs— output directory where deploy hooks write certificates for consumption by reverse proxies. Optional if you use deploy hooks that write directly to mounted volumes.
The daemon command keeps the container running and enables the built-in cron for auto-renewal. For one-shot issuance you can also use run --rm mode:
|
|
This registers your account email with the default Let’s Encrypt production endpoint. The --server flag switches to ZeroSSL or staging:
|
|
DNS-01 Challenge for Wildcard Certificates
The HTTP-01 challenge requires port 80 on the host and cannot issue wildcard certificates. The DNS-01 challenge requires no open ports and can issue *.example.com certs that cover every subdomain.
Cloudflare DNS API Integration
Cloudflare is the most common provider in homelabs. Generate an API token from the Cloudflare dashboard with Zone:DNS:Edit permission, then export it to the environment:
|
|
Issue a wildcard certificate covering both *.example.com and the bare domain:
|
|
The ec-256 flag requests an ECDSA P-256 key. ECDSA certificates are smaller, faster to negotiate, and supported by every modern client. For RSA compatibility with legacy clients, omit --keylength or use --keylength 2048.
acme.sh creates a DNS TXT record at _acme-challenge.example.com, polls for propagation, and removes it after validation. The whole process takes about thirty seconds.
Other DNS Providers
The same pattern works for dozens of providers. Set the required environment variables and change the --dns flag:
| Provider | Environment Variables | –dns flag |
|---|---|---|
| Cloudflare | CF_Token, CF_Zone_ID | dns_cf |
| DigitalOcean | DO_API_KEY | dns_dgon |
| AWS Route53 | AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY | dns_aws |
| Hetzner | HETZNER_API_TOKEN | dns_hetzner |
| Gandi | GANDI_LIVEDNS_KEY | dns_gandi_livedns |
| DuckDNS | DuckDNS_Token | dns_duckdns |
Run acme.sh --list-dns inside the container to see every supported provider.
Auto-Renewal Setup
acme.sh includes a cron mode that checks all certificates daily and renews those expiring within sixty days. The daemon start command in the compose file enables this automatically.
To trigger renewal manually or test the cron logic:
|
|
Enable the auto-upgrade feature so acme.sh updates itself:
|
|
This writes the upgrade configuration to ~/.acme.sh/account.conf inside the container. Combined with the daily cron, your certificate pipeline becomes fully self-maintaining.
Systemd Timer Alternative
If you prefer host-level scheduling over Docker cron, disable the daemon mode and run acme.sh via a systemd timer:
|
|
|
|
Enable and start:
|
|
Deploy Hooks — Updating Reverse Proxies
A certificate renewal is useless if running services do not reload with the new key pair. acme.sh deploy hooks automate this.
Traefik Integration
Traefik reads certificates from file. Create a deploy hook that writes the renewal output to the Traefik dynamic configuration volume:
|
|
Configure Traefik to serve the file-based certificates:
|
|
Nginx Integration
For Nginx, the deploy hook writes certificates and reloads the service:
|
|
The Nginx config references the shared cert volume:
|
|
Caddy Integration
Caddy can use certificates from a mounted directory with the file_server or tls directive. The deploy hook writes to the shared path:
|
|
And the Caddyfile references the cert files:
app.example.com {
tls /etc/caddy/certs/fullchain.pem /etc/caddy/certs/key.pem
reverse_proxy app:3000
}
Complete Docker Compose Stack with Traefik
Here is a full working stack that issues certificates and serves applications behind Traefik:
|
|
The certificate issuance flow: acme.sh runs the DNS-01 challenge, stores the certificate in ./certs/traefik/, and Traefik references those files via the shared volume. On renewal, the --reloadcmd signals Traefik to reload configuration without dropping connections.
Verification and Troubleshooting
Check Certificate Details
Use openssl to verify certificate attributes:
|
|
Review acme.sh Logs
acme.sh writes detailed logs to $LE_WORKING_DIR/acme.sh.log:
|
|
Common Issues
-
DNS propagation delay: Some providers take longer than others. acme.sh waits 120 seconds by default. Increase with
--dnssleep 300for slow providers. -
Rate limits: Let’s Encrypt allows 50 certificates per week per registered domain. Use the staging endpoint during testing to avoid exhausting production limits:
1acme.sh --issue --dns dns_cf -d *.example.com --server letsencrypt-staging -
Permission errors on cert files: Ensure the reverse proxy container can read the certificate files. Use
chmod 644on the cert volume or run the proxy container with the same UID that wrote the certificates. -
Cron not running inside container: Verify the container is in daemon mode and cron is enabled by checking the container logs:
1docker logs acme.sh | grep cronIf cron is not active, exec into the container and start the daemon manually:
1docker exec acme.sh /bin/sh -c "crond -b && acme.sh --cron"
Conclusion
acme.sh eliminates every friction point in homelab certificate management. The single-script architecture works anywhere a shell runs. The DNS-01 provider matrix covers practically every DNS service in use today. Deploy hooks bridge the gap between certificate issuance and service reload so that renewal is completely hands-off.
For a homelab running ten, twenty, or fifty services behind a reverse proxy, setting up acme.sh once with wildcard certificates and auto-renewal saves more time than any other TLS management approach. The compose stack in this guide can be running in ten minutes — and then never needs attention again unless you add a new domain.
The full configuration files are available on the blog repository. Drop the compose file on any Docker host, set your Cloudflare API token, and your services will never serve an expired certificate again.