Block ads on your whole network with Blocky on a Raspberry Pi

Set up Blocky on a Raspberry Pi as your network DNS resolver. Blocks ads and trackers for every device on your LAN, including phones and TVs that can't run blockers.

last verified 2026-05-04 · difficulty 3/5 · Self-host ·Half a day
  • A Raspberry Pi 3, 4, or 5 with Ethernet. Wired only; a DNS box on WiFi is asking for trouble.
  • microSD card, 8 GB or larger.
  • Raspberry Pi Imager on your desktop.
  • An Ethernet cable.
  • A USB-C (Pi 4/5) or micro-USB (Pi 3) power supply.
  • Admin access to your home router so you can change the DHCP DNS setting.
  • Comfort with a terminal and SSH.

Blocky is a single Go binary that does network-wide DNS filtering. No database, no web UI, one YAML config file. It speaks DNS-over-HTTPS and DNS-over-TLS to upstreams natively, starts in under a second, and idles around 30 MB of RAM. Pi-hole has a web UI; Blocky has a YAML file. Different tastes.

This guide walks through a fresh install on a Raspberry Pi running Raspberry Pi OS Lite. The end state: every device on your home network resolves DNS through the Pi, with ads and trackers blocked at the resolver. If you want a web dashboard or per-client rules, Pi-hole and AdGuard Home are reasonable alternatives. This guide is about Blocky.

A used Pi 3 works fine for a household. A Pi 4 is overkill for DNS but you probably have one.

Step 1: Flash Raspberry Pi OS Lite

In Raspberry Pi Imager:

  1. Choose your device (Pi 3 / 4 / 5).
  2. Choose OS: Raspberry Pi OS Lite (64-bit). The Lite image has no desktop, which is what you want.
  3. Choose your SD card.
  4. Click Next, then Edit Settings (the cog icon). Configure before flashing:
    • Hostname: something memorable like dnspi.
    • Enable SSH and paste your public key. Skip password auth.
    • Username: pick whatever, e.g. pi.
    • Skip WiFi configuration. This box runs on Ethernet.
    • Set your locale and timezone.
  5. Write and let verification finish.

Slot the card into the Pi, plug in the Ethernet, power on.

Step 2: Find the Pi and SSH in

The Pi will pick up a DHCP address. Find it from your router’s DHCP lease list, or from your desktop:

# Adjust the subnet to match your LAN
nmap -sn 192.168.1.0/24 | grep -B2 -i raspberry

SSH in:

ssh pi@<pi-dhcp-ip>

If the first connection hangs for a minute, the Pi may still be expanding the filesystem on first boot. Give it two minutes.

Step 3: Set a static IP

A DNS server with a moving IP is a DNS server that breaks every time the lease rotates. Pin it.

Recent Raspberry Pi OS releases (Bookworm and Trixie) use NetworkManager. Older images (Bullseye and earlier) use dhcpcd. Check which one is running:

systemctl is-active NetworkManager

NetworkManager (Bookworm, Trixie, current default)

Find the connection name, then modify it. The default name is usually Wired connection 1 or netplan-eth0:

nmcli con show

# Replace the connection name and IPs to match your LAN
sudo nmcli con mod "Wired connection 1" \
  ipv4.addresses 192.168.1.50/24 \
  ipv4.gateway 192.168.1.1 \
  ipv4.dns 9.9.9.9 \
  ipv4.method manual

sudo nmcli con up "Wired connection 1"

dhcpcd (older images)

Edit /etc/dhcpcd.conf and add at the bottom:

interface eth0
static ip_address=192.168.1.50/24
static routers=192.168.1.1
static domain_name_servers=9.9.9.9

Then sudo systemctl restart dhcpcd.

Either way, verify and reconnect:

ip addr show eth0
ping -c 3 1.1.1.1

Reconnect SSH on the new static IP. From here on, this is “the Pi’s IP”.

Step 4: Install Blocky

Blocky ships as a single binary. No package manager involved.

Check the latest release at github.com/0xERR0R/blocky/releases and set the version. As of this guide’s verification date, the latest is v0.29.0.

