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
6.5 KiB
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
- You delegate one subdomain (e.g.
acme.example.com) to the machine running this container. - For every domain you want a certificate for, you point its
_acme-challengerecord at that subdomain via CNAME. - 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.comand*.example.comshare 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 sameacme.example.comdelegation. 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