,

How to Build a Zero-Leak WireGuard VPN with Pi-hole and Docker

How to Build a Zero-Leak WireGuard VPN with Pi-hole and Docker

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:

  1. An Ubuntu 22.04 / 24.04 or Debian 12 server with root or sudo privileges.
  2. Docker Engine and Docker Compose v2 installed.
  3. A public static IP or dynamic DNS (DDNS) hostname pointing to your server.
  4. UDP port 51820 accessible 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’s PEERDNS directive 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:

  1. On your connected device, visit browserleaks.com/ip or dnsleaktest.com.
  2. Run an Extended Test.
  3. 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.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.