docs: restructure DNS setup section in README
Build Container / docker (push) Successful in 20s

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
This commit is contained in:
2026-09-11 13:30:03 +02:00
co-authored by Claude Fable 5.1
parent e110b47acb
commit 28864708c8
+80 -72
View File
@@ -1,36 +1,83 @@
# DNS Wildcard Certificate Generator
A Docker container for easily obtaining wildcard SSL certificates from Let's Encrypt using the `certbot-dns-standalone` plugin.
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
This uses the `dns-standalone` authenticator which runs its own DNS server to respond to ACME DNS-01 challenges. You need to configure your DNS to delegate `_acme-challenge` queries to this container.
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.
## Prerequisites
1. A server with port 53 (DNS) available
2. DNS configuration to route challenge queries to your server (see DNS Setup below)
The container only needs to be running (with port 53 reachable) while a certificate is
being requested or renewed.
## DNS Setup
### Option 1: Direct NS Record
### Step 1: Delegate a subdomain to the container (once per zone)
Point `_acme-challenge` records to your certbot server using CNAME and NS records:
Pick a subdomain, e.g. `acme.example.com`. Add these two records to the `example.com` zone:
```dns
; For acme.example.com as your certbot endpoint
acme IN NS ns.acme.example.com.
ns.acme IN A 1.2.3.4
| Name | Type | Value |
|------------------------|------|-------------------------|
| `ns.acme.example.com` | A | `1.2.3.4` |
| `acme.example.com` | NS | `ns.acme.example.com.` |
; For each domain you want certificates for
_acme-challenge.example.com IN CNAME 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):
```bash
dig +norecurse @<your-dns-provider-ns> _acme-challenge.ai.example.com TXT
```
Where `1.2.3.4` is the IP of the server running this container.
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.
### Option 2: DNS Proxy/Forwarding
Then check the delegation works end to end while the container is running:
If you already run a DNS server, configure it to forward `_acme-challenge` queries to the container.
```bash
dig @1.1.1.1 _acme-challenge.example.com TXT
```
## Usage
@@ -42,10 +89,14 @@ docker run -it --rm \
-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" \
-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
```bash
@@ -55,52 +106,20 @@ 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 DNS server |
| `DNS_IPV6_ADDRESS` | No | `::` | IPv6 address to bind DNS server |
| `DNS_PORT` | No | `53` | Port for DNS server |
| `STAGING` | No | `false` | Use Let's Encrypt staging server (for testing) |
| `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 |
### Examples
### Non-standard port
**Test with staging server first:**
```bash
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" \
-e STAGING="true" \
dns-wildcard-cert
```
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:
**Dry run (no certificates saved):**
```bash
docker run -it --rm \
-p 53:53/tcp -p 53:53/udp \
-e EMAIL="youremail@example.com" \
-e DOMAINS="-d example.com -d *.example.com" \
-e DRY_RUN="true" \
dns-wildcard-cert
```
**Bind to specific IP:**
```bash
docker run -it --rm \
-v "/etc/letsencrypt:/etc/letsencrypt" \
-v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
-p 1.2.3.4:53:53/tcp -p 1.2.3.4:53:53/udp \
-e EMAIL="youremail@example.com" \
-e DOMAINS="-d example.com -d *.example.com" \
-e DNS_ADDRESS="0.0.0.0" \
dns-wildcard-cert
```
**Use non-standard port (with DNS forwarding):**
```bash
docker run -it --rm \
-v "/etc/letsencrypt:/etc/letsencrypt" \
@@ -114,7 +133,7 @@ docker run -it --rm \
## Certificate Renewal
For renewal, you can run the same container periodically or use certbot's renew command:
Run the same container again periodically, or use certbot's renew command:
```bash
docker run -it --rm \
@@ -130,7 +149,7 @@ docker run -it --rm \
Certificates are stored in the `/etc/letsencrypt` volume:
- Certificate: `/etc/letsencrypt/live/<domain>/fullchain.pem`
- Private Key: `/etc/letsencrypt/live/<domain>/privkey.pem`
- Private key: `/etc/letsencrypt/live/<domain>/privkey.pem`
## Docker Compose
@@ -156,17 +175,6 @@ volumes:
letsencrypt-lib:
```
## Parameter Changes
**Note:** The old certbot-dns-standalone parameter format has changed:
| Old Format | New Format |
|------------|------------|
| `--authenticator certbot-dns-standalone:dns-standalone` | `--authenticator dns-standalone` |
| `--certbot-dns-standalone:dns-standalone-address=` | `--dns-standalone-address=` |
| `--certbot-dns-standalone:dns-standalone-ipv6-address=` | `--dns-standalone-ipv6-address=` |
| `--certbot-dns-standalone:dns-standalone-port=` | `--dns-standalone-port=` |
## License
MIT