christianandClaude Fable 5.1 28864708c8
Build Container / docker (push) Successful in 20s
docs: restructure DNS setup section in README
Split DNS setup into one-time delegation (NS + A) and per-name CNAME
records, with a table showing which _acme-challenge record each -d
entry needs. Nested wildcards like *.ai.example.com need their own
record, which was not obvious before. Add a note on wildcard TXT
records shadowing missing challenge names, plus dig commands to verify
before running certbot. Drop the obsolete plugin parameter table and
redundant run examples.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lyu3TWGffosqSY6kDpXCd1
2026-09-11 13:30:03 +02:00
2026-01-03 20:00:52 +01:00
2026-01-03 20:00:52 +01:00
2026-01-03 20:00:52 +01:00
2026-01-03 20:00:52 +01:00
2026-01-03 20:00:52 +01:00

DNS Wildcard Certificate Generator

Docker container that obtains wildcard certificates from Let's Encrypt. It uses the certbot-dns-standalone plugin, which runs its own tiny DNS server to answer the ACME DNS-01 challenge. No API credentials for your DNS provider are needed.

How It Works

  1. You delegate one subdomain (e.g. acme.example.com) to the machine running this container.
  2. For every domain you want a certificate for, you point its _acme-challenge record at that subdomain via CNAME.
  3. When certbot runs, Let's Encrypt looks up _acme-challenge.<domain>, follows the CNAME, and lands on the container's DNS server, which answers with the challenge token.

The container only needs to be running (with port 53 reachable) while a certificate is being requested or renewed.

DNS Setup

Step 1: Delegate a subdomain to the container (once per zone)

Pick a subdomain, e.g. acme.example.com. Add these two records to the example.com zone:

Name Type Value
ns.acme.example.com A 1.2.3.4
acme.example.com NS ns.acme.example.com.

1.2.3.4 is the public IP of the machine running this container. Port 53 (TCP and UDP) must be reachable from the internet.

Step 2: Add a CNAME for every name you request (once per name)

Let's Encrypt validates each name in the certificate separately. For each name it queries _acme-challenge.<name>. So you need one CNAME per distinct name, all pointing at the subdomain from Step 1.

Rule of thumb: take every -d entry, drop the leading *. if present, and prefix the result with _acme-challenge.. Duplicates collapse into one record.

You request (-d) Record needed
example.com _acme-challenge.example.com CNAME acme.example.com.
*.example.com same record as above (already covered)
*.ai.example.com _acme-challenge.ai.example.com CNAME acme.example.com.
*.dev.ai.example.com _acme-challenge.dev.ai.example.com CNAME acme.example.com.
other-domain.org _acme-challenge.other-domain.org CNAME acme.example.com.

Notes:

  • example.com and *.example.com share one record. A wildcard one level deeper (*.ai.example.com) is a different name and needs its own record.
  • Other domains you own (other-domain.org) can reuse the same acme.example.com delegation. Only Step 2 is needed for them.
  • The CNAME target can be anything under the delegated subdomain. The container answers every TXT query it receives, regardless of name. acme.example.com. is the simplest choice.

Watch out: wildcard TXT records

If your zone has a wildcard record like *.example.com TXT "v=spf1 ...", then _acme-challenge.ai.example.com silently resolves to that SPF string instead of failing. Let's Encrypt then reports an incorrect TXT record. An explicit CNAME for _acme-challenge.ai.example.com overrides the wildcard, so adding the record from Step 2 fixes this.

Verify before running certbot

Ask the authoritative name server directly (no cache):

dig +norecurse @<your-dns-provider-ns> _acme-challenge.ai.example.com TXT

You want to see a CNAME acme.example.com. line in the answer. If you see an SPF string or nothing, the record is missing or the wildcard is winning.

Then check the delegation works end to end while the container is running:

dig @1.1.1.1 _acme-challenge.example.com TXT

Usage

Quick Start

docker run -it --rm \
  -v "/etc/letsencrypt:/etc/letsencrypt" \
  -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
  -p 53:53/tcp -p 53:53/udp \
  -e EMAIL="youremail@example.com" \
  -e DOMAINS="-d example.com -d *.example.com -d *.ai.example.com" \
  -e STAGING="true" \
  dns-wildcard-cert

Run with STAGING="true" first. Once it succeeds, run again without it to get a real certificate. Let's Encrypt rate-limits failed attempts against production.

Build Locally

docker build -t dns-wildcard-cert .

Environment Variables

Variable Required Default Description
EMAIL Yes - Email for Let's Encrypt registration
DOMAINS Yes - Domain flags, e.g. -d example.com -d *.example.com
DNS_ADDRESS No 0.0.0.0 IPv4 address to bind the DNS server to
DNS_IPV6_ADDRESS No :: IPv6 address to bind the DNS server to
DNS_PORT No 53 Port for the DNS server (needs forwarding if not 53)
STAGING No false Use the Let's Encrypt staging server (for testing)
DRY_RUN No false Perform a dry run without saving certificates

Non-standard port

If port 53 is taken on the host, run the container on another port and have your existing DNS server forward _acme-challenge queries there:

docker run -it --rm \
  -v "/etc/letsencrypt:/etc/letsencrypt" \
  -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
  -p 5555:5555/tcp -p 5555:5555/udp \
  -e EMAIL="youremail@example.com" \
  -e DOMAINS="-d example.com -d *.example.com" \
  -e DNS_PORT="5555" \
  dns-wildcard-cert

Certificate Renewal

Run the same container again periodically, or use certbot's renew command:

docker run -it --rm \
  -v "/etc/letsencrypt:/etc/letsencrypt" \
  -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
  -p 53:53/tcp -p 53:53/udp \
  --entrypoint certbot \
  dns-wildcard-cert renew

Certificate Location

Certificates are stored in the /etc/letsencrypt volume:

  • Certificate: /etc/letsencrypt/live/<domain>/fullchain.pem
  • Private key: /etc/letsencrypt/live/<domain>/privkey.pem

Docker Compose

version: '3.8'

services:
  certbot:
    build: .
    ports:
      - "53:53/tcp"
      - "53:53/udp"
    environment:
      - EMAIL=youremail@example.com
      - DOMAINS=-d example.com -d *.example.com
      - STAGING=false
    volumes:
      - letsencrypt:/etc/letsencrypt
      - letsencrypt-lib:/var/lib/letsencrypt

volumes:
  letsencrypt:
  letsencrypt-lib:

License

MIT

S
Description
No description provided
Readme
36 KiB
Languages
Shell 63.4%
Dockerfile 36.6%