Public DNS resolvers see every domain you look up. Running your own resolver on a RamNode VPS gives you a private, fast, filtering nameserver that you control end to end. This guide pairs two excellent open source tools:
- Blocky is a fast, lightweight DNS proxy written in Go. It handles ad and tracker blocking from external blocklists, per client groups, caching, custom DNS records, and metrics.
- Unbound is a validating, recursive, caching resolver from NLnet Labs. Instead of forwarding your queries to Google or Cloudflare, it walks the DNS hierarchy from the root servers down, performing DNSSEC validation along the way.
Chaining them means Blocky does the filtering and presents the user facing resolver, while Unbound does the actual recursion privately. No third party ever sees your full query stream.
Architecture
Clients --> Blocky :53 (filtering, caching) --> Unbound :5335 (recursive, DNSSEC) --> Root serversUnbound listens on a non standard local port so it does not collide with Blocky on port 53. Blocky uses Unbound as its single upstream.
Prerequisites
- A RamNode VPS running Ubuntu 24.04 LTS. DNS is light, so a small plan with 1 to 2 GB RAM is plenty.
- Root or sudo access.
- The VPS public IP. You will point your devices or network at this address.
A note on open resolvers
A DNS resolver exposed to the entire internet can be abused for amplification attacks. Do not allow the world to query your resolver. Either restrict access by source IP in the firewall to only your known networks, or front it with a VPN such as WireGuard and only let VPN clients reach port 53. This guide assumes you will lock down access; do not skip that step.
Step 1: Install and configure Unbound
sudo apt update
sudo apt install -y unbound unbound-anchor dnsutilsStop the service while you configure it:
sudo systemctl stop unboundCreate /etc/unbound/unbound.conf.d/recursive.conf:
server:
# Listen only on loopback, on a non standard port for Blocky to use
interface: 127.0.0.1
port: 5335
do-ip4: yes
do-ip6: no
do-udp: yes
do-tcp: yes
# Only localhost (Blocky) may query Unbound
access-control: 127.0.0.0/8 allow
access-control: 0.0.0.0/0 refuse
# Run as the unbound user
username: "unbound"
directory: "/etc/unbound"
# Performance and privacy
prefetch: yes
prefetch-key: yes
cache-min-ttl: 60
cache-max-ttl: 86400
msg-cache-size: 128m
rrset-cache-size: 256m
aggressive-nsec: yes
hide-identity: yes
hide-version: yes
qname-minimisation: yes
# Serve slightly stale records while refreshing, per RFC 8767
serve-expired: yes
serve-expired-ttl: 86400
# Logging
verbosity: 1
use-syslog: yesThe validator and iterator modules are enabled by default in Ubuntu's packaging, which is what gives you DNSSEC validation plus recursion. Validate the config:
sudo unbound-checkconfStart and enable Unbound:
sudo systemctl enable --now unboundTest it directly:
dig @127.0.0.1 -p 5335 example.comA successful answer with an ad flag on a signed domain confirms recursion and DNSSEC validation are working. Test validation explicitly against the deliberately broken test zone:
dig @127.0.0.1 -p 5335 dnssec-failed.orgThat should return SERVFAIL, which is correct: Unbound is refusing a record that fails validation.
Step 2: Free up port 53
Ubuntu runs systemd-resolved with a stub listener on port 53, which will conflict with Blocky. Point the system at Unbound and disable the stub. Edit /etc/systemd/resolved.conf:
[Resolve]
DNS=127.0.0.1:5335
DNSStubListener=noRestart and relink resolv.conf:
sudo systemctl restart systemd-resolved
sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.confConfirm nothing is still bound to port 53:
sudo ss -tulnp | grep :53Step 3: Install Blocky
Blocky ships as a single static binary. Create a user and directories:
sudo useradd -r -s /usr/sbin/nologin blocky
sudo mkdir -p /opt/blockyDownload the latest release for your architecture from the project's GitHub releases page, then place the binary:
cd /tmp
curl -fsSLO https://github.com/0xERR0R/blocky/releases/latest/download/blocky_Linux_x86_64.tar.gz
tar xzf blocky_Linux_x86_64.tar.gz
sudo mv blocky /opt/blocky/blocky
sudo chown root:root /opt/blocky/blocky
sudo chmod 755 /opt/blocky/blockyBecause Blocky binds privileged port 53 as a non root user, grant it the capability:
sudo setcap 'cap_net_bind_service=+ep' /opt/blocky/blockyStep 4: Configure Blocky
Create /opt/blocky/config.yml:
upstreams:
groups:
default:
# Point Blocky at our local Unbound resolver
- 127.0.0.1:5335
blocking:
denylists:
ads:
- https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
trackers:
- https://raw.githubusercontent.com/blocklistproject/Lists/master/tracking.txt
clientGroupsBlock:
default:
- ads
- trackers
caching:
minTime: 5m
maxTime: 30m
prefetching: true
# Optional: a local A record for an internal host
customDNS:
mapping:
nas.lan: 10.0.0.20
ports:
dns: 53
http: 4000
prometheus:
enable: true
log:
level: infoSet ownership:
sudo chown -R blocky:blocky /opt/blockyThe key schema points to know: upstream servers live under upstreams.groups, blocklists under blocking.denylists, and you map which groups receive which lists under blocking.clientGroupsBlock. Allowlists, if you add them, take precedence over denylists for the same domain.
Step 5: Create a systemd unit
Create /etc/systemd/system/blocky.service:
[Unit]
Description=Blocky DNS proxy
After=network.target unbound.service
Wants=unbound.service
[Service]
User=blocky
Group=blocky
ExecStart=/opt/blocky/blocky --config /opt/blocky/config.yml
Restart=on-failure
RestartSec=5
AmbientCapabilities=CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.targetEnable and start:
sudo systemctl daemon-reload
sudo systemctl enable --now blocky
sudo systemctl status blockyStep 6: Test the full chain
Query Blocky on port 53 locally:
dig @127.0.0.1 example.comConfirm blocking works by querying a known ad domain. It should return 0.0.0.0 or NXDOMAIN rather than a real address:
dig @127.0.0.1 doubleclick.netBlocky's REST API and metrics are on port 4000. Check that it is alive:
curl http://127.0.0.1:4000/api/blocking/statusStep 7: Lock down access with the firewall
This is the step that keeps you off open resolver blocklists. Allow DNS only from the networks you trust. Replace the example CIDR with your real client network or VPN subnet:
sudo ufw allow OpenSSH
sudo ufw allow from 203.0.113.0/24 to any port 53 proto udp
sudo ufw allow from 203.0.113.0/24 to any port 53 proto tcp
sudo ufw enableNever use sudo ufw allow 53 with no source restriction on a public VPS. The Blocky metrics port 4000 should stay bound to localhost or be restricted the same way.
Step 8: Point your devices at the resolver
On each client, or on your router for whole network coverage, set the primary DNS server to your RamNode VPS public IP. Verify from a client:
dig @your.vps.ip example.comMaintenance
- Refresh blocklists. Blocky reloads lists periodically on its own. You can force a refresh through the REST API or by restarting the service.
- Monitor. The Prometheus endpoint on port 4000 exposes query counts, cache hit rates, and block counts. Point Grafana at it for a live dashboard.
- Update Blocky. Download the new binary, replace
/opt/blocky/blocky, reapplysetcap, and restart the service. - Update Unbound root anchor. The DNSSEC trust anchor refreshes automatically through
unbound-anchor, but you can add a monthly systemd timer if you want belt and suspenders.
Troubleshooting
- Blocky fails to start on port 53. Something still holds the port, almost always the
systemd-resolvedstub. Recheck Step 2 andss -tulnp | grep :53. - Queries resolve but nothing is blocked. The blocklists failed to download. Check
journalctl -u blockyfor fetch errors, often a DNS bootstrap problem; add abootstrapDnsentry pointing at Unbound. - SERVFAIL on everything. Unbound is not reachable on 127.0.0.1:5335. Confirm it is running and listening with
ss -tulnp | grep 5335.
Wrap up
You now run a private, recursive, DNSSEC validating resolver with network wide ad and tracker blocking, entirely on your own RamNode VPS. Your DNS queries no longer flow through a third party, filtering happens before recursion, and the whole stack fits comfortably on a small instance. Combine it with WireGuard and you have a filtering resolver you can use securely from anywhere.
