Run your own private DNS resolver with advanced filtering, DNS over HTTPS (DoH), and DNS over TLS (DoT) — all in a simple Docker setup.
Pi-hole provides customizable DNS filtering and query logging, dnsdist handles DNS/DoT/DoH backends, and Caddy manages HTTPS with automatic Let's Encrypt certificates.
- Custom DNS filtering: Centralized control over which domains are resolved
- Secure DNS: DoH at
https://<DOMAIN_DNS>/dns-query
and DoT on port853
- Automatic TLS: Caddy obtains and renews certificates via Let's Encrypt
- Dashboard: Full query visibility and management at
https://<DOMAIN_DASHBOARD>/admin/
- Persistence & health checks: Data volumes and restart policies included
Set up A/AAAA DNS records pointing both domains to your server's public IP:
DOMAIN_DNS
→ e.g.,dns.example.com
DOMAIN_DASHBOARD
→ e.g.,dashboard.example.com
Ensure the following ports are available (no other services using them):
80, 443
→ Caddy (HTTP/HTTPS)53/tcp, 53/udp
→ DNS (dnsdist → Pi-hole)853/tcp
→ DoT (dnsdist)
-
Clone the repository:
git clone --depth=1 https://github.com/allenhack638/self-hostable-private-dns.git cd self-hostable-private-dns
-
Configure environment:
cp .env.example .env # Edit .env with your domain names and password
-
Start the stack:
docker compose up -d
-
⚠️ If you see "port 53 already in use" errors, then disable systemd-resolved:# Stop the containers first docker compose down # Disable systemd-resolved sudo systemctl stop systemd-resolved sudo systemctl disable systemd-resolved # Start the stack again docker compose up -d
To re-enable systemd-resolved later (if needed):
docker compose down
sudo systemctl enable systemd-resolved
sudo systemctl start systemd-resolved
Why this order matters: If you disable
systemd-resolved
first, your system will lose DNS resolution temporarily, preventing Docker from pulling images and Caddy from verifying certificates. Starting the stack first allows everything to download properly, then we only disablesystemd-resolved
if there's actually a conflict.
Variable | Description | Example |
---|---|---|
TZ |
Timezone in tz database format | Asia/Kolkata |
FTLCONF_webserver_api_password |
Dashboard login password (use a strong password!) | your-secure-password |
DOMAIN_DNS |
Public domain for DoH/DoT (TLS via Caddy) | dns.example.com |
DOMAIN_DASHBOARD |
Public domain for the dashboard | dashboard.example.com |
Timezone reference: See the full list in tz database time zones.
Protocol | Address / URL | Port |
---|---|---|
DNS | <SERVER_IP> |
53 |
DoT | <DOMAIN_DNS> |
853 |
DoH | https://<DOMAIN_DNS>/dns-query |
443 |
Dashboard | https://<DOMAIN_DASHBOARD>/admin/ |
443 |
Port(s) | Service |
---|---|
53/tcp, 53/udp | DNS (dnsdist → Pi-hole) |
80, 443 | Caddy (HTTP/HTTPS) |
853/tcp | DoT (dnsdist) |
- Pi-hole configuration: Stored in
data/pihole/
- Caddy certificates: Stored in
data/caddy-data/
- Shared certificates: Copied to
data/shared-certs/
for dnsdist access
All data persists between container restarts and updates.
All services (live):
docker compose logs -f
Individual services:
docker compose logs dns-pihole
docker compose logs dns-caddy
docker compose logs dns-dnsdist
The stack includes built-in health checks and restart policies to ensure services stay running.
This project is built with these excellent open-source tools:
- Pi-hole – Network-level DNS filtering and management
- dnsdist – High-performance DNS load balancer with DoH/DoT support
- Caddy – Modern web server with automatic HTTPS
Issues, questions, and feature requests are welcome! Please open an issue or start a discussion in this repository.
Need custom configurations? I can help with:
- Custom ACLs and fine-grained access control
- Advanced dnsdist load-balancing strategies
- Conditional forwarding rules
- Integration with upstream/external resolvers
- Enterprise-grade scaling and security hardening
📬 Contact: allenbenny038@gmail.com