A self-hosted acme-dns server running in Docker, designed for automated DNS-01 ACME certificate challenges without exposing your primary DNS zone to write access.
acme-dns is a limited DNS server with a REST API. Instead of giving your ACME client (e.g., Certbot, Traefik, cert-manager) full write access to your DNS zone, you delegate a single subdomain (e.g., acmedns.yourdomain.com) to this server. The ACME client only writes _acme-challenge TXT records to acme-dns — nothing else is ever touched.
- Wildcard certificates — DNS-01 is the only ACME challenge type that supports wildcard certs (
*.yourdomain.com). - Minimal DNS exposure — Only a narrow subdomain is delegated; your main DNS zone stays read-only.
- Works behind NAT / firewalls — No inbound HTTP required, only port 53.
- Multi-provider compatible — Works with any ACME client that supports
acme-dns(Certbot, Traefik, cert-manager, acme.sh, Caddy, etc.).
- A VPS or server with a public IP address (this setup uses Hetzner, but any cloud provider works)
- Docker and Docker Compose installed
- A domain and access to your DNS provider
- Port
53(TCP + UDP) open on your server firewall - Port
8181open if you want to expose the REST API (alternatively, keep it internal behind a reverse proxy)
acme-dns/
├── Dockerfile
├── docker-compose.yml
├── config/
│ └── config.cfg # acme-dns configuration
└── data/ # SQLite database (auto-created, gitignored)
Before starting the container, configure two DNS records with your provider:
| Type | Name | Value |
|---|---|---|
A |
acmedns.yourdomain.com |
YOUR_SERVER_PUBLIC_IP |
NS |
acmedns.yourdomain.com |
acmedns.yourdomain.com |
The NS record delegates the acmedns subdomain to your acme-dns server, so it becomes authoritative for any *.acmedns.yourdomain.com TXT records.
Cloudflare users: Set the A record proxy status to DNS only (grey cloud). Cloudflare cannot proxy DNS (port 53) traffic.
Edit config/config.cfg and replace the placeholders:
| Placeholder | Replace with |
|---|---|
acmedns.yourdomain.com |
Your chosen subdomain |
YOUR_SERVER_PUBLIC_IP |
Your VPS public IPv4 address |
admin.yourdomain.com |
Your admin email (replace @ with .) |
This setup uses port 8181 for the REST API instead of the default 443, because 443 is already occupied by a Traefik reverse proxy on the same host. If you have a free 443, you can change port = "8181" to port = "443" and update docker-compose.yml accordingly.
# Clone or copy this repo
git clone https://github.com/yourusername/acme-dns-docker.git
cd acme-dns-docker/acme-dns
# Build and start
docker compose up -d --build
# Check logs
docker compose logs -fVerify DNS is working:
dig acmedns.yourdomain.com A
dig acmedns.yourdomain.com NSOnce the server is running, register a client to get credentials:
curl -s -X POST http://YOUR_SERVER_PUBLIC_IP:8181/register | jq .You'll receive a JSON payload like:
{
"username": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"password": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"fulldomain": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.acmedns.yourdomain.com",
"subdomain": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"allowfrom": []
}Save this — you'll need it to configure your ACME client.
Install the certbot-dns-acmedns plugin and create a credentials file:
# /etc/letsencrypt/acmedns.json (chmod 600)
{
"yourdomain.com": {
"username": "...",
"password": "...",
"fulldomain": "...",
"subdomain": "..."
}
}Then add a CNAME in your DNS pointing _acme-challenge.yourdomain.com → fulldomain from the registration response.
Run Certbot:
certbot certonly \
--authenticator dns-acmedns \
--dns-acmedns-credentials /etc/letsencrypt/acmedns.json \
-d yourdomain.com \
-d "*.yourdomain.com"In your Traefik static config:
certificatesResolvers:
myresolver:
acme:
email: you@yourdomain.com
storage: /acme.json
dnsChallenge:
provider: acme-dnsSet environment variables:
ACME_DNS_API_BASE=http://YOUR_SERVER_PUBLIC_IP:8181
ACME_DNS_STORAGE_PATH=/path/to/acmedns-store.json
export ACMEDNS_BASE_URL="http://YOUR_SERVER_PUBLIC_IP:8181"
acme.sh --issue \
--dns dns_acmedns \
-d yourdomain.com \
-d "*.yourdomain.com"Open the required ports on your VPS:
# UFW example
ufw allow 53/tcp
ufw allow 53/udp
ufw allow 8181/tcp # or 443 if you're using that portFor Hetzner Cloud, also configure the firewall rules in the Hetzner Cloud Console under your server's Firewall section.
data/is gitignored — it contains the SQLite database with registered client credentials.- The
config/directory is mounted read-only into the container. - Consider setting
disable_registration = trueinconfig.cfgafter you've registered all your clients. - If your acme-dns REST API port is public, consider adding
allowfromIP restrictions per client in the registration response.
This project is a Docker wrapper around joohoi/acme-dns, which is MIT licensed.