Self-Hosted VPN/DNS: Pi-hole + WireGuard + Unbound (2025 Guide)
Introduction
This guide walks you through creating a self-hosted VPN server that combines network-level ad blocking, privacy-focused DNS resolution, and secure remote access. Unlike commercial VPN services (which often have questionable privacy practices), you'll have complete control over your infrastructure.
What You'll Build
A cloud-hosted server running:
- Pi-hole: Network-wide ad and tracker blocking
- Unbound: Recursive DNS resolver for enhanced privacy
- WireGuard: Modern, fast VPN protocol
Once configured, your devices connect to this server 24/7, giving you:
- Ad blocking across all apps and browsers
- Protection on untrusted networks (coffee shops, airports, hotels)
- Direct DNS queries to root nameservers (bypassing ISP and third-party DNS providers)
- Encrypted connections between your devices
Why This Matters
Security: Public WiFi networks are often compromised. Your encrypted VPN tunnel protects all traffic from eavesdropping and man-in-the-middle attacks.
Privacy: By running your own recursive DNS resolver, you eliminate third-party DNS providers (Google, Cloudflare, your ISP) from seeing your browsing patterns. Your queries go directly to authoritative nameservers.
Ad Blocking: Pi-hole blocks ads, trackers, and malware domains at the network level using curated blocklists. This works across all devices and apps, not just web browsers.
Censorship Circumvention: For users in regions with internet restrictions, this provides a trusted pathway to the open internet.
Family Safety: Parents can monitor and control internet access for children's devices through Pi-hole's logging and blocking features.
Prerequisites
- Basic Linux command line knowledge
- A cloud hosting account (recommendations below)
- Devices to connect (smartphones, laptops, tablets)
- SSH client for your operating system
- Approximately $5-10/month for hosting costs
Technical Requirements:
- Server: Minimum 1GB RAM, 1 vCPU, 10GB storage
- Recommended: 2GB RAM, 2 vCPU, 20GB storage (for 10+ devices)
- Operating System: Ubuntu 24.04 LTS
Choosing Your Cloud Provider
Any VPS (Virtual Private Server) provider works, but here are current recommendations:
Important Note About Streaming Services
Most cloud hosting providers have their IP ranges blocked by streaming services (Netflix, HBO Max, Disney+, etc.). If streaming access is important:
- Use a residential ISP connection (Raspberry Pi at home)
- Research providers specifically advertising streaming-friendly IPs
- Accept that streaming may not work through this VPN
For this guide, I assume you're primarily focused on security, privacy, and ad blocking rather than geo-unblocking streaming services.
Initial Server Setup
Create Your Server
- Sign up for your chosen cloud provider
- Create a new Ubuntu 24.04 LTS server
- Choose a data center location close to you for better latency
- Select at least 1GB RAM (2GB recommended)
- Add your SSH key during creation (highly recommended)
First Login and Updates
SSH into your server:
ssh root@your-server-ip
Update the system:
apt update && apt upgrade -y
apt autoremove -y
reboot
Wait 30 seconds, then reconnect via SSH.
Configure Firewall
Install and configure UFW (Uncomplicated Firewall):
apt install -y ufw
# Set default policies
ufw default deny incoming
ufw default allow outgoing
# Allow SSH (critical - don't lock yourself out)
ufw allow 22/tcp
# Allow WireGuard VPN
ufw allow 51820/udp
# Enable firewall
ufw enable
Verify the firewall is active:
ufw status verbose
Install Fail2Ban
Protect against brute-force SSH attacks:
apt install -y fail2ban
systemctl enable fail2ban
systemctl start fail2ban
Fail2Ban will automatically ban IP addresses after multiple failed SSH login attempts.
Configure Automatic Security Updates
Enable unattended upgrades for security patches:
apt install -y unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades
Select "Yes" when prompted to enable automatic updates.
Create a Non-Root User (Optional but Recommended)
If you're logged in as root:
adduser username
usermod -aG sudo username
Copy your SSH keys to the new user:
rsync --archive --chown=username:username ~/.ssh /home/username
Test SSH login with the new user before disabling root login:
ssh username@your-server-ip
Installing Pi-hole
Pi-hole provides network-level ad blocking and a DNS server with a web interface for management.
Installation
Pi-hole uses an automated installer:
apt install -y curl
curl -sSL https://install.pi-hole.net | bash
Installation Wizard
The installer will ask several questions:
- Upstream DNS Provider: Choose any option (we'll replace this with Unbound later). For now, select Google or Cloudflare.
- Blocklists: Accept the default blocklists (you can customize later).
- Admin Interface: Select "Yes" to install the web interface.
- Web Server: Select "Yes" to install lighttpd.
- Logging: Enable query logging if you want to monitor DNS requests.
- Privacy Mode: Choose your preferred privacy level (0 = show everything, 3 = anonymous).
The installer will complete and display your admin password. Save this password.
Set a Custom Admin Password
Change the randomly generated password to something memorable:
pihole -a -p
Enter your desired password when prompted.
Access the Admin Interface
Open a web browser and navigate to:
http://your-server-ip/admin
Login with the password you just set.
Initial Pi-hole Configuration
In the web interface:
- Navigate to Settings > DNS
- Note the current upstream DNS settings (we'll change these after installing Unbound)
- Explore the Dashboard to familiarize yourself with the interface
Installing Unbound
Unbound is a recursive DNS resolver that queries root nameservers directly, eliminating third-party DNS providers and enhancing privacy.
Installation
apt install -y unbound
Configuration
Create a new configuration file for Pi-hole integration:
nano /etc/unbound/unbound.conf.d/pi-hole.conf
Paste the following configuration:
server:
# Basic settings
verbosity: 0
interface: 127.0.0.1
port: 5335
do-ip4: yes
do-udp: yes
do-tcp: yes
# IPv6 support (disable if not needed)
do-ip6: no
# Performance settings
num-threads: 2
msg-cache-slabs: 2
rrset-cache-slabs: 2
infra-cache-slabs: 2
key-cache-slabs: 2
# Cache sizes (adjust based on your RAM)
rrset-cache-size: 256m
msg-cache-size: 128m
# Performance optimizations
so-rcvbuf: 1m
so-sndbuf: 1m
# Prefetch popular entries
prefetch: yes
prefetch-key: yes
# Aggressive NSEC caching for faster responses
aggressive-nsec: yes
# Privacy enhancements
qname-minimisation: yes
hide-identity: yes
hide-version: yes
# Security hardening
harden-glue: yes
harden-dnssec-stripped: yes
harden-below-nxdomain: yes
harden-referral-path: yes
use-caps-for-id: yes
# Access control
access-control: 127.0.0.1/32 allow
access-control: 0.0.0.0/0 refuse
# Root hints
root-hints: "/var/lib/unbound/root.hints"
# Trust anchor for DNSSEC
auto-trust-anchor-file: "/var/lib/unbound/root.key"
# Logging (disable for production)
logfile: ""
Save and exit (Ctrl+X, Y, Enter).
Download Root Hints
Download the current root hints file:
wget -O /var/lib/unbound/root.hints https://www.internic.net/domain/named.root
Restart Unbound
systemctl restart unbound
systemctl enable unbound
Test Unbound
Verify Unbound is working:
dig @127.0.0.1 -p 5335 google.com
You should see a successful DNS response. The first query will be slower as the cache is empty.
Configure Pi-hole to Use Unbound
- Open the Pi-hole admin interface
- Navigate to Settings > DNS
- Uncheck all upstream DNS servers
- In the Custom 1 (IPv4) field, enter:
127.0.0.1#5335 - Click Save at the bottom of the page
Verify the Configuration
In the Pi-hole admin interface, go to Tools > Query Log and make a test query:
nslookup example.com your-server-ip
You should see the query appear in Pi-hole's logs, and it should be resolved by your local Unbound instance.
Installing WireGuard with PiVPN
PiVPN automates WireGuard VPN setup and integrates seamlessly with Pi-hole.
Installation
curl -L https://install.pivpn.io | bash
Installation Wizard
The installer will guide you through configuration:
- Static IP Warning: Acknowledge that your cloud server has a static IP
-
User Selection: If logged in as root, create a standard user account when prompted. Use a simple username like
vpnuser. This user stores VPN configuration files. - Unattended Upgrades: Select "Yes" to enable automatic security updates
- VPN Protocol: Select WireGuard
- Port: Use the default port 51820 (we already opened this in UFW)
- DNS Provider: Select Pi-hole (it should auto-detect your Pi-hole installation)
- Public IP: Confirm your server's public IP address
- Reboot: Allow the installer to reboot when finished
After Reboot
Reconnect via SSH and verify PiVPN is working:
pivpn list
This should show "No clients found" since we haven't created any profiles yet.
Creating Client Profiles
Each device needs its own WireGuard configuration profile.
Create a Profile
Generate a configuration for your first device:
pivpn add -n devicename
Replace devicename with a descriptive name (maximum 15 characters):
iphone(your iPhone)laptop(your laptop)tablet(your tablet)work(work computer)
The configuration file is saved to /home/vpnuser/configs/devicename.conf
Create Multiple Profiles
Repeat for each device:
pivpn add -n iphone
pivpn add -n laptop
pivpn add -n tablet
List All Profiles
pivpn list
Generate QR Codes for Mobile Devices
For smartphones and tablets, generate a QR code:
pivpn -qr iphone
A QR code appears in the terminal that you can scan with the WireGuard mobile app.
Client Installation
iOS and Android
- Install the WireGuard app from the App Store or Google Play Store
- Open the app and tap the + button
- Select Create from QR code
-
Scan the QR code generated by
pivpn -qr devicename - Give the connection a name (e.g., "Family VPN")
- Toggle the connection ON
- (Optional) Enable "Connect on Demand" in the profile settings for automatic connection
macOS
- Install WireGuard from the Mac App Store
-
Download the configuration file from your server:
scp vpnuser@your-server-ip:/home/vpnuser/configs/laptop.conf ~/Downloads/ - Open WireGuard and click Import tunnel(s) from file
-
Select the downloaded
.conffile - Click Activate to connect
Windows
- Download WireGuard from wireguard.com
- Install the application
-
Download the configuration file using WinSCP or similar:
- Server:
your-server-ip - Username:
vpnuser - Path:
/home/vpnuser/configs/laptop.conf
- Server:
- Open WireGuard, click Import tunnel(s) from file
-
Select the
.conffile - Click Activate
Linux (Ubuntu/Debian)
Install WireGuard:
apt install -y wireguard openresolv
Download the configuration file:
scp vpnuser@your-server-ip:/home/vpnuser/configs/laptop.conf ~/
Move it to the WireGuard directory:
sudo mv ~/laptop.conf /etc/wireguard/
Manual Connection
Connect to the VPN:
sudo wg-quick up laptop
Disconnect:
sudo wg-quick down laptop
Automatic Connection with NetworkManager
For automatic connection on boot and easier management:
sudo nmcli connection import type wireguard file /etc/wireguard/laptop.conf
Enable auto-connect:
nmcli connection modify laptop connection.autoconnect yes
List all connections:
nmcli connection show
Disable auto-connect if needed:
nmcli connection modify laptop connection.autoconnect no
Verify Your Connection
Once connected, verify your VPN is working:
-
Check your IP address:
curl ifconfig.meThis should show your server's IP, not your local IP.
-
Test DNS resolution:
nslookup doubleclick.netThis ad domain should resolve to
0.0.0.0(blocked by Pi-hole). -
Check Pi-hole logs:
- Open the Pi-hole admin interface
- Navigate to Tools > Query Log
- You should see queries from your device's WireGuard IP (usually
10.6.0.xor similar)
Security Hardening
Restrict Pi-hole Admin Access
By default, anyone who can reach your server's IP can access the Pi-hole admin interface. Restrict it to only VPN-connected devices.
Edit the lighttpd configuration:
nano /etc/lighttpd/external.conf
Add the following (replace IP addresses with your own):
# Restrict admin panel to specific IPs
# Update these IPs to match your WireGuard client IPs
$HTTP["remoteip"] !~ "10.6.0.2|10.6.0.3|10.6.0.4" {
$HTTP["url"] =~ "^/admin/" {
url.access-deny = ( "" )
}
}
Find your WireGuard client IPs:
cat /etc/wireguard/wg0.conf
Look for the Peer sections and note the AllowedIPs values.
Restart lighttpd:
systemctl restart lighttpd
Harden SSH Access
If you haven't already, disable password authentication and root login:
nano /etc/ssh/sshd_config
Find and modify these lines:
PasswordAuthentication no
PermitRootLogin no
Restart SSH:
systemctl restart sshd
Warning: Only do this after confirming SSH key authentication works, or you'll lock yourself out.
Monitor Failed Login Attempts
Check Fail2Ban status:
fail2ban-client status sshd
This shows how many IPs have been banned for failed SSH attempts.
Keep Software Updated
Your unattended-upgrades configuration handles security updates automatically, but you should periodically check for major updates:
apt update
apt list --upgradable
Update Pi-hole:
pihole -up
Update PiVPN:
pivpn update
Maintenance and Monitoring
Regular Maintenance Tasks
Weekly:
- Review Pi-hole query logs for unusual activity
- Check blocked domain percentage (Dashboard)
Monthly:
- Review and update blocklists
- Check server disk space:
df -h - Review Fail2Ban banned IPs:
fail2ban-client status sshd
Quarterly:
- Update Unbound root hints:
wget -O /var/lib/unbound/root.hints https://www.internic.net/domain/named.root systemctl restart unbound
Managing Client Access
List all connected clients:
pivpn list
Show connection statistics:
pivpn -c
Remove a client:
pivpn remove
Select the profile to remove from the menu.
Temporarily disable a client by moving its config:
mv /home/vpnuser/configs/oldphone.conf /home/vpnuser/configs/oldphone.conf.disabled
Backup Your Configuration
Create regular backups of your critical configuration:
# Create backup directory
mkdir -p /root/backups
# Backup Pi-hole
pihole -a -t
mv /root/pi-hole-*.tar.gz /root/backups/
# Backup WireGuard configs
tar -czf /root/backups/wireguard-$(date +%Y%m%d).tar.gz /etc/wireguard/
# Backup Unbound config
cp -r /etc/unbound/ /root/backups/unbound-$(date +%Y%m%d)/
# Download backups to your local machine
scp -r vpnuser@your-server-ip:/root/backups/ ~/vpn-backups/
Monitor Server Resources
Check CPU and memory usage:
htop
(Install with apt install htop if needed)
Check disk space:
df -h
Monitor active WireGuard connections:
wg show
Pi-hole Maintenance
Update gravity (blocklists):
pihole -g
Clear query logs (if needed):
pihole flush
View live query log in terminal:
pihole -t
Troubleshooting
VPN Won't Connect
Check WireGuard status on server:
wg show
systemctl status wg-quick@wg0
Verify firewall allows WireGuard:
ufw status | grep 51820
Check client configuration:
Ensure the client .conf file has the correct server endpoint IP address.
Test connectivity:
ping your-server-ip
If this fails, your client can't reach the server at all (network/firewall issue).
DNS Not Resolving
Verify Unbound is running:
systemctl status unbound
Test Unbound directly:
dig @127.0.0.1 -p 5335 google.com
Check Pi-hole is using Unbound:
In the Pi-hole admin interface, go to Settings > DNS and verify 127.0.0.1#5335 is set as the upstream DNS.
Check Pi-hole status:
pihole status
Verify DNS on client:
From your connected VPN client:
nslookup google.com
This should show your WireGuard server IP as the DNS server.
Ads Still Appearing
Check device is using VPN:
curl ifconfig.me
This should show your server's IP, not your home IP.
Verify DNS queries going through Pi-hole:
Open the Pi-hole admin interface and go to Tools > Query Log. Make a test query and verify it appears in the logs.
Check blocklists are active:
Navigate to Group Management > Adlists and ensure lists are enabled and recently updated.
Update gravity:
pihole -g
Some ads can't be blocked:
YouTube ads, sponsored social media posts, and first-party ads cannot be blocked at the DNS level. These require browser extensions or app-level blocking.
Slow DNS Resolution
Check Unbound cache:
The first DNS query for a domain is always slower (recursive lookup). Subsequent queries should be nearly instant.
Increase cache sizes:
Edit /etc/unbound/unbound.conf.d/pi-hole.conf and increase these values:
rrset-cache-size: 512m
msg-cache-size: 256m
Restart Unbound:
systemctl restart unbound
Check server resources:
htop
If CPU or RAM is maxed out, upgrade your server instance.
Connection Drops Frequently
Mobile devices (iOS/Android):
Enable "Connect on Demand" or "Always-on VPN" in the WireGuard app settings. Some phones aggressively close VPN connections to save battery.
Check server load:
uptime
If the load average is consistently above the number of CPU cores, your server is overloaded.
Check for IP conflicts:
Ensure WireGuard client IPs don't conflict:
cat /etc/wireguard/wg0.conf
Each Peer section should have a unique AllowedIPs value.
Can't Access Pi-hole Admin Interface
From VPN client:
Verify you're connected to the VPN, then navigate to:
http://10.6.0.1/admin
(Use your server's WireGuard IP, found with wg show)
Check lighttpd restrictions:
If you configured IP restrictions, ensure your current WireGuard IP is whitelisted:
nano /etc/lighttpd/external.conf
Verify lighttpd is running:
systemctl status lighttpd
Pi-hole Not Blocking Anything
Check DNS settings in WireGuard config:
On the client device, verify the WireGuard profile has DNS = 10.6.0.1 (or your server's WireGuard IP).
Verify Pi-hole is enabled:
pihole status
If disabled:
pihole enable
Check query log:
If queries don't appear in Pi-hole's query log, DNS requests aren't reaching Pi-hole. Check your client's DNS configuration.
Advanced Configuration
Custom Blocklists
Add additional blocklists in the Pi-hole admin interface:
- Navigate to Group Management > Adlists
- Add list URLs (examples below)
- Click Add
- Run
pihole -gto update
Recommended blocklists:
- The Block List Project:
https://blocklistproject.github.io/Lists/ - OISD:
https://big.oisd.nl/domainswild - Hagezi's lists:
https://github.com/hagezi/dns-blocklists
Warning: Very large blocklists (millions of domains) can slow down DNS resolution. Start conservative and add more only if needed.
Whitelist Management
Some sites break when ads are blocked. Add them to the whitelist:
- Navigate to Whitelist in Pi-hole admin
- Add the domain
- Click Add
Common domains to whitelist:
s.youtube.com(YouTube history)www.googleadservices.com(Google Shopping)amazon-adsystem.com(Amazon product images)
Pre-made whitelist:
git clone https://github.com/anudeepND/whitelist.git
cd whitelist/scripts
sudo python3 whitelist.py
Group Management
Create different filtering levels for different devices:
- Navigate to Group Management > Groups
- Create groups (e.g., "Kids", "Adults", "Work")
- Assign clients to groups in Group Management > Clients
- Assign adlists to groups in Group Management > Adlists
This allows stricter filtering for children's devices while allowing work devices more access.
Split Tunneling
To route only specific traffic through the VPN (e.g., DNS only), edit your client .conf file:
Change:
AllowedIPs = 0.0.0.0/0, ::/0
To:
AllowedIPs = 10.6.0.0/24
This routes only VPN subnet traffic through the tunnel, letting other traffic use your local internet connection directly.
IPv6 Support
If your server and ISP support IPv6:
- Enable IPv6 in Unbound (
do-ip6: yes) - Add IPv6 configuration to WireGuard (
Address = 10.6.0.2/24, fd42:42:42::2/64) - Update Pi-hole to listen on IPv6
- Update UFW to allow IPv6 WireGuard traffic
Detailed IPv6 setup is beyond the scope of this guide, but the components support it.
Monitoring with Grafana (Optional)
For advanced monitoring:
- Install Prometheus and Grafana
- Configure Pi-hole exporter
- Create dashboards for DNS queries, blocked domains, and VPN connections
This is optional but provides excellent visibility into your system's performance.
Resources
Official Documentation
Video Tutorials
- You're running Pi-Hole wrong! Setting up your own Recursive DNS Server!
- High Availability Pi-Hole? Yes please!
Blocklist Resources
Community Support
Additional Guides
Cost Analysis
Bandwidth considerations:
Average usage per device: 50-100MB/day (mostly DNS queries, minimal data transfer). A 1TB/month bandwidth limit handles 20+ devices easily.
Final Notes
This setup provides a robust, self-hosted VPN solution with comprehensive ad blocking and privacy-focused DNS resolution. The initial setup takes 1-2 hours, but ongoing maintenance is minimal (10-15 minutes per month).
Key advantages over commercial VPNs:
- Complete control and visibility
- No third-party logging concerns
- Significantly lower cost for families
- Customizable filtering and access controls
- Learning opportunity for networking and Linux administration
The main limitations:
- Requires basic technical knowledge
- May not work with streaming services
- Single point of failure (consider backup strategies)
- Responsibility for security and maintenance
For most users, this solution provides better privacy, security, and value than commercial VPN services while offering flexibility that commercial services cannot match.
Last updated: October 2025
Tested on: Ubuntu 24.04 LTS, Pi-hole v5.18.3, WireGuard (kernel module), Unbound 1.19.3