If you followed the Pi-hole and Tailscale on Turing Pi article, you already have a private mesh between your own devices. Install it on your phone, laptop, and Turing Pi node, and they can securely communicate as though they’re on the same LAN, no matter where you are. If you also worked through the Cloudflare Tunnel guide, you’ve seen how to securely expose individual self-hosted services to the internet without opening ports on your router. Both are excellent remote-access tools, but they solve different problems.

Tailscale is excellent for connecting your own devices, while Cloudflare Tunnel securely publishes individual services such as Nextcloud or Gitea. Neither is designed to provide unrestricted access to your entire home network through a traditional VPN connection.

WireGuard fills that gap. It’s a modern VPN protocol that runs entirely on hardware you own, with your Turing Pi acting as the VPN server. Anyone you give a client configuration can securely connect to your home network as if they were physically on the LAN, with no third-party coordination server and no account-based device limits.

In this guide, you’ll deploy WireGuard on ARM64 using WG-Easy, a Docker container that provides a web interface for managing clients, generating configuration files, and monitoring connections. By the end, you’ll have a fully self-hosted WireGuard VPN running on your Turing Pi. You’ll configure the one router port WireGuard requires, generate client profiles for a phone and laptop, verify that all traffic is routing through your Turing Pi, integrate Pi-hole for network-wide ad blocking, and understand when WireGuard is the better choice than Tailscale or Cloudflare Tunnel.


Part 1: How WireGuard Actually Works

WireGuard has been part of the mainline Linux kernel since version 5.6, and Ubuntu 22.04’s default 5.15 kernel includes it natively on both ARM64 and x86_64. There’s no separate kernel module to compile or install. The support is already built into the kernel, so WireGuard is ready to use out of the box.

The model is straightforward. Your Turing Pi acts as the VPN server, and every phone, laptop, or tablet you want to connect becomes a VPN client. Each client receives its own configuration containing the server’s public endpoint, the client’s private key, and the information needed to establish an encrypted tunnel. Once connected, the device behaves as though it were sitting on your home network, with traffic routed securely through the Turing Pi before reaching the internet.

This is also where WireGuard differs from Tailscale. Tailscale establishes connections through its coordination infrastructure, allowing devices to connect without exposing anything on your router. A self-hosted WireGuard server has no coordination service, so your clients need a way to reach it directly. That means forwarding a single UDP port, 51820 by default, from your router to the Turing Pi.


Part 2: WireGuard vs Tailscale vs Cloudflare Tunnel

By this point, the series has covered three different ways to access your homelab remotely. They solve different problems, so choosing the right one depends on what you’re trying to accomplish.

WireGuardTailscaleCloudflare Tunnel
Self-hostedYes, fullyNo, coordination runs through Tailscale’s infrastructureNo, traffic routes through Cloudflare’s edge
Port forwarding requiredYes, one UDP portNoNo
Full LAN accessYesYesNo
Client device limitNone, self-managedFree tier has device limitsNot device-based
Setup complexityModerate: Docker Compose plus router configurationLow: install and authenticateLow to moderate: cloudflared plus DNS records
Best forFull VPN access and sharing your home networkSecure access across your own devicesPublishing specific services to the internet

Choose WireGuard if you want a fully self-hosted VPN with complete LAN access and the freedom to share access without relying on a third-party service. The trade-off is forwarding a single UDP port on your router.

Choose Tailscale if you want the quickest way to securely connect your own devices with almost no network configuration. It handles NAT traversal for you, so there’s no need to expose your home network.

Choose Cloudflare Tunnel if you want to publish specific applications, such as Nextcloud, Gitea, or a status page, without giving users access to your entire network or requiring them to install a VPN client.

These tools aren’t mutually exclusive. Many homelabs run all three together: Tailscale for personal access, Cloudflare Tunnel for selected public services, and WireGuard when full VPN access is needed.


