Network-wide ad blocking and local DNS running on your NAS. Automated deployment, configuration as code, and router integration.
“I should buy a Raspberry Pi for this…” - Before realizing you already have a server running 24/7
Why Synology Instead of a Pi
Ran Pi-hole on a Raspberry Pi 3 for two years. SD card died. Lost my entire config - custom blocklists, local DNS overrides, 50+ whitelisted domains I’d tuned over time. Spent an evening rebuilding from memory.
My Synology was sitting there running 24/7 with RAID, automated backups, and Docker already installed. Why was I maintaining a separate device that could fail?
Moved Pi-hole to the Synology. Same functionality, no dedicated hardware, and actual backups.
Architecture Decision: macvlan + Shim
Pi-hole needs port 53 (DNS). Synology DSM might want port 53 for its own services. Using host networking works but ties you to the NAS’s IP and complicates port management.
macvlan gives Pi-hole its own dedicated IP on your LAN. Clean separation. Pi-hole gets its own IP, Synology keeps its management IP.
The catch: by design, the host can’t communicate with macvlan containers directly. This means the Synology itself can’t use Pi-hole for DNS, which breaks:
- DSM’s own DNS resolution
- Other Docker containers needing DNS
- Accessing Pi-hole’s web UI by hostname from the NAS
The solution: a macvlan shim. A small network interface on the host that bridges the gap. Took me an afternoon to figure out. Works perfectly.
Repo Structure
Everything’s in Git for reproducibility:
pihole-synology-docker/
├── docker-compose.yml # Pi-hole container with macvlan
├── .env.example # Password and timezone template
├── deploy.sh # Automated deployment from workstation
├── verify.sh # Comprehensive health checks
├── update-router-dns.sh # Router DHCP automation (template)
├── macvlan-shim.sh # Solves host ↔ container networking
├── apply-config.sh # Push config changes to running instance
├── backup.sh # Automated backup (cron-ready)
└── config/
├── adlists.csv # Curated blocklists with tiers
├── whitelist.txt # Pre-emptive false positive fixes
└── 99-custom.conf # Local DNS + conditional forwarding
Configuration as code. Destroy and rebuild Pi-hole in 5 minutes.
Full source: pihole-synology-docker
Docker Compose Configuration
Pi-hole gets its own IP via macvlan:
services:
pihole:
container_name: pihole
image: pihole/pihole:latest
restart: unless-stopped
hostname: pihole
networks:
pihole_net:
# ════════════════════════════════════════════════════════════
# CONFIGURATION: Set your Pi-hole's IP address here
# ════════════════════════════════════════════════════════════
# Pick an unused IP on your LAN (outside DHCP range).
# Example: 192.168.1.53 (matching DNS port)
ipv4_address: 192.168.1.53
environment:
TZ: ${TZ:-UTC}
FTLCONF_webserver_api_password: ${PIHOLE_PASSWORD:-}
FTLCONF_dns_upstreams: '1.1.1.1;1.0.0.1' # Cloudflare (change if preferred)
FTLCONF_dns_listeningMode: all
DNSMASQ_USER: root # Required on Synology — see pihole/docker-pi-hole#963
volumes:
- /volume1/docker/pihole/etc-pihole:/etc/pihole
- /volume1/docker/pihole/etc-dnsmasq.d:/etc/dnsmasq.d
cap_add:
- NET_ADMIN
dns:
- 127.0.0.1 # Pi-hole uses itself for DNS
- 1.1.1.1 # Fallback during container startup
networks:
pihole_net:
driver: macvlan
driver_opts:
# ════════════════════════════════════════════════════════════
# CONFIGURATION: Set your NAS's network interface here
# ════════════════════════════════════════════════════════════
# Check with: ssh your-nas "ip addr"
# Common values: eth0, eth1, bond0
parent: eth0
ipam:
config:
# ════════════════════════════════════════════════════════════
# CONFIGURATION: Set your LAN network details here
# ════════════════════════════════════════════════════════════
- subnet: 192.168.1.0/24 # Your LAN subnet
gateway: 192.168.1.1 # Your router/gateway IP
ip_range: 192.168.1.53/32 # Pi-hole's IP (must match ipv4_address above)
# /32 restricts Docker to exactly this one IP
Key configuration notes:
- DNSMASQ_USER: root - Critical for Synology. DSM has non-standard user permissions. Without this, Pi-hole can’t write to its volumes. I spent an hour debugging “permission denied” errors before finding this in a GitHub issue.
- dns: 127.0.0.1 - Pi-hole uses itself for DNS lookups. Prevents dependency loops during startup.
- ip_range: /32 - Restricts Docker to only this one IP. Without this, Docker can allocate additional IPs from your subnet, causing conflicts.
- parent interface - Must be your primary NIC. Check with
ip addron the Synology CLI.
Create .env file with your settings:
# .env
PIHOLE_PASSWORD=your-secure-password-here
TZ=America/New_York
The macvlan Shim Problem and Solution
Deploy the container and you’ll hit the problem immediately: your Synology can’t reach Pi-hole.
# From Synology CLI
ping <PIHOLE_IP>
# No response
macvlan isolation. By design, the host can’t talk to macvlan containers. This breaks:
- DSM DNS resolution
- Other containers needing DNS
- Accessing Pi-hole web UI by hostname
The fix: a network shim. Create a macvlan interface on the host that bridges the gap:
set -euo pipefail
# ═════════════════════════════════════════════════════════════════════
# CONFIGURATION (must match docker-compose.yml)
# ═════════════════════════════════════════════════════════════════════
PARENT_IF="eth0" # Your NAS's primary network interface
SHIM_IF="macvlan-shim"
SHIM_IP="192.168.1.200" # Unused LAN IP for the shim (must not conflict!)
PIHOLE_IP="192.168.1.53" # Pi-hole's macvlan IP (must match docker-compose.yml)
# ═════════════════════════════════════════════════════════════════════
start() {
echo "Creating macvlan shim: ${SHIM_IF} (${SHIM_IP}) → ${PIHOLE_IP}"
# Remove existing shim if present (idempotent)
ip link del "$SHIM_IF" 2>/dev/null || true
# Create macvlan shim on the same parent as the Pi-hole container
ip link add "$SHIM_IF" link "$PARENT_IF" type macvlan mode bridge
ip addr add "${SHIM_IP}/32" dev "$SHIM_IF"
ip link set "$SHIM_IF" up
# Route Pi-hole traffic through the shim
ip route add "${PIHOLE_IP}/32" dev "$SHIM_IF"
echo "Done. Synology can now reach Pi-hole at ${PIHOLE_IP}"
}
Run this after starting the container. Now the Synology can reach Pi-hole.
Make it persist across reboots:
Copy the script to DSM’s boot hooks:
sudo cp macvlan-shim.sh /usr/local/etc/rc.d/
sudo chmod 755 /usr/local/etc/rc.d/macvlan-shim.sh
DSM runs scripts in /usr/local/etc/rc.d/ on boot. Your shim survives reboots.
Automated Deployment (Recommended)
The repo includes deploy.sh - runs from your workstation over SSH:
./deploy.sh
What it does:
- Preflight checks (SSH, Container Manager running)
- Creates data directories on NAS
- Prompts for password and timezone
- Copies all files to NAS
- Pulls image and starts container
- Activates macvlan shim
- Persists shim in rc.d for reboots
Output shows each step with colored status. Takes ~2 minutes depending on image pull.
# ─────────────────────────────────────────────
step "1/7" "Preflight checks"
# ─────────────────────────────────────────────
info "Testing SSH to ${NAS_HOST}..."
ssh -o ConnectTimeout=5 -o BatchMode=yes "${NAS_HOST}" "true" 2>/dev/null \
|| fail "Cannot SSH to ${NAS_HOST}. Check key auth and connectivity."
ok "SSH connection"
Verify everything works:
./verify.sh
Checks:
- Container running without restart loops
- DNS resolution working
- Ad domains blocked
- Web UI reachable
- macvlan shim active
All green? You’re ready for network-wide deployment.
Configuration Management
The repo includes curated config files. Apply them with apply-config.sh:
./apply-config.sh
Blocklists (config/adlists.csv)
Three tiers defined in config/adlists.csv:
# ──────────────────────────────────────────────────────────────
# Essential — start here
# ──────────────────────────────────────────────────────────────
# StevenBlack unified hosts — ads + malware (Pi-hole default)
https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts,1,StevenBlack unified hosts (default)
# OISD Big — one of the most popular all-in-one lists
# Curated from 50+ sources, aggressive dedup, actively maintained
https://big.oisd.nl/domainswild2,1,OISD Big — comprehensive all-in-one
# HaGeZi Multi Pro — fast-growing, excellent maintenance
# Blocks ads, tracking, metrics, telemetry, phishing, malware
https://raw.githubusercontent.com/hagezi/dns-blocklists/main/domains/pro.txt,1,HaGeZi Multi Pro — ads/tracking/malware
Essential (enabled by default):
- StevenBlack unified hosts (~130k domains)
- OISD Big (~1.5M domains, well-maintained)
- HaGeZi Multi Pro (ads/tracking/malware)
Recommended (enabled):
- Firebog curated lists (AdGuard, EasyList, EasyPrivacy)
- Malware/phishing protection (DandelionSprout, DigitalSide)
Aggressive (disabled by default):
- HaGeZi Ultimate (very aggressive, expect breakage)
- OISD NSFW (adult content filter)
I started with all tiers enabled. Broke multiple shopping sites. Spent an evening debugging. Now I use Essential + Recommended. Works for 99% of use cases.
Whitelist (config/whitelist.txt)
Pre-emptive fixes for common false positives. The repo includes ~100 domains that aggressive blocklists often catch:
# ──────────────────────────────────────────────────────────────
# Microsoft — login, updates, services
# ──────────────────────────────────────────────────────────────
login.microsoftonline.com
login.live.com
outlook.office365.com
products.office.com
c.s-microsoft.com
i.s-microsoft.com
dl.delivery.mp.microsoft.com
geo-prod.do.dsp.mp.microsoft.com
displaycatalog.mp.microsoft.com
sls.update.microsoft.com.akadns.net
fe3cr.delivery.mp.microsoft.com
Categories covered:
- Microsoft login/updates
- Apple services and captive portal
- Google safe browsing and fonts
- Amazon/Alexa
- Streaming (Netflix, Spotify, YouTube)
- Gaming (Steam, PlayStation, Xbox)
- Samsung Smart TV services
This is the collective pain of community testing. These domains get blocked by overzealous lists and break things.
Local DNS (config/99-custom.conf)
Add local hostname resolution via dnsmasq using config/99-custom.conf:
# ──────────────────────────────────────────────────────────────
# Local DNS records — homelab devices
# ──────────────────────────────────────────────────────────────
# Faster than going through the router's DNS. Edit to match your hosts.
#
# Format: host-record=hostname,ip[,ipv6][,ttl]
# address=/hostname/ip (wildcards supported)
# Examples (uncomment and customize):
# host-record=nas.lan,192.168.1.2
# host-record=pihole.lan,192.168.1.53
# host-record=router.lan,192.168.1.1
# Wildcard for Kubernetes ingress or other homelab services
# Example: *.homelab.lan → Traefik ingress controller
# address=/.homelab.lan/192.168.1.100
# Example: Specific services
# host-record=plex.homelab,192.168.1.101
Also includes conditional forwarding - sends reverse DNS lookups to your router so local hostnames resolve properly.
Apply configuration:
./apply-config.sh # Apply everything
./apply-config.sh --adlists # Only update blocklists
./apply-config.sh --dry-run # Preview changes
Router Integration
Manual DHCP configuration works, but automation is better. The repo includes update-router-dns.sh - a template for EdgeRouter/UniFi/VyOS routers:
./update-router-dns.sh
# ─────────────────────────────────────────────
step "1/4" "Preflight"
# ─────────────────────────────────────────────
info "Testing SSH to ${ROUTER_HOST}..."
ssh -o ConnectTimeout=5 -o BatchMode=yes "${ROUTER_HOST}" "true" 2>/dev/null \
|| fail "Cannot SSH to ${ROUTER_HOST}. Check key auth and connectivity."
ok "SSH connection"
info "Testing Pi-hole DNS before cutting over..."
if dig +short +timeout=5 +tries=1 @"${PIHOLE_IP}" google.com >/dev/null 2>&1; then
ok "Pi-hole is resolving queries at ${PIHOLE_IP}"
else
fail "Pi-hole is NOT responding at ${PIHOLE_IP}. Run ./verify.sh first."
fi
What it does:
- Tests Pi-hole before cutover
- Shows current DHCP DNS config
- Updates all DHCP scopes to advertise Pi-hole
- Commits and saves router config
- Shows commands for forcing client DHCP renewal
Adapt this template for your router. The pattern works anywhere - SSH in, change DHCP config, commit.
Backup Strategy
Automated backups via backup.sh:
# Cron (daily at 3am):
# 0 3 * * * /volume1/docker/pihole/backup.sh 2>&1 | logger -t pihole-backup
Run manually to test:
sudo /volume1/docker/pihole/backup.sh --verbose
Each backup creates two artifacts:
- Teleporter export - Pi-hole’s built-in backup (settings, lists, DNS records)
- Volume snapshot - Full tar of config directories (nuclear restore option)
Keeps 14 days of history. Auto-rotates old backups.
Why two formats?
Teleporter is portable. Import it via web UI. Works across Pi-hole versions.
Volume snapshot is the nuclear option. Everything on disk. Use it when Teleporter fails or you need byte-for-byte restoration.
I’ve used both. Teleporter for config changes I regretted. Volume snapshot when I accidentally upgraded to a broken Pi-hole version.
Resource Usage
Tested on a 4-bay NAS (Celeron J-series, 8GB RAM):
- CPU: <1% idle, <5% during blocklist updates
- RAM: ~150MB
- Storage: ~500MB (container + config)
Query response times:
- Cached: <1ms
- Blocked: <1ms
- Forwarded to upstream: 10-20ms
Your network DNS is now faster than before. Blocked queries don’t leave your network. No round trip to an ad server just to get blocked.
Troubleshooting
macvlan Shim Not Working
Symptom: Synology can’t ping Pi-hole container.
ping <PIHOLE_IP>
# Destination Host Unreachable
Check:
# Is the shim interface up?
ip addr show macvlan-shim
# Does the route exist?
ip route | grep <PIHOLE_IP>
If missing, the rc.d script didn’t run. Check:
ls -l /usr/local/etc/rc.d/macvlan-shim.sh
# Should be executable (755) and owned by root
Manually run the shim script to test. If it works, reboot and verify it survives.
Container Won’t Start - Port Conflict
Symptom: Container exits immediately with port binding errors.
Check: DSM’s DNS Server package conflicts with port 53.
sudo netstat -tlnp | grep :53
If DSM DNS Server is running:
- Package Center → DNS Server → Stop
- Disable it permanently
macvlan should avoid this conflict (separate IP), but sometimes DSM binds to 0.0.0.0:53 which blocks everything.
Ads Still Showing
Symptom: Devices still get ads after cutover.
Debug from the device:
nslookup google.com
Look at Server: line. Should be your Pi-hole IP. If not:
- Router DHCP not updated - Verify router config, force DHCP renewal
- Device has static DNS - Check device network settings
- Device hardcodes DNS - Chromecast, IoT devices, some smart TVs bypass your DNS
For hardcoded DNS, you need router firewall rules to redirect DNS queries. Different battle.
Sites Broken After Aggressive Blocklists
Symptom: Shopping sites, login pages, or apps randomly fail.
Pi-hole admin → Query Log. Find the blocked domain causing the issue.
Quick fix:
# SSH to Synology
docker exec pihole pihole -w example.com
Or add to config/whitelist.txt and rerun ./apply-config.sh for version control.
Common false positives:
- Microsoft telemetry (breaks Windows Update sometimes)
- Apple metrics (breaks iCloud/App Store)
- CDN domains (breaks images/scripts on various sites)
The repo’s whitelist.txt has the most common ones pre-loaded.
DNS Slow After DSM Update
DSM updates can reset network settings. Verify:
# Container still running?
docker ps | grep pihole
# macvlan shim still active?
ip addr show macvlan-shim
# DSM DNS config?
cat /etc/resolv.conf
Fix as needed - restart container, re-run shim script, update DSM DNS settings (Control Panel → Network).
What I Learned
1. macvlan + Shim > Host Networking
Most guides use host networking because it’s simpler. But macvlan with a shim gives you:
- Port 53 cleanly separated from DSM
- Dedicated IP for Pi-hole (cleaner network architecture)
- No conflicts with Synology services
The shim isn’t obvious. But it solves the isolation problem permanently.
2. Configuration as Code Matters
First time I set up Pi-hole, I clicked through the web UI for an hour configuring everything. Lost it all when I upgraded DSM and the container got recreated.
Now everything’s in Git. Blocklists, whitelist, local DNS records. Lose the container? ./deploy.sh && ./apply-config.sh. Back online in 5 minutes.
3. Automated Backups Are Not Optional
I thought “it’s just DNS config, I’ll remember my settings.” Then I accidentally broke Pi-hole while testing regex rules. No backup. Rebuilt from memory. Missed half my whitelist.
Now backup.sh runs daily via cron. Two backup formats (Teleporter + volume snapshot). 14 days of history. I’ve used it twice. Worth the 10 minutes to set up.
4. Conservative Blocklists, Then Expand
I enabled all the aggressive lists on day one. Broke multiple sites immediately. Learned which domains need whitelisting the hard way.
Now I use Essential + Recommended tiers. Blocks ~95% of ads and tracking. Doesn’t break things. If you need more, enable Aggressive tier and deal with the breakage knowingly.
5. Local DNS Is the Killer Feature
I set this up for ad blocking. Ad blocking is great. But the real win? Local DNS records.
nas.homelab instead of remembering 192.168.x.y. *.homelab.lan wildcard for all my Kubernetes services. This alone justifies running Pi-hole.
What’s Next
You have network-wide ad blocking running on your NAS. Automated deployment. Version-controlled config. Actual backups. No dedicated hardware, no SD card failures.
Optional enhancements:
- Group management - Different blocklists for kids’ devices vs. adults
- Regex blacklisting - Block entire advertising networks with patterns
- Query analytics - See what your IoT devices are really phoning home about (prepare to be disturbed)
- Conditional forwarding - Better integration with local hostnames from DHCP
The core setup is production-ready. These are optimizations for when you’re bored and want to tinker.