Commercial VPN subscriptions promise privacy, but they simply swap your ISP for another centralized entity that sees every unencrypted DNS lookup and connection timestamp. If you own a home server or an inexpensive Linux VPS, deploying your own WireGuard tunnel paired with a dedicated Pi-hole ad-blocking resolver provides complete control over your remote traffic with zero logging and zero recurring subscription fees.
While running WireGuard or Pi-hole independently in Docker is straightforward, chaining them together properly on mobile devices introduces subtle network challenges: DNS fallback leaks on cellular connections, MTU packet fragmentation over 4G/5G, and Docker bridge isolation. In this guide, we walk through building a resilient, production-ready WireGuard and Pi-hole stack with unified networking, persistent volumes, and mobile client profiles.
Network Architecture & Prerequisites
To ensure WireGuard clients cannot bypass the ad-blocker, both containers communicate over a dedicated internal Docker bridge network with static IP assignments:
- WireGuard Subnet:
10.13.13.0/24(WireGuard client address pool) - Internal Docker Network:
172.28.0.0/16(Private bridge network) - Pi-hole Container IP:
172.28.0.100(Static internal DNS endpoint) - WireGuard Container IP:
172.28.0.2(WireGuard gateway)
Before launching the containers, ensure you have:
- An Ubuntu 22.04 / 24.04 or Debian 12 server with root or sudo privileges.
- Docker Engine and Docker Compose v2 installed.
- A public static IP or dynamic DNS (DDNS) hostname pointing to your server.
- UDP port
51820accessible through your server’s firewall and home router (if self-hosting at home).
Step 1: Host Kernel IP Forwarding
For WireGuard to route traffic from your tunnel interface out to the public internet, Linux kernel IPv4 forwarding must be enabled on the host machine.
Open /etc/sysctl.conf in your editor or append the setting directly:
sudo sysctl -w net.ipv4.ip_forward=1
echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.d/99-wireguard.conf
sudo sysctl -p /etc/sysctl.d/99-wireguard.conf
Verify that the setting returns 1:
cat /proc/sys/net/ipv4/ip_forward
# Output should be: 1
Step 2: The Unified Docker Compose Stack
Create a dedicated directory for your stack and create the docker-compose.yml file:
mkdir -p ~/wireguard-pihole/config/{wireguard,pihole,dnsmasq}
cd ~/wireguard-pihole
nano docker-compose.yml
Paste the following production configuration. Replace vpn.yourdomain.com with your actual public IP or DDNS domain, and choose a secure web password for the Pi-hole admin interface:
services:
pihole:
container_name: pihole
image: pihole/pihole:latest
restart: unless-stopped
hostname: pihole-vpn
environment:
TZ: 'UTC'
WEBPASSWORD: 'SetYourSecurePasswordHere'
FTLCONF_LOCAL_IPV4: '172.28.0.100'
DNSMASQ_LISTENING: 'all'
volumes:
- ./config/pihole:/etc/pihole
- ./config/dnsmasq:/etc/dnsmasq.d
networks:
vpn_net:
ipv4_address: 172.28.0.100
ports:
- "127.0.0.1:8080:80/tcp"
wireguard:
container_name: wireguard
image: lscr.io/linuxserver/wireguard:latest
restart: unless-stopped
cap_add:
- NET_ADMIN
- SYS_MODULE
environment:
PUID: 1000
PGID: 1000
TZ: 'UTC'
SERVERURL: 'vpn.yourdomain.com'
SERVERPORT: 51820
PEERS: 'phone,laptop,tablet'
PEERDNS: '172.28.0.100'
INTERNAL_SUBNET: '10.13.13.0/24'
ALLOWEDIPS: '0.0.0.0/0, ::/0'
LOG_CONFS: 'true'
volumes:
- ./config/wireguard:/config
- /lib/modules:/lib/modules:ro
ports:
- "51820:51820/udp"
networks:
vpn_net:
ipv4_address: 172.28.0.2
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
- net.ipv4.ip_forward=1
depends_on:
- pihole
networks:
vpn_net:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/16
Key Architectural Decisions in This Compose File
- Static Internal IP (
172.28.0.100): WireGuard’sPEERDNSdirective points directly to this internal address. Client configurations generated by the container will automatically route every DNS query straight to Pi-hole. - Restricted Pi-hole Port (
127.0.0.1:8080:80): Binding Pi-hole’s web dashboard to localhost avoids exposing the admin interface to the public internet. Access it securely via SSH port forwarding or through your WireGuard tunnel. DNSMASQ_LISTENING: 'all': Standard Pi-hole configurations only accept queries on local interfaces. Because WireGuard packets arrive via the Docker bridge network, Pi-hole must permit queries from non-local subnets (safe because port 53 is not published to the public internet).
Step 3: Initializing the Stack & Connecting Clients
Launch the containers in detached mode:
docker compose up -d
Check the container initialization logs to confirm key generation and peer profile creation:
docker compose logs -f wireguard
Connecting Android / iOS via QR Code
The LinuxServer WireGuard image generates an ASCII QR code directly in the terminal for each peer specified in your environment variables. To display the QR code for peer 1 (phone):
docker exec -it wireguard /app/show-peer 1
Open the official WireGuard app on your Android or iOS device, tap the + button, select Create from QR code, and scan the terminal output. Give the tunnel a name (e.g., Home-Shield) and save.
Connecting Windows, macOS, or Linux Laptops
The raw configuration files are stored inside your host volume directory:
ls -l ./config/wireguard/peer2/
# Contains: peer2.conf, peer2.png
Copy peer2.conf to your laptop securely using SCP or SFTP, and import it into the desktop WireGuard client.
Step 4: Preventing Real-World Pitfalls
1. The Mobile MTU Handshake Stall
Default WireGuard client configs specify an MTU of 1420. On many mobile carrier networks (particularly LTE/5G networks that wrap packets in additional carrier encapsulation headers), an MTU of 1420 causes packet fragmentation. The connection will appear active and exchange small packets (like pings), but web pages and TLS handshakes will hang indefinitely.
The Fix: Edit the client configuration on your phone or laptop. Under the [Interface] section, add an explicit MTU value of 1280 (the minimum IPv6 MTU, guaranteed to pass unfragmented across any mobile carrier):
[Interface]
PrivateKey = xxxxx...
Address = 10.13.13.2/32
DNS = 172.28.0.100
MTU = 1280
2. Verifying Zero DNS Leaks
Once connected, verify that your client is not falling back to your cellular or local Wi-Fi router’s DNS servers:
- On your connected device, visit browserleaks.com/ip or dnsleaktest.com.
- Run an Extended Test.
- Verify that the only DNS resolvers visible belong to your VPS/home upstream provider (or Cloudflare/Quad9 if configured in Pi-hole), with zero queries leaking to your mobile ISP.
3. Accessing the Pi-hole Admin Interface
Because port 80 is mapped to 127.0.0.1:8080 on the server for security, create an SSH tunnel from your local machine to access the web GUI:
ssh -L 8080:localhost:8080 user@your-server-ip
Navigate to http://localhost:8080/admin in your browser and log in with the WEBPASSWORD you set in your compose file. You will immediately see incoming queries from your WireGuard peer (10.13.13.x) being filtered in real time.
Routine Maintenance & Upgrades
Because all configurations and WireGuard cryptographic keys reside inside the ./config directory on your host, updating the stack takes two commands without risking peer disconnects:
cd ~/wireguard-pihole
docker compose pull
docker compose up -d --remove-orphans
The containers restart with the latest security patches while retaining all existing peer keys, blocklists, and query logs.
Verdict
Setting up your own WireGuard and Pi-hole stack takes under twenty minutes and delivers a private, encrypted tunnel with ad and telemetry blocking across all your devices. By isolating the services inside a private Docker bridge network and tuning the client MTU for carrier compatibility, you achieve the security of an enterprise VPN setup on lightweight consumer or cloud hardware.

Leave a Reply