BLOCKY_VERSION="v0.29.0"   # check the releases page for the current tag
ARCH="arm64"                # Pi 3/4/5 with the 64-bit OS

curl -L -o /tmp/blocky.tar.gz \
  "https://github.com/0xERR0R/blocky/releases/download/${BLOCKY_VERSION}/blocky_${BLOCKY_VERSION}_Linux_${ARCH}.tar.gz"

tar -xzf /tmp/blocky.tar.gz -C /tmp
sudo mv /tmp/blocky /usr/local/bin/blocky
sudo chmod +x /usr/local/bin/blocky

blocky version

Create the config directory:

sudo mkdir -p /etc/blocky

Step 5: Write the config

sudo nano /etc/blocky/config.yml

A reasonable starter config. Comments inline so each block is obvious:

# Upstream resolvers. Blocky speaks DoH and DoT natively.
# Pick resolvers you trust. Common options: Quad9, Mullvad,
# Cloudflare 1.1.1.1, NextDNS, your VPN provider's resolver.
upstreams:
  groups:
    default:
      # Quad9: Swiss-based, DNSSEC-validating, no logging
      - https://dns10.quad9.net/dns-query
      # Cloudflare: fast, widely available
      - https://1.1.1.1/dns-query

# Plain DNS used only at startup, to resolve the DoH hostnames
# above before Blocky itself is ready to answer queries.
bootstrapDns:
  - 9.9.9.9
  - 1.1.1.1

blocking:
  # One or more lists of domains to block. Hosts-file format
  # works; so do plain domain lists. StevenBlack's hosts file
  # is the standard starting point.
  denylists:
    ads:
      - https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts

  # Domains to never block, even if a denylist contains them.
  # Add entries when something legitimate breaks. The pipe
  # syntax lets you list domains inline.
  allowlists:
    ads:
      - |
        example-payment-provider.com

  # Apply the "ads" group to all clients.
  clientGroupsBlock:
    default:
      - ads

  # Refresh blocklists every 4 hours. Cached locally so a
  # missing upstream at boot doesn't take blocking down.
  loading:
    refreshPeriod: 4h
    downloads:
      timeout: 60s
      cooldown: 2s
    maxErrorsPerSource: 5

ports:
  dns: 53
  http: 4000   # API and Prometheus metrics

log:
  level: info
  format: text

# Optional: Prometheus metrics at http://<pi-ip>:4000/metrics
prometheus:
  enable: true
  path: /metrics

Replace example-payment-provider.com with a domain you actually need to allow, or remove the allowlist block entirely if nothing breaks. You can come back to it.

Step 6: Run Blocky as a service

sudo nano /etc/systemd/system/blocky.service
[Unit]
Description=Blocky DNS
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/local/bin/blocky --config /etc/blocky/config.yml
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

By default this runs as root, which is what lets it bind port 53. If you’d rather run as an unprivileged user, grant the binary the capability to bind low ports and add a User= line:

sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/blocky
# Then add:  User=pi   under [Service] in the unit file

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable blocky
sudo systemctl start blocky
sudo systemctl status blocky

If the unit fails immediately, check the pitfalls section below.

Step 7: Test before changing the network

Test against the Pi directly from your desktop, before changing anything on the router. This way, if something is wrong, only your single test query fails, not every device in the house.

# Replace 192.168.1.50 with the Pi's IP

# Should be blocked (returns 0.0.0.0)
nslookup doubleclick.net 192.168.1.50

# Should resolve normally
nslookup github.com 192.168.1.50

# If you added an allowlist entry, it should resolve
nslookup example-payment-provider.com 192.168.1.50

Tail the live log on the Pi while you test:

sudo journalctl -u blocky -f

You should see each query land, with blocked domains marked accordingly.

Step 8: Point the network at the Pi

Once Blocky answers correctly, change your router’s DHCP DNS setting to the Pi’s IP.

