0xERR0R/blocky: a self-hosted DNS proxy and ad-blocker you configure in YAML
Fast and lightweight DNS proxy as ad-blocker for local network with many features
At a glance
- What is it?
- blocky is a Go DNS proxy that filters queries against external allow and denylists, serves DoH, DoT, DoQ and DoH3, and keeps no database. It fits homelabs and small networks; it is the wrong tool if you want a web UI to click through or a long-term query analytics product.
- Who is it for?
- Adopt blocky if you already run a DNS resolver for a home or small office network, you are comfortable editing YAML, and you want per-client blocking groups with DoH, DoT, DoQ and DoH3 on the same process. Do not adopt it if you need a graphical admin console, per-device policy managed from a browser, or a long-term query analytics product; blocky's query logging writes to CSV or a SQL database, but the interface is config and API, not a dashboard.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What blocky actually replaces on your network
Most ad-blocking DNS setups are one of two things: a resolver with a web dashboard bolted on, or a hosts file you keep editing. blocky sits between them. It is a DNS proxy, so it does not answer queries from root servers itself; it receives queries from clients on your LAN, decides whether to block, forward, or answer from cache, and forwards the rest to upstream resolvers you name.
The audience is narrow and specific. You need a machine that stays on, a router or DHCP server whose DNS setting you control, and a willingness to edit YAML. In exchange you get filtering that is defined per client group, so a kids' group and a smart-home group can have different allow and denylists, different upstream resolvers, and different caching behavior from the same process. That per-group split is the feature that separates blocky from a single global blocklist, and it is the reason the configuration file is longer than a typical ad-blocker's.
The README is explicit that blocky does not collect user data, telemetry or statistics. That is a design claim rather than a feature you can toggle, and it is worth reading alongside the query logging options: logging is something you turn on and point at CSV or a database, not something that happens by default.
The query path: lists, groups, cache, upstreams
blocky's filtering is list-driven. You point it at external allow and denylists, and the documentation states it reloads those lists periodically, which matters because ad and malware lists change and you do not want to restart the process to pick up changes. Matching supports regex, so a single rule can cover a family of hostnames instead of enumerating them.
Blocking is not limited to the requested name. The README lists three inspection points: the request domain, the CNAME in the response (it calls this deep CNAME inspection), and response IP addresses checked against IP lists. The third one is the interesting case. A tracker that resolves through a CNAME to a known ad host is caught at the response stage even though the original query looked clean. The cost is that response-stage blocking requires blocky to see and evaluate the answer, which adds work on the resolution path that a request-only filter never does.
Upstream resolution is configurable per client group, and the README describes two related behaviors: custom DNS resolution for certain domain names, and conditional forwarding to an external DNS server. Conditional forwarding is what you use when an internal domain such as a corporate or homelab zone must go to a specific resolver rather than out to the internet. Caching sits in front of all of this, with prefetching of frequently used queries, and the README states that multiple external resolvers are used simultaneously. It also says upstream resolvers are chosen at random from the configured set, which distributes DNS traffic across providers rather than pinning it to one.
The protocol surface is wider than most self-hosted blockers. UDP and TCP DNS, DoH, DoT, DoQ per RFC 9250, and DoH3 per RFC 9114 are all listed. blocky also exposes a DoH endpoint of its own, so clients that speak DoH can point at blocky rather than at a public resolver. The go.mod confirms the dependency set behind this: miekg/dns for DNS handling and quic-go for the QUIC-based transports.
Installing blocky: the Docker image and the build path
The README's quick start does not give install commands. It says: "You can jump to Installation chapter in the documentation." The documentation lives at https://0xERR0R.github.io/blocky/, and the installation chapter is at https://0xerr0r.github.io/blocky/latest/installation/. That is where the project says to get it, so there is no command to copy here.
What the repository does establish is how the published image is produced. The Dockerfile starts from golang:alpine in a build stage pinned to the native build platform, sets CGO_ENABLED=0, and cross-compiles to the target architecture with GOOS and GOARCH, so the build never runs under emulation for the arm targets. The final stage is FROM scratch, which means the shipped image contains the binary and its data files and nothing else. There is no shell inside it.
The image name is fixed in the Makefile as DOCKER_IMAGE_NAME=spx01/blocky, and the README's Docker pulls badge points at hub.docker.com/r/spx01/blocky. The README also states the image has multi-arch support and that blocky supports x86-64, ARM and MIPS architectures. If you build from source instead, the Makefile's default build target is make build, and the binary name is blocky.
The README also lists a community supported Helm chart for k8s deployment, without naming it. If you are deploying to Kubernetes, that is the path the project points at rather than a raw container run.
One operational detail from the Dockerfile is worth knowing before you mount anything. It creates a /seed-dir directory in the build stage and copies it into the final stage so that a fresh named volume mounted over an image directory inherits that directory's ownership. The comment in the file explains the intent: this makes a volume such as -v blocky_cache:/app/cache writable by the unprivileged container user without any host-side setup. If you add persistence for cache or lists, mount it over a directory the image already created, or you will be fixing permissions yourself.
Where blocky gets in your way
The stateless design is the source of most of the friction. The README lists statelessness as a feature: no database, no temporary files, single binary. The consequence is that anything you want to persist, you must configure explicitly. Query logging to CSV or to MySQL, MariaDB, PostgreSQL or Timescale is available, but it is opt-in and it is a separate moving part from the proxy itself. If you want history, you are now running a database next to a process whose selling point was not needing one.
Configuration errors are the second failure mode. There is no web UI to validate a rule, so a malformed regex or a typo in a group name surfaces as behavior rather than as a red field in a form. The repository ships a JSON schema (the go.mod includes invopop/jsonschema and santhosh-tekuri/jsonschema), which suggests editor-side validation is possible, but that is a different experience from a console that shows you what a rule will match.
Third, the container is built from scratch. There is no shell inside the final image, so you cannot exec in and poke around when a list fails to download. Debugging happens through logs and through the host, not inside the container.
Finally, blocky is the wrong tool if your network's DNS is managed by someone else. If you do not control the resolver setting handed out by DHCP, blocky will sit there answering queries that never arrive. It is also a poor fit if you want per-device policy configured from a browser by a non-technical person; the per-client group model is powerful, but it lives in YAML.
How blocky differs from Pi-hole
Pi-hole is the obvious comparison, and the difference is architectural rather than a matter of feature checklists. Pi-hole is built around a web administration interface and a database-backed query log; you install it, open a browser, and manage blocklists and clients from there. blocky has no such interface. Its configuration is one or more YAML files, and the README presents that as an advantage: simple to maintain, simple to backup.
The protocol support diverges too. The README lists DoQ (RFC 9250) and DoH3 (RFC 9114) among blocky's supported transports, alongside DoH and DoT. blocky can also serve as a DoH endpoint for clients. If your reason for running a local resolver is to move clients off plaintext DNS to an encrypted transport that your router does not otherwise offer, that transport list is the deciding factor.
The third difference is the per-client group model. In blocky, a client group carries its own allow and denylists and its own upstream resolvers, defined in the same config file. That is a configuration-first way to separate a kids' network from an IoT VLAN. Pi-hole's model is oriented around its admin UI and its own client management. Neither is better in the abstract; they suit different operators. If you would rather not maintain a config file in version control, blocky's approach will feel like overhead. If you would rather not maintain a web application and its database, Pi-hole's will.
Maintenance, releases and the Apache-2.0 licence
The last push to the default branch was on 2026-09-09, and the most recent release listed is v0.35.0 on 2026-09-05, preceded by v0.34.0 on 2026-07-27 and v0.33.0 on 2026-06-30. The cadence over that window is roughly monthly. The repository is not archived.
Upgrade cost is low by design. The project is stateless, so there is no schema migration to run when you move to a new version, and the release artifacts include a multi-arch Docker image and a single binary. The realistic upgrade steps are pulling a new image or replacing a binary and restarting. The thing to watch is configuration compatibility: YAML keys can change meaning or move between releases, and the release notes are where that is recorded. The repository ships a JSON schema, so validating your config file against the schema for the version you are about to run is a cheaper check than discovering a renamed key from a startup failure.
blocky is licensed under Apache-2.0, and the Dockerfile labels the image with org.opencontainers.image.licenses="Apache-2.0". Apache-2.0 is a permissive licence that includes an explicit patent grant and requires that notices be preserved. It permits commercial use and modification. It is not a copyleft licence, so it does not oblige you to publish changes you make. If you redistribute blocky or a modified build inside a product, the notice and attribution obligations apply; that is a question for your own legal review, not something this article can settle. The project also accepts sponsorship through several channels, which has no bearing on the licence terms.
Editorial conclusion
Adopt blocky if you already run a DNS resolver for a home or small office network, you are comfortable editing YAML, and you want per-client blocking groups with DoH, DoT, DoQ and DoH3 on the same process. Do not adopt it if you need a graphical admin console, per-device policy managed from a browser, or a long-term query analytics product; blocky's query logging writes to CSV or a SQL database, but the interface is config and API, not a dashboard. Before you commit, verify three things in your own environment: that your router or DHCP server can hand out the host running blocky as the only resolver, that the upstream resolvers you list actually answer with the protocols you configured, and that the external blocklist URLs you reference are reachable from the container, which runs on a scratch base image with no shell to debug from.
Frequently asked questions
What is blocky?
blocky is a DNS proxy and ad-blocker for local networks, written in Go and licensed under Apache-2.0. It filters queries against external allow and denylists, supports per-client groups, and speaks DNS over UDP, TCP, DoH, DoT, DoQ and DoH3.
How do I install blocky?
The README's quick start links to the installation chapter of the documentation at 0xERR0R.github.io/blocky. The project publishes a multi-arch Docker image named spx01/blocky and a single binary, and the README states blocky is stateless, so there is no database to set up.
Does blocky need a database to run?
No. The README lists statelessness as a feature: no database and no temporary files. Query logging to CSV or to MySQL, MariaDB, PostgreSQL or Timescale is available, but it is optional and separate from the proxy itself.
Which DNS protocols does blocky support?
The README lists DNS over UDP and TCP, DNS over HTTPS (DoH), DNS over TLS (DoT), DNS over QUIC (DoQ, RFC 9250) and DNS over HTTPS/3 (DoH3, RFC 9114). It also states that blocky provides a DoH endpoint of its own.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/0xerr0r-blocky)