Zum Hauptinhalt springen
S-EDV news
← Alle Anleitungen
📘 Anleitung Server & Netzwerk 22.10.2026 · 11 min Lesezeit

Blocky mit Docker Compose: schlanker DNS-Werbeblocker per YAML

Blocky filtert Werbung und Tracker für das ganze Netz per DNS und wird komplett über eine YAML-Datei konfiguriert. Die Anleitung zeigt die getestete Einrichtung mit Docker Compose, den Port-53-Konflikt mit systemd-resolved, eigene Denylists und Allowlists, Steuerung per REST-API, Prometheus-Metriken, Backup mit echtem Restore und den Update-Weg.

WerbelinksMit * markierte Links sind Werbelinks: Bei einem Kauf erhalten wir eine Provision, der Preis bleibt gleich. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Mehr dazu

Titelbild der Anleitung: Schriftzug Blocky DNS Werbeblocker, daneben Netzwerkgrafik mit Geräten, DNS-Filter und gestoppten Werbeanfragen

Blocky ist ein schlanker DNS-Proxy mit Werbe- und Trackingfilter, geschrieben in Go. Anders als Pi-hole hat Blocky keine Weboberfläche: Konfiguriert wird per YAML-Datei, gesteuert per REST-API und CLI, Kennzahlen liefert ein Prometheus-Endpunkt. Das passt zu Admins, die ihren DNS-Filter versionieren und ins vorhandene Monitoring einhängen wollen. Alle Befehle und Ausgaben stammen aus einem Testlauf mit Blocky v0.34.0 und dem anschließenden Update auf v0.35.0, das aktuelle Release vom 5. September 2026.

Voraussetzungen

Blocky ist sparsam: Mit rund 71.500 Sperreinträgen meldete die Go-Laufzeit im Startlog 27 MB Speicher.

  • Linux-Server oder Mini-PC mit Docker Engine und Docker Compose v2, x86_64 oder ARM (das Image v0.35.0 gibt es für amd64, arm64, armv7 und armv6)
  • 1 CPU-Kern und 512 MB freier Arbeitsspeicher genügen für Blocky allein, 1 GB Speicherplatz für Image, Listen-Cache und Abfrageprotokoll
  • eine feste IPv4-Adresse im LAN, im Beispiel 192.168.1.10, damit Router und Clients den DNS-Server dauerhaft finden
  • Zugriff auf den DHCP-Server, um später die LAN-IP von Blocky als einzigen DNS-Server zu verteilen (kein zweiter, ungefilterter Server, sonst umgehen Clients den Filter)
  • ausgehende Verbindungen auf Port 53, 853 (DNS over TLS) und 443 (DNS over HTTPS und Listen-Downloads)
  • zum Prüfen dig aus dem Paket dnsutils (Debian/Ubuntu) bzw. bind-tools (Alpine)

Schritt 1: Projektordner und Port 53 prüfen

Legen Sie einen Ordner für den Stack an und prüfen Sie vorher, wer auf Port 53 lauscht. Auf Debian- und Ubuntu-Systemen mit systemd-resolved ist das der lokale Stub-Resolver:

mkdir -p /opt/blocky/lists && cd /opt/blocky
sudo ss -lunpt | grep ':53 ' 

Im Test sah die Ausgabe so aus:

udp UNCONN 0 0 127.0.0.53%lo:53  0.0.0.0:* users:(("systemd-resolve",pid=154,fd=18))
tcp LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=154,fd=19))

systemd-resolved belegt nur Loopback-Adressen. Ein Mapping 53:53 ohne IP bindet aber an 0.0.0.0 und scheitert. Binden Sie den DNS-Port deshalb an die LAN-IP; systemd-resolved kann bleiben.

Verifizieren: ss -lunpt zeigt auf Port 53 nur Adressen aus 127.0.0.0/8 oder gar nichts.

Schritt 2: Compose-Datei und .env anlegen

Neben latest gibt es die Tags development und main mit unveröffentlichtem Code. Schreiben Sie die Version deshalb in der .env fest:

