Migrating from Pi-hole to Blocky on Kubernetes

Published January 13, 2026 · 5 min read

I've been using Pi-hole for blocking ads for a long time. Well not only ads, but also other stuff I don't want on my network like phishing, malware and tracking. I had Pi-hole running on my NAS and with all the updates to several services running on it (including Pi-hole), the NAS was not keeping up. Monitoring would notify me on failed DNS attempts during backups. I was looking to move services away from the NAS, but then other stuff would also need improving.

Why I block ads, and the other stuff

Let's start with the simple ones, why would I want to block malware, viruses, phishing and tracking? Well because these things are actively harming my family or systems, because that is what they do. When I would walk down a street, I wouldn't want a drone following me, pickpockets and needles infecting me with diseases. So these were simple.

Ad blocking... Partially ad blocking is the only way to block the tracking that is usually bundled as a single package. But since Linus Tech Tips is telling me that Ad-blocking is piracy . I should probably have a couple of stronger arguments (although this is a post about improving my adblocker)

  1. Ads make the web almost unusable. We are always glad when we are back home after holidays, because most pages have 80% of the attention going to the advertisements. Having full screen banners or a half a minute of advertisement per 3 minutes of other content, is just a terrible deal.
  2. Most ads don't add anything to the world. I could argue that having ads makes products worse, because it more about being top of mind and not about having a good product. This will shift budget from product quality and development into marketing.
  3. It's my data-bundle and computer/phone. Thereof I can show whatever I want (within the limits of the law) and no-one should be able to force me to consume anything.
  4. It makes me spend more than I like. We even do our shopping at a store with minimal advertising and sales. It's usually more expensive per product, but it saves us a lot due to not buying impulse purchases.

Requirements

Must haves

  1. Support blocklists - the best way to include domains that were not on my watchlist
  2. Reloading of blocklists - a way to keep the unwanted domains up to date
  3. Custom DNS resolving - Used to route some services internally
  4. Very fast - Due to having a secondary external DNS, my solution should be faster to have it work. Pi-hole was struggling when the system had any load.

Should haves:

  1. Way to turn blocking off - Because things break and I don't want to jump in with the highest priority to fix it.
  2. Debug blocked domains
  3. Lightweight - I'm going to run this on a Raspberry pi alongside a lot of other services.

Nice to haves:

  1. Stateless - It's going to a Kubernetes cluster and changing things in code vs an interface with a database and backups is just a much nicer way to handle.
  2. Statistics/Dashboards - Because... (Blocky doesn't have this out of the box, it exposes metrics via Prometheus)

What is Blocky?

Blocky is a DNS proxy and ad-blocker for local networks written in Go. It's fast, lightweight, and designed to run in containerized environments. Unlike Pi-hole, Blocky is stateless and configured entirely through YAML files, making it perfect for Kubernetes deployments.

Implementation

Deployment YAML
apiVersion: apps/v1
kind: Deployment
metadata:
  name: blocky
  namespace: blocky
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: blocky
  template:
    metadata:
      labels:
        app.kubernetes.io/name: blocky
    spec:
      containers:
      - name: blocky
        image: ghcr.io/0xerr0r/blocky:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 53
          name: dns-tcp
          protocol: TCP
        - containerPort: 53
          name: dns-udp
          protocol: UDP
        - containerPort: 4000
          name: http
          protocol: TCP
        livenessProbe:
          httpGet:
            path: /
            port: 4000
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /
            port: 4000
          initialDelaySeconds: 10
          periodSeconds: 5
        volumeMounts:
        - mountPath: /app/config.yml
          name: config
          readOnly: true
          subPath: config.yml
      volumes:
      - name: config
        configMap:
          name: blocky-config
Services YAML
apiVersion: v1
kind: Service
metadata:
  name: blocky-dns
  namespace: blocky
  annotations:
    metallb.universe.tf/allow-shared-ip: blocky-dns
spec:
  type: LoadBalancer
  loadBalancerIP: ... # static MetalLB ip - used for router and VPN
  externalTrafficPolicy: Local
  selector:
    app.kubernetes.io/name: blocky
  ports:
  - name: dns-tcp
    port: 53
    protocol: TCP
    targetPort: 53
  - name: dns-udp
    port: 53
    protocol: UDP
    targetPort: 53
---
apiVersion: v1
kind: Service
metadata:
  name: blocky-http
  namespace: blocky
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: blocky
  ports:
  - name: http
    port: 4000
    protocol: TCP
    targetPort: 4000
Configuration YAML
apiVersion: v1
kind: ConfigMap
metadata:
  name: blocky-config
  namespace: blocky
data:
  config.yml: |
    blocking:
      allowlists:
        ads: # Allow umami webpage and my own umami page
        - |
          umami.is
          umami.eeql.nl
      clientGroupsBlock:
        .../24: # VPN clients
        - ads
        - streaming
        .../32: # Work computer
        - ads
        - streaming
        default:
        - ads
      denylists:
        ads:
        - https://big.oisd.nl/domainswild
        - https://nsfw.oisd.nl/domainswild
        - |
          graph.facebook.com
          api.k8slens.dev
        streaming:
        - |
          *.netflix.com
          *.hbomax.com
          *.disneyplus.com
          *.disney.go.com
          *.youtube.com
          *.primevideo.com
      loading:
        downloads:
          cooldown: 120s
        refreshPeriod: 24h
    caching:
      minTime: 5m
      prefetching: true
    customDNS:
      customTTL: 30m
      filterUnmappedTypes: true
      mapping:
        '*.entwood.eeql.nl': ...
        grocy.eeql.nl: ...
        ha.eeql.nl: ...
    filtering:
      queryTypes:
      - AAAA # I don't have IPv6
    ports:
      dns: 53
      http: 4000
    prometheus:
      enable: true
      path: /metrics
    upstreams:
      groups:
        default:
        - ...
        - ...
      init:
        strategy: fast # Make sure that dns is responsive ASAP due to also being used for VPN

Share

Share on Mastodon - Share on LinkedIn - Share on Facebook