WireGuard Tunnel: Connecting a Raspberry Pi to Google Cloud

Set up a persistent VPN tunnel between a Raspberry Pi behind NAT and a GCE VM. Foundation for distributed homelab systems.

Tailscale seemed like the obvious choice. Install on both machines, authenticate, done. The marketing promised exactly what I needed: my Raspberry Pi at home talking to a GCE VM in the cloud.

It worked for three days. Then the connection started dropping. Sometimes it would reconnect. Sometimes it wouldn’t. The Pi showed “connected” while the VM couldn’t reach it. Debugging meant reading through Tailscale’s coordination server logs, wondering if the issue was their infrastructure or mine.

I spent an afternoon on this before switching to WireGuard. Ten lines of config on each machine. No external servers. No authentication flows. It’s been running for weeks without a single drop.

The Problem

You have a Raspberry Pi behind your home router’s NAT. You have a GCE VM with a public IP. You want bidirectional communication - the VM should be able to initiate connections to the Pi, not just receive them.

The challenge: NAT hides the Pi. The GCE VM can’t reach it directly. You need a persistent tunnel where the Pi initiates, but both sides can talk.

Why WireGuard

  • ~10 lines of config per machine - no complex setup
  • No external dependencies - runs entirely on your infrastructure
  • NAT-friendly - PersistentKeepalive keeps the tunnel alive
  • Kernel-level performance - minimal overhead

The Architecture

WireGuard architecture between GCE VM and Raspberry Pi

The key insight: the Pi initiates the connection. It knows the GCE public IP and can reach it. The GCE VM doesn’t need to know where the Pi is - WireGuard remembers where packets came from.

PersistentKeepalive sends a packet every 25 seconds to keep the NAT mapping alive. Without it, the router forgets the mapping and the GCE VM can no longer reach the Pi.

Prerequisites

  • Google Cloud account with a project and billing enabled
  • gcloud CLI installed and configured
  • Raspberry Pi 5 with Raspberry Pi OS (64-bit)

Step 1: Create the GCE VM

If you don’t have a Google Cloud project yet, create one:

gcloud projects create wireguard-tunnel --name="WireGuard Tunnel"
gcloud config set project wireguard-tunnel

Enable billing for the project in the Google Cloud Console, then enable the Compute Engine API:

gcloud services enable compute.googleapis.com

Start by reserving a static IP. Ephemeral IPs change when you stop and start the VM, which would break the Pi’s Endpoint config and require manual updates every time.

gcloud compute addresses create wireguard-ip \
    --region=asia-south1

View the reserved IP - you’ll need this later for the Pi’s configuration:

gcloud compute addresses describe wireguard-ip \
    --region=asia-south1 \
    --format='get(address)'

Create the VM with the static IP attached:

gcloud compute instances create wireguard-server \
    --zone=asia-south1-a \
    --machine-type=e2-micro \
    --image-family=debian-12 \
    --image-project=debian-cloud \
    --address=wireguard-ip \
    --tags=wireguard

The e2-micro instance is free-tier eligible and more than enough for WireGuard - it’s just encrypting and forwarding packets.

Open the firewall for WireGuard traffic:

gcloud compute firewall-rules create allow-wireguard \
    --direction=INGRESS \
    --priority=1000 \
    --network=default \
    --action=ALLOW \
    --rules=udp:51820 \
    --source-ranges=0.0.0.0/0 \
    --target-tags=wireguard

SSH into the VM to continue setup:

gcloud compute ssh wireguard-server --zone=asia-south1-a

Step 2: Configure WireGuard on the GCE VM

Install WireGuard:

sudo apt update && sudo apt install -y wireguard

Generate keys. WireGuard uses public-key cryptography - each machine gets a keypair, and they exchange public keys to authenticate.

sudo su
cd /etc/wireguard
umask 077
wg genkey | tee privatekey | wg pubkey > publickey

The umask 077 ensures new files are only readable by root. View the public key - you’ll need this for the Pi’s configuration:

cat publickey

Create the server configuration at /etc/wireguard/wg0.conf:

[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <contents of privatekey>

[Peer]
# Raspberry Pi
PublicKey = <Pi's public key - from Step 3>
AllowedIPs = 10.0.0.2/32

Leave the Pi’s public key blank for now - you’ll add it after setting up the Pi.

Step 3: Configure WireGuard on the Raspberry Pi

SSH into your Pi and install WireGuard:

sudo apt update && sudo apt install -y wireguard

Generate keys the same way:

sudo su
cd /etc/wireguard
umask 077
wg genkey | tee privatekey | wg pubkey > publickey

View the public key - you’ll add this to the GCE config:

cat publickey

Create the client configuration at /etc/wireguard/wg0.conf:

[Interface]
Address = 10.0.0.2/24
PrivateKey = <contents of privatekey>

[Peer]
# GCE VM
PublicKey = <GCE's public key from Step 2>
Endpoint = <GCE static IP from Step 1>:51820
AllowedIPs = 10.0.0.1/32
PersistentKeepalive = 25

The Pi’s config has two settings the GCE config doesn’t need:

Endpoint tells the Pi where to find the GCE VM. The GCE VM doesn’t need an Endpoint because the Pi initiates - WireGuard learns the Pi’s address from incoming packets.

PersistentKeepalive = 25 sends a packet every 25 seconds. Without this, the router’s NAT mapping expires and the GCE VM can no longer reach the Pi.

Step 4: Exchange Keys and Connect

Key exchange flow between GCE and Raspberry Pi

Go back to the GCE VM and add the Pi’s public key to the config:

sudo nano /etc/wireguard/wg0.conf
# Add the Pi's public key to the [Peer] section

Start WireGuard on the GCE VM first:

sudo wg-quick up wg0

Then start it on the Pi:

sudo wg-quick up wg0

Step 5: Verify the Connection

Check the tunnel status on either machine:

sudo wg show

You should see latest handshake with a recent timestamp - this confirms the tunnel is working.

Test connectivity from both directions:

# From Pi
ping 10.0.0.1

# From GCE
ping 10.0.0.2

Both should work. The GCE VM can now reach the Pi even though it’s behind NAT.

Step 6: Enable on Boot

Run this on both machines to start WireGuard automatically:

sudo systemctl enable wg-quick@wg0

wg-quick is a helper script that reads the config file and sets up the interface, routes, and addresses. The interface name wg0 comes from the config filename.

Making It Resilient

Even with WireGuard’s reliability, network conditions can cause temporary disconnects. Add a health check on the Pi that automatically restarts the tunnel if it goes down:

sudo cat > /usr/local/bin/wg-health-check.sh << 'EOF'
#!/bin/bash
STATE_FILE="/tmp/wg-health-state"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

if ping -c 1 -W 5 10.0.0.1 > /dev/null 2>&1; then
    # Tunnel is up
    if [ ! -f "$STATE_FILE" ]; then
        logger -t wireguard "Health check started - tunnel healthy at $TIMESTAMP"
    elif [ "$(cat $STATE_FILE)" = "down" ]; then
        logger -t wireguard "Tunnel recovered at $TIMESTAMP"
    fi
    echo "up" > "$STATE_FILE"
else
    # Tunnel is down
    logger -t wireguard "Tunnel down at $TIMESTAMP, restarting..."
    echo "down" > "$STATE_FILE"
    wg-quick down wg0 2>/dev/null || true
    sleep 2
    wg-quick up wg0
fi
EOF

sudo chmod +x /usr/local/bin/wg-health-check.sh

The script logs on first run, when the tunnel goes down, and when it recovers. View the logs with:

# Recent WireGuard health check entries
journalctl -t wireguard --since "24 hours ago"

# Follow live
journalctl -t wireguard -f

Schedule it to run every 5 minutes:

(sudo crontab -l 2>/dev/null; echo "*/5 * * * * /usr/local/bin/wg-health-check.sh") | sudo crontab -

Scaling to Multiple Pis

WireGuard handles multiple peers natively. To add a second Pi (or third, fourth…), add another [Peer] block to the GCE config:

[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <server-private-key>

[Peer]
# Raspberry Pi 1
PublicKey = <pi1-public-key>
AllowedIPs = 10.0.0.2/32

[Peer]
# Raspberry Pi 2
PublicKey = <pi2-public-key>
AllowedIPs = 10.0.0.3/32

Each Pi gets a unique IP in the 10.0.0.0/24 subnet and generates its own keypair. All Pis can be behind the same home NAT - each initiates its own connection to the GCE endpoint.

For the browser automation system in later posts, this means you can run 2 Pis with 6 dongles each = 12 different exit IPs, with the GCE orchestrator load-balancing between them.

When WireGuard Doesn’t Fit

This approach has real costs:

Manual key exchange. Every peer needs to exchange public keys out-of-band. For two machines, this is trivial. For a fleet, it’s painful. Tailscale’s coordination server solves this - you trade simplicity for their infrastructure dependency.

No automatic NAT traversal. If both machines are behind NAT (no public IP anywhere), WireGuard alone won’t work. You’d need a relay or one machine with port forwarding configured.

Static configuration. Adding or removing peers requires editing config files and restarting. No web UI, no API.

WireGuard works best when:

  • You have a small number of machines (2-10)
  • At least one has a public IP
  • You value reliability over convenience

It’s probably overkill for:

  • Large fleets needing dynamic membership
  • Situations where all machines are behind NAT
  • Teams wanting a management UI

Troubleshooting

No handshake:

  • Check GCE firewall allows UDP 51820
  • Verify the Endpoint IP is correct in the Pi’s config
  • Confirm public keys match on both ends

Handshake works but ping fails:

  • Check AllowedIPs are correct
  • Verify the Address lines don’t conflict with existing networks

Connection drops after inactivity:

  • Ensure PersistentKeepalive is set on the Pi
  • Some aggressive NATs need 15-20 seconds instead of 25

What’s Next

The GCE VM can now communicate with the Raspberry Pi as if they were on the same network. In the next post, we’ll set up Playwright on the Pi for headless browser automation, routing requests through multiple LTE connections.

About the Author

Ashish Anand

Ashish Anand

Founder & Lead Developer

Full-stack developer with 10+ years experience in Python, JavaScript, and DevOps. Creator of DevGuide.dev. Previously worked at Microsoft. Specializes in developer tools and automation.