BLOCKY_TAG=v0.35.0
DNS_IP=192.168.1.10

Die compose.yaml:

services:
  blocky:
    image: spx01/blocky:${BLOCKY_TAG}
    container_name: blocky
    restart: unless-stopped
    ports:
      - "${DNS_IP}:53:53/tcp"
      - "${DNS_IP}:53:53/udp"
      - "4000:4000/tcp"
    environment:
      - TZ=Europe/Berlin
    volumes:
      - ./config.yml:/app/config.yml:ro
      - ./lists:/app/lists:ro
      - blocky_logs:/logs
      - blocky_cache:/app/cache
volumes:
  blocky_logs:
  blocky_cache:
EckdatenWert
Imagespx01/blocky:v0.35.0 (alternativ ghcr.io/0xerr0r/blocky)
Ports53/udp und 53/tcp (DNS), 4000/tcp (REST-API, Prometheus)
Volumes./config.yml (nur lesend), ./lists (eigene Listen), blocky_logs (Abfrageprotokoll), blocky_cache (Listen-Cache)
UmgebungTZ für Zeitstempel, optional BLOCKY_CONFIG_FILE (Standard /app/config.yml)
Benutzer im ContainerUID 100, Healthcheck im Image enthalten

/logs und /app/cache gehören im Image bereits UID 100, benannte Volumes brauchen daher keine Rechteanpassung.

Verifizieren: docker compose config -q gibt nichts aus und endet mit Exit-Code 0. Die Variablen aus der .env erscheinen in docker compose config eingesetzt.

Schritt 3: config.yml mit Sperr- und Ausnahmelisten

Basis ist das Minimalbeispiel der Dokumentation, ergänzt um eigene Listen, Metriken und ein SQLite-Abfrageprotokoll:

upstreams:
  groups:
    default:
      - 46.182.19.48
      - 80.241.218.68
      - tcp-tls:fdns1.dismail.de:853
      - https://dns.digitale-gesellschaft.ch/dns-query
blocking:
  denylists:
    ads:
      - https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
      - /app/lists/deny.txt
  allowlists:
    ads:
      - /app/lists/allow.txt
  clientGroupsBlock:
    default:
      - ads
  blockType: zeroIP
  loading:
    refreshPeriod: 4h
    downloads:
      cachePath: /app/cache
ports:
  dns: 53
  http: 4000
prometheus:
  enable: true
statistics:
  enable: true
queryLog:
  type: sqlite
  target: /logs/querylog.db
  logRetentionDays: 7
log:
  level: info

Listen werden in Gruppen definiert (hier ads), und erst clientGroupsBlock ordnet diese Gruppen Clients zu. Fehlt diese Zuordnung, haben die Listen laut Dokumentation keine Wirkung. Clients ohne eigene Zuordnung landen in default. Ausnahmen haben Vorrang: Steht eine Domain in Sperr- und Ausnahmeliste, wird sie aufgelöst.

Die beiden eigenen Listen legen Sie im Ordner lists an. Unterstützt werden Hosts-Format, eine Domain pro Zeile, Wildcards und reguläre Ausdrücke zwischen Schrägstrichen:

printf '# eigene Sperrliste\n*.example.net\n' > lists/deny.txt
printf '# eigene Ausnahmen\nwww.example.net\n' > lists/allow.txt

Der Eintrag *.example.net sperrt die Domain samt aller Subdomains, die Ausnahme gibt www.example.net wieder frei. So sehen Sie beim Testen sofort, ob beide Mechanismen greifen. Die Upstream-Server stammen aus dem Dokumentationsbeispiel (dismail.de und Digitale Gesellschaft, Schweiz); Mit der Standardstrategie parallel_best fragt Blocky laut Dokumentation je Anfrage zwei zufällig gewichtete Upstreams aus der Gruppe parallel und nimmt die schnellere Antwort; das verteilt Ihre DNS-Anfragen auf mehrere Anbieter.

Der Validator des Images prüft die Datei, ohne den Dienst zu starten:

docker compose run --rm --no-deps blocky validate --config /app/config.yml

Verifizieren: Der Validator meldet keinen Fehler. Bei Tippfehlern erscheint die Zeilennummer, siehe Troubleshooting.

Schritt 4: Starten und Filter prüfen

docker compose up -d
docker compose ps
docker compose logs --no-log-prefix | grep -E "Version:|entries|up and running"

Im Test meldete docker compose ps nach wenigen Sekunden Up 15 seconds (healthy). Die Logzeilen bestätigen Version, geladene Listen und Listener:

INFO _/  Version: v0.34.0            Build time: 20260727-201920     _/
INFO server:        ads: 71505 entries
INFO server:        ads: 1 entries
INFO server: http server is up and running on addr/port [::]:4000
INFO server: TCP server is up and running on address :53
INFO server: UDP server is up and running on address :53

Nun der eigentliche Funktionstest von einem Client im LAN:

for d in s-edv.com doubleclick.net ads.example.net www.example.net; do
  echo "== $d"; dig +short @192.168.1.10 $d A
done
== s-edv.com
152.53.177.79
== doubleclick.net
0.0.0.0
== ads.example.net
0.0.0.0
== www.example.net
172.66.175.59
104.20.21.8

Die erlaubte Domain wird normal aufgelöst, doubleclick.net aus der StevenBlack-Liste und ads.example.net aus der eigenen Wildcard-Liste liefern 0.0.0.0 (Blocktyp zeroIP), die Ausnahme www.example.net kommt trotz Wildcard durch. Gesperrte Antworten trugen eine TTL von 21600 Sekunden; wer Freigaben schneller wirksam haben möchte, senkt blocking.blockTTL.

Verifizieren: Gesperrte Domains liefern 0.0.0.0, freigegebene Domains echte Adressen, und docker compose ps zeigt healthy.

Schritt 5: Steuerung per REST-API und CLI

Ohne Weboberfläche erfolgt die Bedienung über HTTP auf Port 4000 oder über das Binary im Container.

curl -s http://192.168.1.10:4000/api/blocking/status
curl -s "http://192.168.1.10:4000/api/blocking/disable?duration=2m"
curl -s http://192.168.1.10:4000/api/blocking/status
curl -s http://192.168.1.10:4000/api/blocking/enable
{"enabled":true}
{"autoEnableInSec":119,"disabledGroups":["ads","default"],"enabled":false}

Während der Pause lieferte doubleclick.net im Test echte Google-Adressen, nach enable wieder 0.0.0.0. duration schaltet den Filter automatisch wieder ein. Nach Änderungen an den Listen laden Sie diese ohne Neustart neu:

curl -s -X POST http://192.168.1.10:4000/api/lists/refresh
curl -s -X POST http://192.168.1.10:4000/api/cache/flush

Beide antworteten mit HTTP 200. Die CLI zeigt, warum eine Domain gesperrt ist:

docker exec blocky ./blocky query doubleclick.net
INFO 	reason:        BLOCKED (ads: doubleclick.net)
INFO 	response type:              BLOCKED
INFO 	response:               A (0.0.0.0)
INFO 	return code:                NOERROR

Sicherheitshinweis: Die API hat keine Authentifizierung. Jeder, der Port 4000 erreicht, kann den Filter abschalten. Beschränken Sie Port 4000 per Firewall auf Admin-Rechner und Prometheus und geben Sie ihn nie ins Internet frei.

Verifizieren: /api/blocking/status liefert {"enabled":true}, und ./blocky query nennt für gesperrte Domains den Grund BLOCKED samt Gruppe.

Schritt 6: Prometheus-Metriken und Statistik

Mit prometheus.enable: true stellt Blocky Kennzahlen unter /metrics bereit, statistics.enable: true aktiviert zusätzlich eine JSON-Übersicht der letzten 24 Stunden:

