How to Self-Host Blocky with Docker
What Is Blocky?
Blocky is a DNS proxy and ad blocker written in Go. It sits between your devices and upstream DNS servers, blocking ads, trackers, and malware domains at the DNS level. Unlike Pi-hole or AdGuard Home, Blocky has no web UI by default — it’s configured entirely through a YAML file, making it ideal for infrastructure-as-code setups. It supports DNS-over-HTTPS (DoH), DNS-over-TLS (DoT), conditional forwarding, client-specific rules, and caching out of the box. It replaces cloud DNS services like Google DNS, Cloudflare DNS, and NextDNS.
Prerequisites
- A Linux server (Ubuntu 22.04+ recommended)
- Docker and Docker Compose installed (guide)
- 64 MB of free RAM (Blocky is extremely lightweight)
- Port 53 available (not used by another DNS resolver — check with
sudo lsof -i :53) - Basic familiarity with YAML configuration
Docker Compose Configuration
Create a project directory:
mkdir -p /opt/blocky && cd /opt/blocky
Create a docker-compose.yml file:
services:
blocky:
image: spx01/blocky:v0.28.2
container_name: blocky
restart: unless-stopped
ports:
- "53:53/tcp"
- "53:53/udp"
- "4000:4000/tcp" # HTTP API / Prometheus metrics
volumes:
- ./config.yml:/app/config.yml:ro
environment:
- TZ=America/New_York # Change to your timezone
healthcheck:
test: ["/app/blocky", "healthcheck"]
interval: 30s
timeout: 3s
start_period: 60s
retries: 3
Create a config.yml file alongside the Compose file:
# Upstream DNS resolvers — queries go here when not blocked
upstreams:
groups:
default:
- 1.1.1.1 # Cloudflare
- 8.8.8.8 # Google
- 9.9.9.9 # Quad9
- tcp-tls:1.1.1.1:853 # Cloudflare DNS-over-TLS
strategy: parallel_best # Query all, use fastest response
timeout: 2s
# Ad/tracker blocking
blocking:
denylists:
ads:
- https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
- https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt
- https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt
- https://raw.githubusercontent.com/hagezi/dns-blocklists/main/hosts/pro.txt
allowlists:
ads: []
clientGroupsBlock:
default:
- ads
blockType: zeroIp # Return 0.0.0.0 for blocked domains
blockTTL: 1m
loading:
refreshPeriod: 24h # Refresh blocklists daily
downloads:
timeout: 60s
attempts: 3
cooldown: 10s
strategy: failOnError
# DNS response caching
caching:
minTime: 5m
maxTime: 30m
prefetching: true # Preload frequently queried domains
prefetchExpires: 2h
prefetchThreshold: 5
cacheTimeNegative: 30m # Cache NXDOMAIN responses
# Custom DNS entries (optional — for local network devices)
# customDNS:
# mapping:
# printer.lan: 192.168.1.100
# nas.lan: 192.168.1.50
# Conditional forwarding (optional — resolve local hostnames via your router)
# conditional:
# mapping:
# lan: 192.168.1.1 # Forward .lan queries to your router
# 168.192.in-addr.arpa: 192.168.1.1 # Reverse DNS for local IPs
# Ports
ports:
dns: 53 # Standard DNS
http: 4000 # HTTP API, Prometheus metrics
# Logging
log:
level: info
format: text
timestamp: true
privacy: false # Set to true to anonymize client IPs in logs
Start the stack:
docker compose up -d
Initial Setup
- Verify Blocky is running:
docker compose logs blocky
Look for "blocky is up and running" in the output.
- Test DNS resolution:
# Should resolve normally
dig @127.0.0.1 google.com
# Should be blocked (returns 0.0.0.0)
dig @127.0.0.1 ads.google.com
- Point your devices to Blocky — set your router’s DNS to the server’s IP address, or change DNS on individual devices. See our guide to replacing Google DNS for detailed instructions.
Configuration
All configuration is in config.yml. After making changes, restart the container:
docker compose restart blocky
Adding More Blocklists
Add URLs to the denylists section. Popular lists:
blocking:
denylists:
ads:
- https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
- https://raw.githubusercontent.com/hagezi/dns-blocklists/main/hosts/pro.txt
malware:
- https://raw.githubusercontent.com/hagezi/dns-blocklists/main/hosts/tif.txt
adult:
- https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/porn/hosts
clientGroupsBlock:
default:
- ads
- malware
kids-tablet:
- ads
- malware
- adult
Per-Client Rules
Apply different blocklists to different clients by IP or hostname:
blocking:
clientGroupsBlock:
default:
- ads
192.168.1.100:
- ads
- malware
kids-*:
- ads
- malware
- adult
Allowlisting Domains
If a domain is falsely blocked:
blocking:
allowlists:
ads:
- |
# Inline allowlist
example.com
*.example.com
Advanced Configuration (Optional)
Encrypted DNS Upstream
Use DNS-over-TLS or DNS-over-HTTPS for upstream queries:
upstreams:
groups:
default:
- tcp-tls:1.1.1.1:853 # Cloudflare DoT
- tcp-tls:dns.quad9.net:853 # Quad9 DoT
- https://dns.cloudflare.com/dns-query # Cloudflare DoH
Prometheus Metrics
Blocky exposes metrics on the HTTP port:
prometheus:
enable: true
path: /metrics
Access at http://your-server:4000/metrics. Pair with Grafana for dashboards — the Blocky project provides a Grafana dashboard template.
DNSSEC Validation (v0.28+)
dnssec:
validate: true
maxChainDepth: 10
Query Logging to Database
For persistent query logs beyond container restarts:
queryLog:
type: postgresql
target: postgres://user:password@db:5432/blocky_logs
logRetentionDays: 30
Reverse Proxy
The HTTP API on port 4000 can be proxied for remote management. Only expose this internally — it has no authentication.
For DNS, no reverse proxy is needed — clients connect directly on port 53.
See our reverse proxy setup guide.
Backup
Blocky is stateless — all configuration is in config.yml. To back up:
- Back up
config.ymlanddocker-compose.yml - That’s it — there’s no database or persistent state to back up
If you’re using query logging to a database, back up that database separately.
See our backup strategy guide.
Troubleshooting
Port 53 already in use
Symptom: Container fails to start with “address already in use” on port 53.
Fix: On Ubuntu, systemd-resolved listens on port 53 by default. Disable it:
sudo systemctl disable systemd-resolved
sudo systemctl stop systemd-resolved
sudo rm /etc/resolv.conf
echo "nameserver 1.1.1.1" | sudo tee /etc/resolv.conf
Blocklists not loading
Symptom: Ads aren’t being blocked. Logs show download failures. Fix: Check if the blocklist URLs are accessible from inside the container:
docker compose exec blocky wget -q -O /dev/null https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
If blocked by your network, add bootstrap DNS:
bootstrapDns:
- tcp+udp:1.1.1.1
DNS resolution slow after switching
Symptom: Websites load slower after pointing DNS to Blocky.
Fix: The first query for each domain takes longer (upstream resolution). Subsequent queries are cached. Enable prefetching for frequently used domains. If latency is still high, check your upstream resolvers — try switching to Cloudflare (1.1.1.1) or Google (8.8.8.8).
No web UI?
Symptom: You expected a web dashboard but Blocky doesn’t have one. Fix: Blocky is intentionally UI-less — configure via YAML. If you want a web UI, use the HTTP API (port 4000) or pair with Grafana + Prometheus. If you need a built-in UI, consider AdGuard Home or Pi-hole.
Resource Requirements
- RAM: 30-50 MB idle, ~100 MB with large blocklists loaded
- CPU: Minimal — handles thousands of queries per second on a single core
- Disk: Negligible (stateless — config file only)
Blocky is the most lightweight DNS ad blocker available. It runs comfortably on a Raspberry Pi Zero.
Verdict
Blocky is the best DNS ad blocker for infrastructure-as-code enthusiasts who prefer YAML configuration over web UIs. It’s absurdly lightweight, supports encrypted DNS natively, and the per-client filtering is more flexible than Pi-hole’s. If you want a web dashboard for monitoring and configuration, use AdGuard Home instead. If you want the largest community and most third-party integrations, use Pi-hole. But if you’re comfortable with config files and want the smallest, fastest DNS blocker possible, Blocky is the right choice.
FAQ
Can Blocky replace Pi-hole?
Yes. Blocky does everything Pi-hole does (DNS proxying, ad blocking, caching, conditional forwarding) without the web UI. It adds native DoH/DoT support that Pi-hole requires add-ons for.
Does Blocky support DNS-over-HTTPS?
Yes, both as a client (querying upstream via DoH) and as a server (serving DoH to your devices on the HTTPS port).
How do I see what’s being blocked?
Check the container logs (docker compose logs -f blocky) or enable Prometheus metrics and view them in Grafana. The HTTP API also provides query/blocking statistics at http://your-server:4000/api/.
Related
Get self-hosting tips in your inbox
New guides, comparisons, and setup tutorials — delivered weekly. No spam.