The exact path depends on your router. Common locations:

  • LAN settings or DHCP server section.
  • A field labelled DNS server, Primary DNS, or DNS Name Server.
  • On UniFi: Settings then Networks, edit each network, DHCP Name Server.
  • On a typical ISP router: LAN then DHCP, look for a “DNS override” toggle.

Set the primary DNS to the Pi’s IP. Save. If your router lets you set a secondary DNS, leave it blank (see the pitfalls section).

Devices pick up the new DNS on their next DHCP lease renewal. To force it on a desktop:

# Linux
sudo dhclient -r && sudo dhclient

# macOS
sudo ipconfig set en0 BOOTP && sudo ipconfig set en0 DHCP

# Or just toggle WiFi off and on

On Linux, confirm with resolvectl status | grep "DNS Servers". The Pi’s IP should appear.

Ongoing maintenance

# Recent queries
sudo journalctl -u blocky --since "1 hour ago"

# Restart after config changes
sudo systemctl restart blocky

# Force a blocklist refresh without restarting
curl -X POST http://localhost:4000/api/lists/refresh

To update Blocky, repeat Step 4 with a new BLOCKY_VERSION, then sudo systemctl restart blocky. Read the release notes first; config keys occasionally change between minor versions.

Optional: query log to a file

If you want a record of queries without setting up Prometheus and Grafana, Blocky can write a CSV:

# Add to /etc/blocky/config.yml
queryLog:
  type: csv
  target: /var/log/blocky/
  logRetentionDays: 7
sudo mkdir -p /var/log/blocky
sudo chown pi:pi /var/log/blocky   # match the user Blocky runs as
sudo systemctl restart blocky

A week of CSV is enough to spot a noisy device or a dead blocklist without filling the SD card.

  • From a device using the Pi as DNS, nslookup doubleclick.net returns 0.0.0.0 (blocked).
  • nslookup github.com resolves normally.
  • sudo journalctl -u blocky -f shows queries arriving from your desktop's IP.
  • Optional: curl http://<pi-ip>:4000/metrics | grep blocky_query reports activity if Prometheus is enabled.
  • Port 53 already in use

    systemd-resolved listens on 53 by default on some images. systemctl status blocky shows "bind: address already in use". Fix: edit /etc/systemd/resolved.conf, set DNSStubListener=no, then sudo systemctl restart systemd-resolved && sudo systemctl restart blocky. If systemd-resolved isn't strictly needed, sudo systemctl disable --now systemd-resolved.

  • Router only accepts one DNS

    Set the Pi as primary and leave secondary blank. A "fallback DNS" is "silently bypass your blocklist whenever the primary is 5 ms slow", which is most of the time. Better to have DNS go down loudly for an hour than to have ads come back without you noticing.

  • Allowlist entry not taking effect

    Blocky reloads denylists and allowlists on the schedule in loading.refreshPeriod, but config changes need a full restart: sudo systemctl restart blocky. Also check that the allowlist domain is in the same group name as the denylist (both under ads: in the starter config).

  • SD card died

    It happens. SD cards are not great long-term storage for a DNS box that writes logs constantly. Keep /etc/blocky/config.yml checked into a git repo somewhere so a reflash takes thirty minutes, not three hours.

  • A specific app or device can't reach the internet

    Some smart TVs and game consoles hardcode 8.8.8.8 and bypass your DNS entirely. Not a Blocky problem, but worth knowing. To force them through Blocky, configure your router to NAT outbound port 53 traffic back to the Pi.

If you want to undo this:

  1. In the router, change the DHCP DNS back to whatever it was before. If you don't know, 1.1.1.1 (Cloudflare) or your ISP-provided value will work.
  2. On the Pi: sudo systemctl stop blocky && sudo systemctl disable blocky.
  3. Force a DHCP renew on a device and confirm DNS now points elsewhere with resolvectl status (or your platform's equivalent).

The Pi keeps running but does nothing on the network. Wipe the SD card whenever you're done with it.

For a less-drastic rollback, leave the Pi running and just point the router's DHCP DNS elsewhere. You can flip back in two minutes.


Did this work for you?


Comments (0)

No comments yet. Be the first.

Add a comment