A Docker Compose setup that provides a local DNS server (Knot DNS) with ACME certificate authority (Step CA) for development and testing.
- Overview
- Quick Start
- Services
- Configuration
- Usage Examples
- Volumes
- Network
- Health Checks
- Troubleshooting
- File Structure
- Requirements
- Notes
This project sets up:
- Knot DNS Server: Authoritative DNS server for the
.testdomain - Step CA: ACME-enabled certificate authority for issuing TLS certificates
- Custom Network: Isolated Docker network for service communication
┌─────────────────┐ ┌─────────────────┐
│ Knot DNS │ │ Step CA │
│ 10.0.0.10:53 │◄───┤ 10.0.0.11:9000 │
│ │ │ │
│ • test. zone │ │ • ACME endpoint │
│ • ca.test → CA │ │ • ca.test cert │
└─────────────────┘ └─────────────────┘
-
Start the services:
docker-compose up -d # or rebuild everything with: # docker compose up --build --force-recreate -d
-
Wait for services to be healthy:
docker-compose ps
-
Test DNS resolution:
dig @localhost -p 9053 ca.test A
-
Access Step CA:
- ACME directory:
https://ca.test:9000/acme/acme/directory - Root certificate:
https://ca.test:9000/roots.pem
- ACME directory:
-
Extract TSIG and create subdomain
# First extract the tsig.key file and root certificate ./extract-tsig.sh # Optional: enable remote access to Step and Knot ./enable-remote.sh # Create a wildcard subdomain *.mydomain.test entry ./add-subdomain.sh -s mydomain -i <ip it should resolve to>
You should now have a
mydomain-config.yamlfile with all the details to configure an ACME client that should interact with Knot, and Knot will now resolve any query for *.mydomain.test to the IP number you specified. -
Test it out
dig @localhost -p 9053 +short ca.test A dig @localhost -p 9053 +short apples.mydomain.test A
- Port:
9053(mapped from container port 53) - IP:
10.0.0.10(internal network) - Zone file:
knot/test.zone - Config:
knot/knot.conf
DNS Records:
ca.test.→10.0.0.11(Step CA)ns1.test.→10.0.0.10(DNS server)
- Port:
9000 - IP:
10.0.0.11(internal network) - Domain:
ca.test - ACME Endpoint:
/acme/acme/directory
- One-time service that generates CA password
- Creates
/home/step/secrets/passwordwith random password
The Knot DNS server is configured in knot/knot.conf with:
- Authority for
test.domain - HMAC-SHA256 key for dynamic updates automatically generated
- Forwarding to upstream DNS (1.1.1.1, 9.9.9.9) for other domains
- Automatically initialized on first run
- ACME provisioner enabled
- Certificate for
ca.testdomain - Remote management enabled
By default everything runs on its own Docker network with DNS mapped to 0.0.0.0:9053 and step-ca mapped to 0.0.0.0:9000 so these are accessble on the hosts LAN IP. However, to make Knot DNS return the correct LAN IP address when queried for ca.test, you will need to reconfigure the test.zone file to use the LAN IP for ca.test. You can do this as follows:
# First extract the tsig.key from Knot
./extract-tsig.sh
# Now switch to using the LAN IP for ca.test
./enable-remote.sh
# Check if it worked correctly
dig @localhost -p 9053 +short ca.test AThe last command tests if Knot returns the right IP address when resolving ca.test. By default the enable-remote.sh script will try to detect your LAN IP and use that. If this detection fails, you can also provide the IP address that it should use for ca.test:
./enable-remote.sh --ip 192.168.1.10This will then ensure that dns name ca.test will resolve to 192.168.1.10
Use this to create a wildcard subdomain (*.your-sub.test) that points to an IP number of your choice.
-
Extract the TSIG key and CA root
Run extract-tsig.sh. This writes tsig.key and dev_root_ca.pem.
./extract-tsig.sh
-
Create the subdomain
Run add-subdomain.sh. Provide a domain name and IP number that queries using this subdomain should resolve to e.g.:
./add-subdomain.sh -i 192.168.1.50 -s myapp
This creates: *.myapp.test → 192.168.1.50 in Knot and generates a
myapp-config.yamlfile containing:- Step CA root certificate
- TSIG key configuration for DNS updates
- Sub-domain that was configured
Now any query such as
apples.myapp.testwill resolve to the IP address you specified. -
Test
dig @localhost -p 9053 app.<subdomain>.test A
Point your workstation's resolver to resolve ca.test, and any subdomains you created, to your knot-step-acme instance.
Decide the Knot address you’ll use:
- If running locally via Docker: 127.0.0.1:9053
- If using a remote host: :9053
Next, configure your resolver to use your knot-step-acme instance to resolve anything for the *.test domain:
OSX:
sudo mkdir -p /etc/resolver
sudo tee /etc/resolver/test >/dev/null <<EOF
nameserver 127.0.0.1
port 9053
EOF
# If Knot is remote, replace 127.0.0.1 with its LAN IP
# Optional: flush DNS cache
sudo killall -HUP mDNSResponder || trueLinux with systemd-resolved:
sudo mkdir -p /etc/systemd/resolved.conf.d
sudo tee /etc/systemd/resolved.conf.d/test.conf >/dev/null <<EOF
[Resolve]
DNS=127.0.0.1:9053
Domains=~test
EOF
sudo systemctl restart systemd-resolved
# If Knot is remote, use DNS=<LAN-IP>:9053 insteadFor details about resolved.conf see the man page.
DNSMasq:
If you like to use dnsmasq, add the following entry to your dnsmasq.conf file:
# Knot Step Acme Server
server=/test/127.0.0.1#9053
# If Knot is remote, replace 127.0.0.1 with the <LAN-IP> of Knot instead
It is also recommended to use DNSMasq if you cannot get your per-domain resolver to work correctly. In such a case configure DNSMasq as above, and add upstream servers pointing your actual dns servers:
server=1.1.1.1
server=1.0.0.1
server=9.9.9.9
no-resolv # Tells dnsmasq not use /etc/resolv.conf for upstream
DNSMasq will now send everything for *.test to the knot-step-acme instance, and everything else to the upstream dns servers you specified. Restart dnsmasq and configure your system resolver (e.g. /etc/resolv.conf) to use it instead.
If all else Fails:
If the per-domain resolvers do not seem to work and using dnsmasq is not an option for you, as a last resort you can configure knot-step-acme docker-compose.yaml to also port-map your hosts port 53 to the container's port 9053. Assuming knot-step-acme is running on a machine with LAN IP 192.168.1.10, you can expose Knot DNS on port 53 as follows:
services:
# ...
knot:
# ...
ports:
- "0.0.0.0:9053:53/tcp"
- "0.0.0.0:9053:53/udp"
- "192.168.1.10:53:53/tcp"
- "192.168.1.10:53:53/udp"
# ...Restart knot-step-acme:
docker compose restartNow configure your machine to use knot-step-acme as your dns server, i.e. 192.168.1.10. Note that knot-step-acme is configured to use Cloudflare DNS (1.1.1.1) and Quad9 (9.9.9.9) as upstream, so any query it gets that is not for *.test will be forwarded to those upstream dns servers instead.
Verify:
dig +short ca.test A # check if our Step CA can be resolved
dig +short www.joindns4.eu # check if upstream is workingIf you will run your K3D cluster on the same machine, you can tell it to use the same docker network as knot-step-acme:
k3d cluster create my-cluster \
--k3s-arg "--cluster-dns=10.0.0.10@server:*" \
--k3s-arg "--cluster-domain=cluster.local@server:*" \
--network knot-step-acme_lab \
--waitAnything running on K3D will now use the Knot Step Acme instance running on the docker network. If, however, your K3D is not running on the same docker network, you will need to configure K3D's CoreDNS instead. We will have to tell it to use your remote Knot instance on port 9053 to resolve anything with *.test, which you can do so as follows:
# Create a config file pointing .test to your Knot instance (non-standard DNS port 9053)
DNS_SERVER="<LAN IP exposing remote Knot>:9053"
cat > coredns_custom.yaml <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns-custom
namespace: kube-system
data:
test.server: |
test:53 {
errors
cache 30
forward . $DNS_SERVER
}
EOF
# Launch your K3D cluster with custom CoreDNS properties
k3d cluster create my-cluster \
--volume ./coredns_custom.yaml:/var/lib/rancher/k3s/server/manifests/coredns-custom.yaml@server:0 \
--waitYou can test if its working with the following:
# Deploy a test pod
kubectl run test-dns --image=alpine:latest --rm -it -- sh
# Inside the pod, test DNS resolution
nslookup ca.testMost likely you will also want to use cert-manager to issue ACME certificates. If you do, take note that the default configuration has Knot exposed on port 9053. This means cert manager will need to be instructed not to use /etc/resolv.conf from the node. You can do so as follows when installing cert-manager via helm:
DNS_SERVER="<LAN IP exposing remote Knot>:9053"
helm upgrade --install cert-manager jetstack/cert-manager \
-n cert-manager --create-namespace --set installCRDs=true \
--set 'extraArgs={--dns01-recursive-nameservers-only,--dns01-recursive-nameservers=$DNS_SERVER}'See Setting Nameservers for DNS01 Self Check for details about this.
Alternatively, you can also add a mapping for Knot to listen on port 53 on your local LAN IP by editing the docker-compose.yml
services:
# ...
knot:
# ...
ports:
- "0.0.0.0:9053:53/tcp"
- "0.0.0.0:9053:53/udp"
- "192.168.1.10:53:53/tcp"
- "192.168.1.10:53:53/udp"
# ...By default, this setup uses Docker's bridge networking for isolation which is generally the recommended method. If really want, though, you can switch to host networking.
You can do so by setting network_mode: host in the docker-compose.yml file, e.g.:
services:
knot:
# ...
network_mode: host # Use host networking
step-ca:
# ...
network_mode: host # Use host networking Replace the IP addresses in the knot/test.zone with the actual IPs in your network:
$ORIGIN test.
$TTL 60
@ IN SOA ns1.test. hostmaster.test. (1 1h 15m 30d 2h)
IN NS ns1.test.
ns1 IN A 192.168.1.10 ; LAN IP where this will run
@ IN A 192.168.1.100 ; Your development machine's LAN IP
* IN A 192.168.1.100 ; Your development machine's LAN IP
ca IN A 192.168.1.10 ; LAN IP where this will runBefore starting, ensure ports 9000 and 9053 are available:
# Check if ports 9000 and 9053 are in use
sudo netstat -tulpn | egrep ':(9000|9053)\b'If ports are available, you can go ahead and start up knot-step-acme:
docker compose up --build --force-recreate -d-
Get the tsig.key and CA root certificate:
./extract-tsig.sh
-
Trust the Step CA root on your host (so TLS to [https://ca.test:9000] is trusted)
-
macOS:
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain dev_root_ca.pem
-
Debian/Ubuntu:
sudo cp dev_root_ca.pem /usr/local/share/ca-certificates/dev_root_ca.crt sudo update-ca-certificates
-
Windows (Administrator PowerShell):
certutil -addstore -f Root dev_root_ca.pem
-
Use with certbot:
docker run --rm -it \ --network knot-step-acme_lab \ --dns 10.0.0.10 \ -v $(pwd)/dev_root_ca.pem:/etc/certs/dev_root_ca.pem \ -e REQUESTS_CA_BUNDLE="/etc/certs/dev_root_ca.pem" \ certbot/certbot certonly \ --manual \ --server https://ca.test:9000/acme/acme/directory \ --preferred-challenges dns \ --work-dir /tmp --logs-dir /tmp --config-dir /etc/letsencrypt \ -d "example.test" -
Use nsupdate with TSIG key:
# Create nsupdate script cat > update.txt << EOF server localhost 9053 zone test. update add _acme-challenge.example.test. 60 IN TXT "your-acme-challenge-token" send quit EOF # Apply the update nsupdate -k ./tsig.key update.txt # Clean up rm update.txt
# Test DNS resolution
dig @localhost -p 9053 test SOA
dig @localhost -p 9053 ca.test A
dig @localhost -p 9053 anything.test A
# Test from container network
docker run --rm --network knot-step-acme_lab alpine:latest \
nslookup ca.test 10.0.0.10knot-db: Persistent storage for Knot DNS zone filesstep-data: Persistent storage for Step CA configuration and certificates
- Name:
knot-step-acme_lab - Subnet:
10.0.0.0/24 - Gateway:
10.0.0.1
Both services include health checks:
- Knot: Verifies DNS SOA response for
test.domain - Step CA: Verifies HTTPS endpoint accessibility
docker-compose logs knot
docker-compose logs step-ca# From host
dig @localhost -p 9053 ca.test A
# From container network
docker exec knot dig @127.0.0.1 ca.test A# Check if CA is responding
curl -sk https://ca.test:9000/health
# Get CA roots
curl -sk https://ca.test:9000/roots.pemdocker-compose down -v
docker-compose up -d.
├── docker-compose.yml # Main orchestration
├── knot/
│ ├── Dockerfile # Knot DNS container
│ ├── knot.conf # Knot DNS configuration
│ └── test.zone # DNS zone file
└── step-ca/
├── Dockerfile # Step CA container
└── start-step-ca.sh # Step CA initialization script
- Docker
- Docker Compose
- (Optional)
digcommand for testing
- The
.testTLD is reserved for testing (RFC 6761) - Change IP addresses in
knot/test.zoneif needed for your environment - Step CA generates a random password on first run, stored in the
step-datavolume - Services depend on each other: Step CA waits for Knot DNS to be healthy