Skip to content

jevellangelo/acme-dns

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

acme-dns Docker Setup

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.

What is acme-dns?

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.

Why use it?

  • 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.).

Prerequisites

  • 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 8181 open if you want to expose the REST API (alternatively, keep it internal behind a reverse proxy)

Project Structure

acme-dns/
├── Dockerfile
├── docker-compose.yml
├── config/
│   └── config.cfg       # acme-dns configuration
└── data/                # SQLite database (auto-created, gitignored)

DNS Setup

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.

Configuration

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 .)

Port note

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.

Deployment

# 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 -f

Verify DNS is working:

dig acmedns.yourdomain.com A
dig acmedns.yourdomain.com NS

Registering a client

Once 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.

Connecting your ACME client

Certbot

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.comfulldomain from the registration response.

Run Certbot:

certbot certonly \
  --authenticator dns-acmedns \
  --dns-acmedns-credentials /etc/letsencrypt/acmedns.json \
  -d yourdomain.com \
  -d "*.yourdomain.com"

Traefik

In your Traefik static config:

certificatesResolvers:
  myresolver:
    acme:
      email: you@yourdomain.com
      storage: /acme.json
      dnsChallenge:
        provider: acme-dns

Set environment variables:

ACME_DNS_API_BASE=http://YOUR_SERVER_PUBLIC_IP:8181
ACME_DNS_STORAGE_PATH=/path/to/acmedns-store.json

acme.sh

export ACMEDNS_BASE_URL="http://YOUR_SERVER_PUBLIC_IP:8181"
acme.sh --issue \
  --dns dns_acmedns \
  -d yourdomain.com \
  -d "*.yourdomain.com"

Firewall rules

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 port

For Hetzner Cloud, also configure the firewall rules in the Hetzner Cloud Console under your server's Firewall section.

Security notes

  • 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 = true in config.cfg after you've registered all your clients.
  • If your acme-dns REST API port is public, consider adding allowfrom IP restrictions per client in the registration response.

License

This project is a Docker wrapper around joohoi/acme-dns, which is MIT licensed.

About

Self-Hosting acme-dns on a VPS with Docker

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors