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 run or docker-compose.yml file. 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 dir
    

    Then, ensure your BIND configuration (named.conf or included files) points to /var/named/db.example.com for 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/named directory. Any changes BIND makes to zone files within /var/named inside 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.yml example above, the ./bind_config:/etc/bind line handles this. Ensure your named.conf file (and any included files like named.conf.local) are present in the ./bind_config directory 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.conf file, 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 bind or named). 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. Check ls -ld /path/to/your/bind_data and /path/to/your/bind_config on 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 chown your 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 with rndc. Check if rndc.key is present in /etc/bind and if rndc.conf is configured correctly.

  • Fix: Generate rndc.key on your host before starting the container, or ensure it’s included in your mounted configuration. Then, ensure BIND is configured to use it. A common named.conf.local entry:

    controls {
        inet 127.0.0.1 port 953 allow { localhost; } keys { "rndc"; };
    };
    

    And ensure rndc.key is in /etc/bind/rndc.key.

  • Why it works: rndc is BIND’s control utility. The controls block in named.conf defines how BIND listens for rndc commands. The keys directive specifies which cryptographic key (rndc) BIND expects to authenticate incoming commands. The rndc.key file provides the actual key material. Mounting this key into the container allows rndc commands 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.conf and zone files via volumes. A minimal docker-compose.yml for 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 :ro for 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.conf and 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.conf for listen-on and allow-query directives.
  • Fix: Ensure listen-on includes the IP address BIND will bind to inside the container (often 127.0.0.1 and/or any if using host networking or specific container IPs). Ensure allow-query includes 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-on controls which network interfaces BIND listens on for incoming DNS requests. allow-query is 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.

Want structured learning?

Take the full Bind course →