curl -s http://192.168.1.10:4000/metrics | grep -E "^blocky_(blocking_enabled|denylist_cache_entries|build_info)"
curl -s http://192.168.1.10:4000/api/stats | jq -c .summary
blocky_blocking_enabled 1
blocky_build_info{build_time="20260727-201920",version="v0.34.0"} 1
blocky_denylist_cache_entries{group="ads"} 71505
{"avgResponseMs":16,"blocked":5,"cacheHitRate":0,"cached":0,"dropped":0,"errors":0,"filtered":0,"forwarded":4,"local":0,"queries":9}

Als Alarm eignen sich blocky_blocking_enabled == 0 und blocky_denylist_cache_entries nahe null (gescheiterte Downloads).

Verifizieren: /metrics enthält blocky_build_info mit der erwarteten Version, /api/stats liefert JSON statt HTTP 503.

Schritt 7: Backup und Restore

Bei Blocky ist der wichtigste Teil des Backups klein: compose.yaml, .env, config.yml und der Ordner lists. Das SQLite-Abfrageprotokoll im Volume blocky_logs ist optional, enthält aber die Historie für Auswertungen. Die Datenbank läuft im WAL-Modus (drei Dateien), deshalb stoppen Sie den Container für die Sicherung kurz. Den Listen-Cache müssen Sie nicht sichern.

Im Test stand vorher eine Marker-Domain restore-marker.example.org in lists/deny.txt (gesperrt), das Protokoll enthielt 9 Einträge. Die Sicherung:

cd /opt/blocky
docker compose stop
mkdir -p /srv/backup
tar czf /srv/backup/blocky-config-$(date +%F).tar.gz compose.yaml .env config.yml lists
docker run --rm -v blocky_blocky_logs:/logs:ro -v /srv/backup:/backup alpine:3.22 \
  tar czf /backup/blocky-logs-$(date +%F).tar.gz -C /logs .
docker compose start

Den Volume-Namen (Projektordner plus Volume) zeigt docker volume ls. Danach der Ernstfall, also kompletter Verlust von Stack, Volumes und Ordner:

docker compose down -v
cd /opt && rm -rf blocky

Wiederherstellung: Ordner aus dem Archiv zurückholen, Container und Volumes mit docker compose create anlegen, ohne zu starten, Protokoll in das neue Volume entpacken, Eigentümer auf UID 100 setzen, starten:

mkdir -p /opt/blocky && cd /opt/blocky
tar xzf /srv/backup/blocky-config-2026-10-22.tar.gz
docker compose create
docker run --rm -v blocky_blocky_logs:/logs -v /srv/backup:/backup:ro alpine:3.22 \
  sh -c "tar xzf /backup/blocky-logs-2026-10-22.tar.gz -C /logs && chown -R 100:100 /logs"
docker compose start

docker compose create ist hier bewusst gewählt, denn ein per docker volume create angelegtes Volume trägt keine Compose-Labels. Nach dem Restore lieferte der Test dasselbe Bild wie vorher:

doubleclick.net: 0.0.0.0
ads.example.net: 0.0.0.0
www.example.net: 104.20.21.8
restore-marker.example.org: 0.0.0.0

Die Marker-Domain war wieder gesperrt (BLOCKED (ads: restore-marker.example.org)), die Ausnahme griff, und das Abfrageprotokoll enthielt wieder alle 9 Einträge vom Zeitpunkt der Sicherung.

Verifizieren: docker compose ps zeigt healthy, eine eigene Marker-Domain ist gesperrt, eine Ausnahme wird aufgelöst, und sqlite3 "file:querylog.db?mode=ro" "select count(*) from log_entries;" im Volume liefert die erwartete Zahl.

Schritt 8: Update auf eine neue Version

Durch den festen Tag in der .env ist ein Update eine bewusste Entscheidung. Sichern Sie vorher wie in Schritt 7. Im Test lief der Stack zunächst mit v0.34.0 und wurde auf v0.35.0 gehoben:

sed -i 's/^BLOCKY_TAG=.*/BLOCKY_TAG=v0.35.0/' .env
docker compose pull
docker compose up -d
docker compose logs --no-log-prefix | grep "Version:" | tail -1
INFO _/  Version: v0.35.0            Build time: 20260905-163623     _/