Part 3: Prerequisites and Port Forwarding

Before deploying anything, confirm your system has WireGuard support available:

lsmod | grep wireguard

If the command returns nothing, don’t worry. WG-Easy uses the WireGuard support already built into the host kernel, so an empty result simply means the module hasn’t been loaded yet. It loads automatically when the WireGuard interface is created.

Next, forward UDP port 51820 from your router to your Turing Pi node’s LAN IP address. Every router has a different interface, but you’re looking for a setting called Port Forwarding, Virtual Server, or something similar. Create a rule that forwards external UDP port 51820 to the same port on your Turing Pi.

If your Turing Pi sits behind a second router, you’ll need to forward UDP port 51820 on both devices. Forward the port from your ISP router to your own router’s WAN IP, then from your own router to the Turing Pi. Alternatively, if you trust your own router to handle all inbound traffic, placing it in your ISP router’s DMZ achieves the same result.

If your home internet connection doesn’t have a static public IP address, you’ll also need a Dynamic DNS (DDNS) provider so clients can always find your network. DuckDNS and No-IP both offer free tiers. DuckDNS is simple and widely used in the homelab community, while No-IP is generally considered the more reliable choice if uptime is a higher priority.

One important caveat is Carrier-Grade NAT (CGNAT). If your ISP places you behind CGNAT, port forwarding won’t work because your connection doesn’t have a publicly reachable IPv4 address. To check, compare your router’s WAN IP with the output of:

curl ifconfig.me

If the address returned by curl ifconfig.me doesn’t match your router’s WAN IP, your connection is either behind another router or your ISP is using Carrier-Grade NAT (CGNAT). If your router itself has a private WAN address (for example 192.168.x.x or 10.x.x.x), another router is performing NAT upstream. If your router has a public WAN IP that still differs from curl ifconfig.me, your ISP is most likely using CGNAT.

In either case, WireGuard won’t be reachable until you remove the extra layer of NAT or obtain a publicly reachable IPv4 address.

This is also the first guide in the series that requires exposing a port on your router, so it’s worth understanding the trade-off. Unlike Tailscale or Cloudflare Tunnel, WireGuard needs an inbound path for clients to connect. That does increase your attack surface, but it’s important to size the risk correctly. WireGuard doesn’t expose a web interface or respond to unauthenticated packets. Invalid traffic is silently discarded, leaving no banner to fingerprint and no login page to brute-force. Combined with its intentionally small codebase, modern cryptographic design, and years of public scrutiny, opening UDP port 51820 is fundamentally different from exposing a typical web service on ports 80 or 443.


Part 4: Deploy WG-Easy via Docker Compose

Deploying WireGuard on ARM64 with WG-Easy only requires a single Docker Compose file. The configuration below initializes the server, exposes the WireGuard VPN on UDP port 51820, and makes the management interface available on TCP port 51821 for local administration.

Before you continue: Make sure your server is reachable from the internet. If your ISP uses Carrier-Grade NAT (CGNAT), port forwarding alone will not work. In that case, you’ll need a public IPv4 address from your ISP, or an alternative solution such as Tailscale.

Create a directory to store your WireGuard configuration:

mkdir -p /mnt/nvme/wg-easy
cd /mnt/nvme/wg-easy

Save the following as docker-compose.yml:

