BIND DNS Server in Docker with Persistent Zone Config
BIND, the venerable DNS server, can be a bit finicky about where it stores its zone files. When you’re trying to run it in Docker, this becomes a primary hurdle. The core issue is that BIND expects zone data to live in specific filesystem locations, but Docker containers, by default, are ephemeral. If you don’t manage this persistence correctly, your DNS server will forget all its zones every time the container restarts.
Common Causes and Fixes for BIND Zone Persistence Issues
1. Incorrect Volume Mounting for Zone Files
-
Diagnosis: Check your
docker runordocker-compose.ymlfile. Are you mounting a volume or bind mount to/var/named(or wherever your BIND configuration points for zone data)? -
Fix: Ensure you have a volume or bind mount for BIND’s data directory. For example, in
docker-compose.yml:services: bind: image: bind9/bind9 # Or your preferred BIND image volumes: - ./bind_data:/var/named # Mounts a local directory to BIND's data dir - ./bind_config:/etc/bind # Mounts your BIND config dirThen, ensure your BIND configuration (
named.confor included files) points to/var/named/db.example.comfor your zones. -
Why it works: This explicitly tells Docker to map a directory on your host machine (or a Docker-managed volume) to the container’s
/var/nameddirectory. Any changes BIND makes to zone files within/var/namedinside the container are written to this persistent location on your host, surviving container restarts.
2. Missing named.conf and Zone File Mounts
- Diagnosis: Even with a zone file mount, BIND won’t know about your zones if its configuration file isn’t loaded. Check if
/etc/bind/named.conf(or equivalent) is accessible and correctly populated within the container. - Fix: Mount your BIND configuration directory. Using the
docker-compose.ymlexample above, the./bind_config:/etc/bindline handles this. Ensure yournamed.conffile (and any included files likenamed.conf.local) are present in the./bind_configdirectory on your host. - Why it works: BIND needs its configuration to be present at startup. By mounting the configuration directory, you provide BIND with the
named.conffile, which tells it which zones to load and where to find their data files (which are themselves managed by another volume).
3. Incorrect Permissions on Mounted Volumes
- Diagnosis: BIND runs as a specific user (often
bindornamed). If the mounted directories on the host don’t have the correct ownership or permissions for this user, BIND will fail to read config or write zone data. Checkls -ld /path/to/your/bind_dataand/path/to/your/bind_configon your host. - Fix: Ensure the BIND user within the container (check your BIND image’s documentation for the UID/GID, often 1001:1001 or similar) has read/write access to the mounted directories. You might need to
chownyour host directories:sudo chown -R 1001:1001 ./bind_data ./bind_config. - Why it works: The BIND process inside the container needs to interact with the filesystem paths that are mapped from your host. If the user running BIND doesn’t have the necessary read and write permissions on these host directories, it will error out, preventing zone loading or updates.
4. Missing rndc.key and rndc.conf for Dynamic Updates
-
Diagnosis: If you intend to perform dynamic DNS updates (e.g., with
nsupdate), BIND needs to authenticate withrndc. Check ifrndc.keyis present in/etc/bindand ifrndc.confis configured correctly. -
Fix: Generate
rndc.keyon your host before starting the container, or ensure it’s included in your mounted configuration. Then, ensure BIND is configured to use it. A commonnamed.conf.localentry:controls { inet 127.0.0.1 port 953 allow { localhost; } keys { "rndc"; }; };And ensure
rndc.keyis in/etc/bind/rndc.key. -
Why it works:
rndcis BIND’s control utility. Thecontrolsblock innamed.confdefines how BIND listens forrndccommands. Thekeysdirective specifies which cryptographic key (rndc) BIND expects to authenticate incoming commands. Therndc.keyfile provides the actual key material. Mounting this key into the container allowsrndccommands to be processed.
5. Using an Immutable BIND Image Without Proper Configuration
-
Diagnosis: Some BIND Docker images are built with minimal content, assuming you’ll provide all configuration and zone data via volumes. If you expect the image to have default zones or configs, you’ll be disappointed.
-
Fix: Always provide your
named.confand zone files via volumes. A minimaldocker-compose.ymlfor a simple setup:services: bind: image: ubuntu/bind9 # Example, choose a suitable image ports: - "53:53/udp" - "53:53/tcp" volumes: - ./bind_config/named.conf:/etc/bind/named.conf:ro - ./bind_config/named.conf.local:/etc/bind/named.conf.local:ro - ./bind_data:/var/lib/bind # Or /var/named, depending on image(Note the
:rofor read-only if you don’t intend to modify config files from within the container). -
Why it works: This forces you to explicitly define what BIND loads. By providing
named.confand zone data via volumes, you bypass any assumptions about the image’s internal state and ensure BIND uses your defined, persistent data.
6. Incorrect listen-on or allow-query Directives
- Diagnosis: After fixing persistence, you might find clients can’t query your server. Check
named.confforlisten-onandallow-querydirectives. - Fix: Ensure
listen-onincludes the IP address BIND will bind to inside the container (often127.0.0.1and/oranyif using host networking or specific container IPs). Ensureallow-queryincludes the IP ranges of your clients. For a Docker setup,listen-on { any; };is common if BIND is listening on all interfaces within the container.allow-query { localhost; any; };is also a frequent starting point. - Why it works:
listen-oncontrols which network interfaces BIND listens on for incoming DNS requests.allow-queryis an Access Control List (ACL) that dictates which clients are permitted to send queries to the server. Misconfiguration here prevents legitimate queries from reaching BIND, even if zones are loaded correctly.
The next error you’ll hit is likely a SERVFAIL response for queries, indicating BIND is running but unable to resolve them, often due to upstream resolver issues or incorrect forwarders configuration.
Understanding BIND Views
The most surprising true thing about BIND views is that they allow a single DNS server instance to respond differently to the exact same DNS query based entirely on the source IP address of the client making the request. It’s not about caching or load balancing; it’s about presenting entirely separate DNS namespaces from the same server process.
Let’s see BIND views in action. Imagine we want internal clients (on 192.168.1.0/24) to resolve internal.example.com to 10.0.0.1, while external clients (everyone else) resolve the same internal.example.com to a public IP 203.0.113.10.
Here’s a simplified named.conf demonstrating this:
acl "internal" { 192.168.1.0/24; localhost; };
acl "external" { any; };
options {
directory "/var/named";
pid-file "/run/named/named.pid";
session-keyfile "/run/named/session.key";
listen-on port 53 { any; };
allow-query { any; };
recursion yes;
dnssec-validation auto;
// Default view for external clients
view "external" {
match-clients { external; };
zone "example.com" {
type master;
file "db.example.com.public"; // Public IP for example.com
};
zone "internal.example.com" {
type master;
file "db.internal.example.com.public"; // Public IP for internal.example.com
};
// ... other zones for external
};
// View for internal clients
view "internal" {
match-clients { internal; };
zone "example.com" {
type master;
file "db.example.com.private"; // Private IP for example.com
};
zone "internal.example.com" {
type master;
file "db.internal.example.com.private"; // Private IP for internal.example.com
};
// ... other zones for internal
};
};
And the corresponding zone files:
db.internal.example.com.public:
$TTL 86400
@ IN SOA ns1.example.com. admin.example.com. (
2023010101 ; serial
3600 ; refresh
1800 ; retry
604800 ; expire
86400 ; minimum TTL
)
IN NS ns1.example.com.
ns1 IN A 203.0.113.5 ; Public IP of NS for external
internal IN A 203.0.113.10 ; Public IP for internal.example.com
db.internal.example.com.private:
$TTL 86400
@ IN SOA ns1.example.com. admin.example.com. (
2023010101 ; serial
3600 ; refresh
1800 ; retry
604800 ; expire
86400 ; minimum TTL
)
IN NS ns1.example.com.
ns1 IN A 10.0.0.5 ; Private IP of NS for internal
internal IN A 10.0.0.1 ; Private IP for internal.example.com
When a client from 192.168.1.100 queries for internal.example.com, BIND checks its views. It matches the internal ACL and uses the internal view, returning 10.0.0.1. If a client from 8.8.8.8 makes the same query, it matches only the external ACL (as any is broader but checked after more specific ACLs), and BIND uses the external view, returning 203.0.113.10.
This capability is powerful for security (hiding internal infrastructure details), performance (serving local IPs), and compliance. The key is the match-clients directive within each view block, which BIND evaluates in order. The first view whose match-clients list contains the client’s IP address is the one used for that query.
A common pitfall with views is forgetting to define a default or catch-all view. If a client’s IP doesn’t match any specific match-clients list, and there’s no view without match-clients (which acts as the default), the query will fail. Also, the order of view blocks matters if ACLs overlap; BIND uses the first match.
The next concept you’ll likely encounter is how to manage BIND views and zone data dynamically using tools like BIND DLZ (Dynamically Loadable Zones) or integration with databases.