Auch blocky_build_info meldete danach version="v0.35.0". Sperrungen, Ausnahme und Marker-Domain verhielten sich unverändert, das Abfrageprotokoll blieb erhalten. Für ein Zurückrollen setzen Sie den alten Tag wieder ein und führen erneut docker compose up -d aus.

Verifizieren: Logbanner und blocky_build_info zeigen die neue Version, der Container ist healthy, und der Filtertest aus Schritt 4 liefert dieselben Ergebnisse.

Troubleshooting

address already in use auf Port 53: Mit einem Mapping "53:53/udp" ohne IP brach der Start im Test so ab:

Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint blocky (...): failed to bind host port 0.0.0.0:53/tcp: address already in use

Ursache war systemd-resolved auf 127.0.0.53. Binden Sie den Port an die LAN-IP wie in Schritt 2. Ein Abschalten von systemd-resolved ist nicht nötig.

YAML-Fehler, Container startet ständig neu: Ein fehlender Doppelpunkt nach denylists ergab im Test einen Neustart-Kreislauf (Restarting (1)) mit dieser Meldung:

Error: unable to load configuration file '/app/config.yml': failed to unmarshal config from /app/config.yml: wrong file structure: yaml: line 12: mapping values are not allowed in this context

Gemeldet wurde Zeile 12, der fehlende Doppelpunkt stand zwei Zeilen darüber. Der Validator aus Schritt 3 lieferte vorab exakt dieselbe Meldung; prüfen Sie Änderungen also vor dem Neustart.

Upstream nicht erreichbar: Mit einem absichtlich toten Upstream (192.0.2.1) startete Blocky zwar und blieb healthy, warnte aber schon beim Start:

WARN upstream: initial resolver test failed error=test query to example.com failed: all 3 attempts to resolve via upstream tcp+udp:192.0.2.1 failed: ... i/o timeout

Erlaubte Domains liefen danach auf status: SERVFAIL, im Log stand ERROR error on processing request:query resolution failed. Gesperrte Domains wurden weiter mit 0.0.0.0 beantwortet, weil Blocky dafür keinen Upstream fragt. Der grüne Healthcheck ist also kein Beleg für funktionierende Namensauflösung. Prüfen Sie ausgehende Firewall-Regeln für 53, 853 und 443.

Häufige Fragen

Brauche ich eine Weboberfläche?

Für den Betrieb nicht. Die tägliche Bedienung beschränkt sich auf Pausieren und Listenpflege, beides geht per API oder CLI. Sollen Kollegen ohne Kommandozeile Ausnahmen pflegen, sind Pi-hole oder AdGuard Home bequemer.

Kann ich verschiedene Regeln für verschiedene Geräte festlegen?

Ja. clientGroupsBlock akzeptiert laut Dokumentation neben default auch Client-Namen mit Wildcards, IP-Adressen und Subnetze in CIDR-Schreibweise.

Ist das Abfrageprotokoll datenschutzrechtlich problematisch?

Es enthält, welcher Client wann welche Domain aufgerufen hat. Die Dokumentation weist ausdrücklich darauf hin, Nutzer zu informieren. Im Unternehmen beziehen Sie Betriebsrat und Datenschutzbeauftragten ein und halten logRetentionDays kurz oder setzen queryLog.type: none.

Fazit

Blocky ist der DNS-Filter für Admins, die Konfiguration lieber in Dateien als in Oberflächen pflegen. Eine YAML-Datei, ein Container, eine API: schnell eingerichtet, leicht zu sichern. Stolpersteine im Test waren der belegte Port 53, ein YAML-Fehler mit Neustart-Kreislauf und ein grüner Healthcheck trotz totem Upstream. Wer das kennt, die API abschirmt und die Version festschreibt, bekommt einen sehr sparsamen Filter für das ganze Netz.

Weiterführende Anleitungen und Quellen

BlockyDNSWerbeblockerDocker ComposePrometheusSelfhosting