services:
  wg-easy:
    image: ghcr.io/wg-easy/wg-easy:15
    container_name: wg-easy

    environment:
      - INIT_ENABLED=true
      - INIT_USERNAME=admin
      - INIT_PASSWORD=CHANGE_ME_TO_A_LONG_PASSWORD

      # Replace with your public IP or DDNS hostname.
      - INIT_HOST=YOUR_PUBLIC_IP_OR_DDNS_HOSTNAME
      - INIT_PORT=51820

      # VPN clients will use this DNS server.
      - INIT_DNS=1.1.1.1

      # The web UI is only accessible from your LAN.
      - INSECURE=true

    volumes:
      - /mnt/nvme/wg-easy:/etc/wireguard
      - /lib/modules:/lib/modules:ro

    ports:
      - "51820:51820/udp"
      - "51821:51821/tcp"

    restart: unless-stopped

    cap_add:
      - NET_ADMIN
      - SYS_MODULE

    sysctls:
      - net.ipv4.ip_forward=1
      - net.ipv4.conf.all.src_valid_mark=1
      - net.ipv6.conf.all.disable_ipv6=0
      - net.ipv6.conf.all.forwarding=1
      - net.ipv6.conf.default.forwarding=1

    networks:
      wg:
        ipv4_address: 10.42.42.42
        ipv6_address: fdcc:ad94:bacf:61a3::2a

networks:
  wg:
    driver: bridge
    enable_ipv6: true
    ipam:
      driver: default
      config:
        - subnet: 10.42.42.0/24
	- subnet: fdcc:ad94:bacf:61a3::/64

Most of the configuration is straightforward, but a few settings are worth highlighting:

  • INIT_HOST is the address clients use to reach your VPN server. Replace it with either your public IPv4 address or a Dynamic DNS hostname if your ISP changes your public IP periodically. WG-Easy does not configure Dynamic DNS automatically, so make sure your DDNS hostname is already updating correctly before using it here.
  • INIT_DNS specifies the DNS server VPN clients should use after connecting. Any public resolver works, such as Cloudflare (1.1.1.1) or Quad9 (9.9.9.9). If you’re already running Pi-hole from our earlier guide, simply replace this with your Pi-hole server’s IP to enable network-wide ad blocking automatically.
  • INIT_PASSWORD sets the administrator password used during the initial setup. Replace it with a strong password before deploying. These INIT_* variables are only read during the first startup, so you can safely remove them from your Compose file after initialization to avoid storing credentials in plaintext.
  • NET_ADMIN and SYS_MODULE allow the container to create the WireGuard interface and configure routing.
  • The sysctls enable IPv4 and IPv6 packet forwarding so VPN traffic can be routed to your LAN and the internet.
  • Forward only UDP port 51820 on your router. Keep TCP port 51821 accessible only from your local network, or place it behind another secure access method if you need remote administration.

Start the container from the directory containing your docker-compose.yml:

docker compose up -d

After a few seconds, confirm that the container is running:

docker ps

You should see wg-easy running with UDP port 51820 and TCP port 51821 published.

For additional confirmation, inspect the container logs:

docker logs wg-easy

The startup log should end with messages indicating that the wg0 interface was created successfully and the WireGuard service is listening on port 51820.


Part 5: Generate Client Configurations

Open http://<node-ip>:51821 from a device on your local network and log in using the administrator credentials you configured during deployment. Click New Client, enter a descriptive name such as phone or laptop, and create the client. WG-Easy immediately generates both a QR code for mobile devices and a standard WireGuard configuration file for desktop clients.

Each client includes a QR code for the mobile WireGuard app and a .conf file that can be imported into the desktop client. The configuration contains the server’s public endpoint and public key, the client’s private key, the allowed IPs that define which traffic is routed through the VPN, and the DNS server configured during deployment.

Create a separate client for every device instead of sharing the same configuration between multiple devices. This makes it easy to revoke access for a single device without affecting the others.

If you later change your server’s public IP address or INIT_HOST, regenerate the client configuration and import the updated profile into your device so it uses the correct endpoint.


Part 6: Connect and Verify

Install the official WireGuard app on your phone, tap Add Tunnel, scan the QR code generated by WG-Easy, and activate the tunnel.

