Deploy BIND DNS server using Docker containers with production-ready security configurations. This guide covers both authoritative and recursive deployments.
NET_BIND_SERVICE capability, or| Item | Details |
|---|---|
| Official Image | internetsystemsconsortium/bind9 |
| Current Stable | 9.20 tag (BIND 9.20.20) |
| ESV/LTS | 9.18 tag (BIND 9.18.46) |
| Development | 9.21 tag (BIND 9.21.19) |
| Base OS | Alpine Linux |
| User | UID 53 (bind user) |
| Ports | 53/UDP, 53/TCP (DNS), 953/TCP (RNDC) |
Always use specific version tags instead of latest for production deployments.
version: '3.8'
services:
bind:
image: internetsystemsconsortium/bind9:9.20.20
container_name: bind9-recursor
restart: unless-stopped
ports:
- "53:53/udp"
- "53:53/tcp"
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
read_only: true
tmpfs:
- /tmp
- /var/run
- /var/cache/bind
user: "53:53"
security_opt:
- no-new-privileges:true
volumes:
- ./config:/etc/bind:ro
- ./logs:/var/log/named
- ./data:/var/cache/bind
networks:
- dns-net
healthcheck:
test: ["CMD", "dig", "@127.0.0.1", "example.com"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
networks:
dns-net:
driver: bridge
version: '3.8'
services:
bind:
image: internetsystemsconsortium/bind9:9.20.20
container_name: bind9-authoritative
restart: unless-stopped
ports:
- "53:53/udp"
- "53:53/tcp"
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
read_only: true
tmpfs:
- /tmp
- /var/run
user: "53:53"
security_opt:
- no-new-privileges:true
volumes:
- ./config:/etc/bind:ro
- ./zones:/var/lib/bind
- ./logs:/var/log/named
- ./data:/var/cache/bind
networks:
- dns-net
healthcheck:
test: ["CMD", "dig", "@127.0.0.1", "example.com"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
networks:
dns-net:
driver: bridge
Run separate instances for authoritative and recursive functions:
version: '3.8'
services:
# Authoritative-only server (no recursion)
bind-authoritative:
image: internetsystemsconsortium/bind9:9.20.20
container_name: bind9-auth
restart: unless-stopped
ports:
- "53:53/udp"
- "53:53/tcp"
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
read_only: true
tmpfs:
- /tmp
- /var/run
user: "53:53"
security_opt:
- no-new-privileges:true
volumes:
- ./auth-config:/etc/bind:ro
- ./zones:/var/lib/bind
- ./auth-logs:/var/log/named
networks:
- dns-net
command: ["-4"]
# Recursive resolver (internal only)
bind-recursor:
image: internetsystemsconsortium/bind9:9.20.20
container_name: bind9-rec
restart: unless-stopped
ports:
- "5354:53/udp" # Different port for internal recursion
- "5354:53/tcp"
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
read_only: true
tmpfs:
- /tmp
- /var/run
user: "53:53"
security_opt:
- no-new-privileges:true
volumes:
- ./recursor-config:/etc/bind:ro
- ./recursor-logs:/var/log/named
networks:
- dns-net
command: ["-4"]
networks:
dns-net:
driver: bridge
Create config/named.conf:
options {
directory "/var/cache/bind";
pid-file "/tmp/named.pid";
// Listen on all interfaces inside container
listen-on port 53 { any; };
listen-on-v6 port 53 { none; };
// Allow queries from trusted networks only
allow-query { 10.0.0.0/8; 172.16.0.0/12; 192.168.0.0/16; 127.0.0.0/8; };
allow-recursion { 10.0.0.0/8; 172.16.0.0/12; 192.168.0.0/16; 127.0.0.0/8; };
// Disable zone transfers
allow-transfer { none; };
// Forwarders for recursive queries
forwarders {
1.0.0.1;
8.8.8.8;
};
// DNSSEC validation (critical for security)
dnssec-enable yes;
dnssec-validation auto;
// QNAME minimization for privacy
qname-minimization yes;
// Response Rate Limiting (RRL)
rate-limit {
responses-per-second 5;
window 10;
slip 2;
};
// Logging
channel default_log {
file "/var/log/named/named.log" versions 10 size 10m;
severity info;
print-time yes;
print-category yes;
};
logging {
category default { default_log; };
};
// Security
auth-nxdomain no;
recursion yes;
version "not currently available";
hostname "not currently available";
};
Create config/named.conf for authoritative-only:
options {
directory "/var/lib/bind";
pid-file "/tmp/named.pid";
// Listen on all interfaces
listen-on port 53 { any; };
listen-on-v6 port 53 { none; };
// Allow queries from anyone (public authoritative)
allow-query { any; };
// CRITICAL: Disable recursion on authoritative server
recursion no;
additional-from-auth no;
additional-from-cache no;
// Zone transfers only to specific slaves
allow-transfer { 192.168.1.10; 192.168.1.11; };
// DNSSEC
dnssec-enable yes;
dnssec-validation auto;
// Response Rate Limiting (protect against amplification)
rate-limit {
responses-per-second 5;
window 10;
slip 2;
};
// Logging
channel default_log {
file "/var/log/named/named.log" versions 10 size 10m;
severity info;
print-time yes;
print-category yes;
};
logging {
category default { default_log; };
category security { default_log; };
};
// Security
auth-nxdomain no;
version "not currently available";
hostname "not currently available";
};
// Include zone definitions
include "/etc/bind/named.conf.local";
Create config/named.conf.local for zone definitions:
// Example master zone
zone "example.com" {
type master;
file "/var/lib/bind/db.example.com";
allow-transfer { 192.168.1.10; 192.168.1.11; };
};
// Example slave zone
zone "example.org" {
type slave;
file "/var/lib/bind/slaves/db.example.org";
masters { 192.168.1.10; };
allow-transfer { none; };
};
Create zone file zones/db.example.com:
$TTL 86400
@ IN SOA ns1.example.com. admin.example.com. (
2026022701 ; Serial (YYYYMMDDNN format)
3600 ; Refresh
1800 ; Retry
1209600 ; Expire
86400 ) ; Negative Cache TTL
; Name servers
@ IN NS ns1.example.com.
@ IN NS ns2.example.com.
; A records
@ IN A 192.168.1.10
ns1 IN A 192.168.1.10
ns2 IN A 192.168.1.11
www IN A 192.168.1.20
mail IN A 192.168.1.30
; CNAME records
ftp IN CNAME www.example.com.
; MX records
@ IN MX 10 mail.example.com.
; TXT records
@ IN TXT "v=spf1 mx ~all"
BIND in container runs as UID 53. Set permissions:
# Create directories
mkdir -p config zones logs data
# Set ownership for writable directories
sudo chown -R 53:53 logs data zones
# Config is read-only, owned by root
sudo chown -R root:root config
sudo chmod 755 config
sudo chmod 644 config/*
# Start the container
docker compose up -d
# Check status
docker compose ps
# View logs
docker compose logs -f bind
docker logs bind9
docker logs bind9 --follow # Follow logs in real-time
docker exec -it bind9 bash
# Inside container:
rndc status
named-checkconf
named-checkzone example.com /var/lib/bind/db.example.com
docker exec bind9 rndc reload
docker compose down
# Test local resolution
dig @127.0.0.1 example.com
# Test DNSSEC validation
dig @127.0.0.1 dnssec-failed.org
# Should return SERVFAIL if validation working
# Check server status
docker exec bind9 rndc status
# View statistics
docker exec bind9 rndc stats
latestBackup configuration and zone files regularly:
# Backup configuration and zones
tar -czf bind-backup-$(date +%Y%m%d).tar.gz config zones
# Backup to remote location
rsync -av config zones backup-server:/backups/bind/
# Pull latest image
docker compose pull
# Review changes in release notes
# https://kb.isc.org/docs/relnotes
# Stop containers
docker compose down
# Start with new image
docker compose up -d
# Verify version
docker exec bind9 named -v
# Check if port 53 is in use
sudo ss -tulnp | grep :53
# Check logs
docker logs bind9
# Verify configuration syntax
docker exec bind9 named-checkconf
# Check if service is listening
docker exec bind9 netstat -tulnp | grep :53
# Test internal resolution
docker exec bind9 dig @127.0.0.1 google.com
# Check firewall rules
sudo iptables -L -n | grep 53
# Fix ownership
sudo chown -R 53:53 logs data zones
# Verify permissions
ls -la logs data zones
# Limit cache size in config
options {
max-cache-size 256m;
};
BIND doesn’t natively support DoT. Use dnsdist or stubby as frontend:
# Add dnsdist service to docker-compose.yml
dnsdist:
image: powerdns/dnsdist-18:1.8.3
container_name: dnsdist-dot
ports:
- "853:853/tcp" # DoT port
- "53:53/udp"
- "53:53/tcp"
volumes:
- ./dnsdist.conf:/etc/dnsdist/dnsdist.conf:ro
networks:
- dns-net
Running BIND in containers for production? We help with:
Need help? office@linux-server-admin.com or Contact Us