Containerized BIND DNS Server with Smart Management
Quick reference for day-to-day BindCaptain operations. Substitute the
<PLACEHOLDERS> below with your own values.
Install dir: /opt/bindcaptain (default for systemd installs)
Container name: bindcaptain
DNS IP: <PRIMARY-IP>:53 — whatever your listen-on advertises
/opt/bindcaptain/
├── config/ # ← YOUR DNS CONFIGS (mounted in container)
│ ├── named.conf # Main BIND configuration
│ ├── example.com/example.com.db # example.com zone file
│ ├── mydomain.net/ # mydomain.net domain
│ │ ├── mydomain.net.db # Forward zone
│ │ ├── 1.0.10.in-addr.arpa.db # Reverse zones (one per /24)
│ │ ├── 2.0.10.in-addr.arpa.db
│ │ └── 3.0.10.in-addr.arpa.db
│ └── named.ca # Root hints file
├── logs/ # Log files
├── bindcaptain.sh # Container management script
├── tools/
│ ├── bindcaptain_manager.sh # DNS record management (bc.*)
│ └── bindcaptain_refresh.sh # Auto-refresh script (cron)
# Container status
sudo podman ps --filter name=bindcaptain
# Start container
sudo podman start bindcaptain
# Stop container
sudo podman stop bindcaptain
# Restart container
sudo podman restart bindcaptain
# View logs (live)
sudo podman logs -f bindcaptain
# View recent logs
sudo podman logs --tail 20 bindcaptain
cd /opt/bindcaptain
# Container status and info
sudo ./bindcaptain.sh status
# View logs
sudo ./bindcaptain.sh logs
# Force rebuild (if needed)
sudo ./bindcaptain.sh build
⚠ IMPORTANT: Always use the bc.* commands - never edit zone files manually!
BindCaptain ships two distinct bc.* command surfaces. Pick the right one:
| Surface | Where to source | What you get | When to use |
|---|---|---|---|
Chief plugin (chief-plugin/bc_chief-plugin.sh) |
Operator/workstation shell, via Chief. Or directly on the DNS host with BC_HOST unset. |
High-level wrappers: bc.create, bc.update, bc.delete, bc.list, bc.refresh, bc.sync_ptr, plus bc.create_cname / bc.create_txt shortcuts. Output is normalized; --json is supported. |
Default for day-to-day DNS edits, local or remote. |
In-container manager (tools/bindcaptain_manager.sh) |
Sourced inside the container scope on the DNS host (or via sudo bash -c 'source $BC_MANAGER && …'). |
Low-level primitives: bc.create_record, bc.create_cname, bc.create_txt, bc.delete_record, bc.list_records. |
Direct host-local debugging or scripted automation that already runs on the DNS host. |
Important: The
_record-suffixed names (bc.create_record,bc.delete_record,bc.list_records) do not exist in a Chief-plugin operator shell — only the high-level wrappers are present. Tab-completebc.<TAB>to confirm what’s loaded.
# Operator / remote workstation — Chief plugin (recommended)
chief.plugin bc # see chief-plugin/README.md for install
# Or, on the DNS host directly, just source the plugin file with BC_HOST unset:
source /opt/bindcaptain/chief-plugin/bc_chief-plugin.sh
# Direct on the DNS host — in-container manager (low-level)
source /opt/bindcaptain/tools/bindcaptain_manager.sh
bc.help # list all commands available in the loaded surface
bc.create --help # per-command help (Chief plugin)
bc.create_record --help # per-command help (in-container manager)
Signatures are taken verbatim from chief-plugin/bc_chief-plugin.sh. Write operations only support A, CNAME, and TXT — anything else returns Unsupported record type.
bc.create [A] <fqdn> <ip> [ttl]# Either FQDN form…
bc.create newserver.example.com 192.168.1.100
# …or hostname + domain form
bc.create newserver example.com 192.168.1.100
# With explicit TTL and JSON output
bc.create webserver.example.com 192.168.1.200 3600 --json
# Explicit type (A is the default)
bc.create A webserver.example.com 192.168.1.200
bc.create CNAME <fqdn> <target># FQDN form
bc.create CNAME www.example.com webserver
# alias + domain form
bc.create CNAME www example.com webserver
# Point at an external target (trailing dot makes it absolute)
bc.create CNAME ftp.example.com newserver.example.com.
# Shortcut wrapper
bc.create_cname www.example.com webserver
bc.create TXT <name> <domain> <value>bc.create TXT @ example.com 'v=spf1 include:_spf.google.com ~all'
bc.create TXT _dmarc example.com 'v=DMARC1; p=none'
# Shortcut wrapper
bc.create_txt @ example.com 'v=spf1 -all'
bc.update [TYPE] <fqdn> <new_value> [ttl]Implemented as a delete-then-create on the remote host inside one SSH session, so BIND only reloads at the end.
bc.update webserver.example.com 192.168.1.200 # change A IP
bc.update webserver.example.com 192.168.1.200 3600 # also update TTL
bc.update CNAME www.example.com newtarget # change CNAME target
bc.update TXT @ example.com 'v=spf1 -all' # change TXT value
# JSON input (useful for IPAM / Igor-style integrations)
bc.update --json '{"type":"A","fqdn":"web.example.com","rdata":"192.0.2.200","ttl":3600}'
bc.delete <fqdn> [TYPE]bc.delete oldserver.example.com # deletes any matching record
bc.delete oldserver example.com # hostname + domain form
bc.delete www.example.com CNAME # restrict to a record type
bc.delete www.example.com CNAME --json # JSON output
bc.list [domain] [--json|-j]bc.list # all records, all domains
bc.list example.com # one domain
bc.list example.com --json # machine-readable JSON
bc.refresh # validate all zones + reload BIND (--json supported)
bc.sync_ptr # rebuild managed PTR zones from forward A records
bc.status / bc.start / bc.stop / bc.restart # service control on the host
bc.ssh # open an SSH session to BC_HOST
bc.git_refresh # git-pull BindCaptain on the host
Aliases: bc.a=bc.create, bc.up=bc.update, bc.cname=bc.create_cname, bc.txt=bc.create_txt, bc.rm=bc.delete, bc.ls=bc.list.
Only callable when tools/bindcaptain_manager.sh is sourced inside the container scope (or invoked via sudo bash -c 'source $BC_MANAGER && …'). The Chief-plugin wrappers above dispatch to these under the hood.
# A record
# bc.create_record [--backup] <fqdn> <ip> [ttl]
# bc.create_record [--backup] <hostname> <domain> <ip> [ttl]
bc.create_record newserver example.com 192.168.1.100
bc.create_record webserver.example.com 192.168.1.200 3600
# CNAME
# bc.create_cname [--backup] <fqdn> <target>
# bc.create_cname [--backup] <alias> <domain> <target>
bc.create_cname www example.com newserver
bc.create_cname ftp.example.com newserver.example.com.
# TXT
# bc.create_txt [--backup] <name> <domain> <text_value>
bc.create_txt @ example.com 'v=spf1 include:_spf.google.com ~all'
bc.create_txt _dmarc example.com 'v=DMARC1; p=none'
# Delete
# bc.delete_record [--backup] <fqdn> [record_type]
# bc.delete_record [--backup] <name> <domain> [record_type]
bc.delete_record oldserver example.com
bc.delete_record www.example.com CNAME
# List
# bc.list_records [domain] [type]
bc.list_records
bc.list_records example.com
bc.list_records example.com A
$ORIGIN .
$TTL 86400
domain.com IN SOA ns1.domain.com. admin.domain.com. (
2025092401 ; serial (YYYYMMDDNN)
43200 ; refresh (12 hours)
180 ; retry (3 minutes)
1209600 ; expire (2 weeks)
10800 ; minimum (3 hours)
)
NS ns1.domain.com.
$ORIGIN domain.com.
; A Records
hostname IN A 192.168.1.100
ns1 IN A 192.168.1.1
; CNAME Records
www IN CNAME hostname
# Edit main config
sudo vi /opt/bindcaptain/config/named.conf
# Validate configuration
sudo podman exec bindcaptain named-checkconf
# Restart container to apply
sudo podman restart bindcaptain
# 1. Create directory and zone file
sudo mkdir -p /opt/bindcaptain/config/newdomain.com
sudo cp /opt/bindcaptain/config-examples/example.com/example.com.db \
/opt/bindcaptain/config/newdomain.com/newdomain.com.db
# 2. Edit zone file
sudo vi /opt/bindcaptain/config/newdomain.com/newdomain.com.db
# 3. Add zone to named.conf
sudo vi /opt/bindcaptain/config/named.conf
# Add:
# zone "newdomain.com" IN {
# type primary;
# file "newdomain.com/newdomain.com.db";
# check-names warn;
# notify primary-only;
# also-notify { <SECONDARY-IP>; <SECONDARY-IP-2>; };
# allow-transfer { <SECONDARY-IP>; <SECONDARY-IP-2>; };
# allow-query { any; };
# };
# 4. Restart container
sudo podman restart bindcaptain
# Check configuration
sudo podman exec bindcaptain named-checkconf
# Check specific zone
sudo podman exec bindcaptain named-checkzone example.com /var/named/example.com/example.com.db
sudo podman exec bindcaptain named-checkzone mydomain.net /var/named/mydomain.net/mydomain.net.db
# Or validate from host
sudo named-checkzone example.com /opt/bindcaptain/config/example.com/example.com.db
# Test forward DNS
dig @<PRIMARY-IP> host.example.com
dig @<PRIMARY-IP> host.example.net
# Test reverse DNS
dig @<PRIMARY-IP> -x <PRIMARY-IP>
# Test from external
dig @<PRIMARY-IP> primary.example.com +short
# Check NS records
dig @<PRIMARY-IP> example.com NS
# Container logs
sudo podman logs bindcaptain | tail -20
# DNS refresh automation logs
sudo tail -f /opt/bindcaptain/logs/dns_refresh.log
# Cron logs
sudo tail -f /opt/bindcaptain/logs/cron.log
# System cron logs
sudo journalctl -u crond -f
# Container resource usage
sudo podman stats bindcaptain
# Check if container is responding
dig @<PRIMARY-IP> . +short
# View active queries (if logging enabled)
sudo podman exec bindcaptain tail -f /var/log/named/named.log
Note: PTR records are created automatically when A records are added (for IPs in managed reverse networks). No cron jobs or external tools required.
# Chief plugin — creates A record AND PTR record automatically
bc.create hostname.domain.com 192.168.1.100
# In-container manager (low-level, on the host) — same automatic PTR
bc.create_record hostname domain.com 192.168.1.100
# Force a full revalidation + reload
bc.refresh
# Or rebuild PTR zones from forward A records
bc.sync_ptr # Chief plugin
# bc.sync_ptr_from_forwards # in-container manager name
# Backup entire config
sudo tar -czf /opt/bindcaptain-backup-$(date +%Y%m%d).tar.gz /opt/bindcaptain/config/
# Backup specific zone
sudo cp /opt/bindcaptain/config/example.com/example.com.db \
/opt/bindcaptain/config/example.com/example.com.db.backup.$(date +%Y%m%d)
Container Won’t Start:
# Check logs for errors
sudo podman logs bindcaptain
# Check if port is in use
sudo ss -tlnp | grep :53
# Check configuration
sudo podman exec bindcaptain named-checkconf
DNS Not Resolving:
# Check if BIND is listening
sudo podman exec bindcaptain ss -tlnp | grep :53
# Check container ports
sudo podman port bindcaptain
# Test locally first
dig @127.0.0.1 hostname.domain.com
Zone File Errors:
# Always validate after editing
sudo named-checkzone domain.com /opt/bindcaptain/config/domain.com/domain.com.db
# Common issues:
# - Missing trailing dots in CNAMEs
# - Forgot to increment serial number
# - Missing newline at end of file
# - Incorrect file permissions
Permission Issues:
# Fix file ownership
sudo chown -R named:named /opt/bindcaptain/config/
# Fix permissions
sudo find /opt/bindcaptain/config/ -name "*.db" -exec chmod 644 {} \;
sudo chmod 640 /opt/bindcaptain/config/named.conf
# Stop container
sudo podman stop bindcaptain
# Restore from backup
sudo tar -xzf /opt/bindcaptain-backup-YYYYMMDD.tar.gz -C /
# Start container
sudo podman start bindcaptain
| Task | Command |
|---|---|
| Container Status | sudo podman ps --filter name=bindcaptain |
| Restart DNS | sudo podman restart bindcaptain |
| View Logs | sudo podman logs bindcaptain \| tail -20 |
| Edit Zone | sudo vi /opt/bindcaptain/config/domain/domain.db |
| Validate Zone | sudo named-checkzone domain /opt/bindcaptain/config/domain/domain.db |
| Test DNS | dig @<PRIMARY-IP> hostname.domain.com |
| Manual Refresh | sudo /opt/bindcaptain/tools/bindcaptain_refresh.sh |
| Backup Config | sudo tar -czf backup.tar.gz /opt/bindcaptain/config/ |
named-checkzone and named-checkconfdig @<PRIMARY-IP> for testingNeed Help? Check logs first: sudo podman logs bindcaptain | tail -20