Once connected, verify that everything is working as expected:

  • Visit https://ifconfig.me or https://icanhazip.com. The reported public IP should match your home internet connection rather than your current Wi-Fi or mobile network.
  • Access services hosted on your homelab, such as Nextcloud, Grafana, Gitea, or your Kubernetes dashboards, using their normal LAN IP addresses or local DNS names.
  • If you configured INIT_DNS to use your Pi-hole server, visit a few ad-heavy websites and confirm that advertisements are blocked. You should also see DNS queries from your VPN client appear in the Pi-hole dashboard.

For laptops and desktops, install the official WireGuard client for your operating system, import the generated .conf file, and activate the tunnel. The same verification steps apply.

If the tunnel connects but you can’t access your homelab or the internet, check the server logs with:

docker exec wg-easy wg show

A successful connection will display a latest handshake timestamp and transfer statistics for the client. If no handshake appears, verify your router’s UDP port forwarding configuration and ensure your ISP is not placing your connection behind Carrier-Grade NAT (CGNAT).


Part 7: Use Pi-hole for DNS Filtering

If you’re already running Pi-hole on your homelab, you can extend the same network-wide ad and tracker blocking to every WireGuard client. Set INIT_DNS to your Pi-hole server’s IP address before creating clients, and every newly generated configuration will automatically use Pi-hole for DNS resolution.

If you already created clients before changing INIT_DNS, simply edit the client’s DNS setting in the WG-Easy web interface or regenerate the client configuration.

To verify that DNS requests are flowing through Pi-hole, connect a device to the VPN and run:

nslookup doubleclick.net

The DNS server shown should be your Pi-hole instance rather than a public resolver such as 1.1.1.1 or 8.8.8.8.

You can also open the Query Log in the Pi-hole dashboard and confirm that DNS requests are arriving from your WireGuard client subnet (10.42.42.0/24 in the Compose configuration above). If everything is configured correctly, connected devices will receive the same DNS filtering and ad blocking whether they’re at home or connected remotely through the VPN.

Part 8: Access Your Homelab Securely

This is where running your own VPN becomes genuinely useful. Once connected, your phone or laptop behaves exactly like another device on your home network, giving you secure access to internal services without exposing them to the public internet.

Whether you’re running Nextcloud, Gitea, Grafana, Jellyfin, Home Assistant, Kubernetes dashboards, or any other self-hosted service, they’re all reachable using their normal LAN IP addresses or local DNS names over a single VPN connection.

This is the key difference between WireGuard and Cloudflare Tunnel. Cloudflare Tunnel securely publishes individual web applications without requiring visitors to install a VPN client. WireGuard, on the other hand, provides secure access to your entire private network, making it ideal for administrators, developers, and anyone who needs unrestricted access to a homelab while away from home.

The two approaches complement each other rather than compete. You can run Cloudflare Tunnel to expose a public-facing application such as a personal website or blog, while using WireGuard to securely access internal dashboards, SSH, file shares, Kubernetes services, and everything else that should remain private.


Troubleshooting

Most WireGuard problems fall into one of four categories: the client can’t reach the server, the VPN connects but traffic doesn’t flow, DNS isn’t working correctly, or the WireGuard interface never initializes. Start by checking the server status:

docker exec wg-easy wg show

A healthy client will show a recent latest handshake timestamp along with increasing transfer counters. If you don’t see a handshake, work through the checks below.

No handshake appears. The WireGuard server isn’t receiving connection attempts. Verify that UDP port 51820 is forwarded to your server, confirm your router or firewall isn’t blocking inbound UDP traffic, and check that the client’s endpoint matches your current public IP or DDNS hostname. If you’re using Dynamic DNS, make sure it’s updating correctly. Finally, compare your router’s WAN IP with the output of curl ifconfig.me. If the two addresses don’t match, your ISP is likely using Carrier-Grade NAT (CGNAT), which prevents inbound connections from reaching your network. In that case, you’ll need a public IPv4 address from your ISP, or an alternative solution such as Tailscale.

Handshake succeeds, but you can’t access your homelab or the internet. This usually indicates a routing issue. Verify that the client’s Allowed IPs is set to 0.0.0.0/0, ::/0 if you want all traffic to pass through the VPN. Also confirm the container started successfully, the required sysctls are present in your Compose file, and restart the container after making any configuration changes.

DNS isn’t working. If websites don’t load but IP addresses do, DNS is likely the issue. Verify that the client is using the correct DNS server. If you’re using Pi-hole, confirm it accepts queries from the WireGuard client subnet and check the Pi-hole Query Log to verify DNS requests are arriving.

The container starts, but the VPN interface isn’t created. Verify that NET_ADMIN and SYS_MODULE are present under cap_add and that the required sysctls from the Compose file are applied. If you’re running a newer Linux kernel with nftables-only networking, also ensure your installed WG-Easy version is compatible with your kernel.

The QR code doesn’t appear in the web interface. This is typically a browser compatibility issue. Refresh the page, try another browser, or download and import the generated .conf file directly into the WireGuard client.


What You’ve Built

You now have a fully self-hosted WireGuard VPN running on your Turing Pi cluster. Instead of exposing individual services to the internet, you can securely connect to your entire homelab from anywhere with a single VPN connection.

Whether you’re SSHing into Kubernetes nodes, syncing files with Nextcloud, pushing code to Gitea, monitoring Grafana dashboards, or streaming from Jellyfin, every service on your private LAN is now securely reachable through a single VPN connection.

Combined with Pi-hole, your VPN clients also benefit from the same DNS filtering and ad blocking you’d have at home, creating a consistent browsing experience across every device.

This guide completes our remote access series:

  • WireGuard provides secure, high-performance access to your entire private network.
  • Tailscale simplifies private mesh networking when traditional port forwarding isn’t practical.
  • Cloudflare Tunnel securely publishes individual services without exposing your home network.

Each solves a different problem, and many homelabs run all three together. Choose the one that best fits your use case, or combine them to build a flexible, secure remote access strategy for your ARM homelab.


Related Articles


FAQ

What is the difference between WireGuard and Tailscale?

WireGuard is a VPN protocol that you can self-host entirely on your own hardware. In this guide, your Turing Pi acts as the VPN server. Tailscale is built on top of WireGuard but adds a managed coordination service that handles peer discovery, NAT traversal, and device management automatically. If you want complete control, WireGuard is the better choice. If you want the easiest setup without port forwarding, Tailscale is often the better fit.

Does WireGuard work on ARM64?

Yes. WireGuard works natively on modern ARM64 Linux distributions, including Ubuntu Server running on the Turing Pi RK1. No architecture-specific setup is required beyond the standard deployment steps in this guide.

Do I need to open ports for WireGuard?

Yes. By default, you’ll need to forward UDP port 51820 from your router to your WireGuard server. If your ISP uses Carrier-Grade NAT (CGNAT), port forwarding won’t work until you obtain a public IPv4 address or use an alternative solution such as Tailscale.

What is WG-Easy?

WG-Easy is a lightweight web interface for WireGuard that simplifies VPN management. It automatically generates client configurations and QR codes, lets you add or remove devices with a few clicks, and provides a simple dashboard for managing your VPN without editing configuration files manually.

Can I use WireGuard and Tailscale at the same time?

Yes. The two work well together and many homelab users run both. WireGuard is ideal for full access to your private network, while Tailscale is convenient when you need simple remote access without configuring port forwarding.

How many clients can connect to a self-hosted WireGuard server?

WireGuard doesn’t impose a fixed client limit. In practice, the number of simultaneous connections depends on your server’s resources, your router, and your internet connection, particularly your available upload bandwidth.

Is WireGuard safe to self-host?

Yes, when configured correctly. WireGuard uses modern cryptography, doesn’t expose a login page to the internet, and ignores unauthenticated traffic. Keep your server updated, use a strong password for the WG-Easy web interface, back up your client configurations securely, and only expose the required UDP port.