> ## Content Index
> Fetch the complete content index at: https://developerinsider.co/llms.txt
> Use this file to discover other available public pages before exploring further.

# Bypass CGNAT Using WireGuard on a VPS for Full Remote HomeLab Access
- URL: https://developerinsider.co/bypass-cgnat-using-wireguard-on-a-vps-for-full-remote-homelab-access/
- Published: 2026-02-10T16:59:36.000Z
- Updated: 2026-02-22T15:39:18.000Z
- Author: Vineet Choudhary
- Tags: Wiki, Homelab, VPS, CGNAT, WireGuard, Bypass CGNAT, Self-Hosted

If your home internet is behind CGNAT, you can't port-forward. That means no direct access to your HomeLab from outside. Here's how I solved it using a cheap VPS, WireGuard, and Docker.

## Architecture

The local Wireguard client connects outbound to the VPS (bypasses CGNAT). Other devices like Mobile connect to the VPS. Traffic flows: `Other Device` (like `Mobile`) → `Wireguard` → `VPS` → `Wireguard Tunnel` → `HomeLab Wireguard Client` → `VLANs`.

```
┌─────────────────┐         WireGuard Tunnel         ┌──────────────────────────────┐
│      VPS        │◄────────────────────────────────►│  HomeLab Wireguard Client    │
│  (Public IP)    │    Wireguard Client Initiates    │       (Behind CGNAT)         │
│                 │          (CGNAT safe)            │                              │
│  wg-easy :51820 │   Tunnel subnet: 10.8.0.0/24     │  172.16.10.0/24 (VLAN 10)    │
│  Web UI  :51821 │                                  │  172.16.20.0/24 (VLAN 20)    │
│                 │                                  │  172.16.30.0/24 (VLAN 30)    │
└────────┬────────┘                                  └──────────────────────────────┘
         │ Wireguard
    ┌────┴────┐
    │ Other   │  Connects to VPS via WireGuard
    │ Device  │  → reaches HomeLab VLANs via tunnel
    └─────────┘

```

1. **VPS** runs [wg-easy](https://github.com/wg-easy/wg-easy?ref=developerinsider.co) (WireGuard + Web UI)
2. **HomeLab Wireguard Client** connects *outbound* to the VPS (CGNAT doesn't block outbound connections)
3. **Other devices** (like Mobile) connects to the VPS using Wireguard, and traffic to HomeLab IPs flows through the tunnel

## What You Need

- A VPS with a public IP like on Digital Ocean
- Docker installed on both VPS and HomeLab

## 1\. Install Docker

Follow the Docs here: [https://docs.docker.com/engine/install/](https://docs.docker.com/engine/install/?ref=developerinsider.co) and install Docker on your host.

## 2\. Setup and Start wg-easy

### 2.1 Create wg-easy config directory

Create a directory for the configuration files (you can choose any directory you like):

```bash
sudo mkdir -p /etc/docker/containers/wg-easy

```

```bash
cd /etc/docker/containers/wg-easy

```

### 2.2 Create compose file

```bash
sudo nano docker-compose.yml

```

```yaml
services:
  wg-easy:
    image: ghcr.io/wg-easy/wg-easy:15
    container_name: wg-easy
    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
    environment:
      - INSECURE=true
      # - DISABLE_IPV6=false  # Keep IPv6 enabled for VPS if supported
    volumes:
      - /etc/docker/containers/wg-easy:/etc/wireguard
      - /lib/modules:/lib/modules:ro
    ports:
      - "51820:51820/udp"
      - "51821:51821/tcp"
    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

```

### 2.3 Start wg-easy

```bash
sudo docker compose up -d

```

### 2.4\. Setup Firewall

```
sudo ufw allow 51820/udp
sudo ufw allow 51821/tcp

```

### 2.5\. Setup wg-easy

Open `http://YOUR_VPS_IP:51821`, create an admin account, set your VPS public IP as the host, and set the port to `51820`.

## 3\. Create HomeLab client in wg-easy

- Add Client → Name: `HomeLab`  
![](https://cdn.developerinsider.co/images/wiki/homelab/wireguard/vps-tunnel/wg-easy-new-client-homelab.webp)
- Click Edit
- Set **Server Allowed IPs** to your VLAN subnets:  
```
172.16.10.0/24
172.16.20.0/24
172.16.30.0/24
172.16.50.0/24  
```  
![](https://cdn.developerinsider.co/images/wiki/homelab/wireguard/vps-tunnel/wg-easy-server-allowed-ip.webp)
- Set **MTU** to `1420` and **Persistent Keepalive** to `25`. Setting Persistent Keepalive to 25 is critical for CGNAT to keep the tunnel alive.  
![](https://cdn.developerinsider.co/images/wiki/homelab/wireguard/vps-tunnel/wg-easy-mtu-persistent-keepalive.webp)
- Save changes
- Restart the container (required for system routes):  
```bash  
sudo docker compose down && sudo docker compose up -d  
```
- Download the configuration file. You'll need the keys from it

## 4\. Create additional clients in wg-easy

Create additional clients, such as those on your mobile devices, that you can use to access your homelab via a VPS server.

- Add Client  
![](https://cdn.developerinsider.co/images/wiki/homelab/wireguard/vps-tunnel/wg-easy-new-client-phone.webp)
- Click Edit
- Set **Allowed IPs** to `0.0.0.0/0`, `::/0` for a full tunnel, where all internet traffic from your device routes through the VPS, or to `10.8.0.0/24`, `172.16.0.0/16` for split tunneling, where only homelab traffic goes through the VPS.
- Keep **Persistent Keepalive** to `0`
- Save changes
- Scan the QR code using the WireGuard app on your mobile device, or download and import the configuration file if you can't scan the QR code.

## 5\. Set Up the WireGuard Client on HomeLab

### 5.1 Create WireGuard client config directory

Create a directory for the configuration files (you can choose any directory you like):

```bash
sudo mkdir -p /etc/docker/containers/wg-client

```

```bash
cd /etc/docker/containers/wg-easy

```

### 5.2 Create compose file

```bash
sudo nano docker-compose.yml

```

On your HomeLab server, create this `docker-compose.yml`:

```yaml
services:
  wireguard:
    image: lscr.io/linuxserver/wireguard:latest
    container_name: wg-client
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Kolkata
    volumes:
      - /etc/docker/containers/wg-client:/config
      - /lib/modules:/lib/modules:ro
    network_mode: host

```

### 5.3 Create the WireGuard client config

Take the config you downloaded from wg-easy and modify it. Save it as `/etc/docker/containers/wg-client/wg_confs/wg0.conf`:

```
sudo mkdir -p wg_confs

```

```
sudo nano wg0.conf

```

```ini
[Interface]
PrivateKey = <from downloaded config>
Address = 10.8.0.2/32, fdcc:ad94:bacf:61a4::cafe:2/128
MTU = 1420

# Don't set DNS — HomeLab should use its own
# DNS = 1.1.1.1

# Enable forwarding for VPN traffic to reach HomeLab VLANs
PostUp   = iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -j MASQUERADE; iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -j MASQUERADE; iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT

[Peer]
PublicKey = <from downloaded config>
PresharedKey = <from downloaded config>
AllowedIPs = 10.8.0.0/24, fdcc:ad94:bacf:61a4::/112
Endpoint = <YOUR_VPS_IP>:51820
PersistentKeepalive = 25

```

**Three things changed from the downloaded config:**

1. **AllowedIPs** → `10.8.0.0/24` instead of `0.0.0.0/0`. You only want VPN traffic going through the tunnel, not all your HomeLab's internet.
2. **DNS** → removed. HomeLab keeps its own DNS.
3. **PostUp/PostDown** → added masquerade and forwarding rules.

### 5.4 Enable IP forwarding

```bash
echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.d/99-wireguard.conf
sudo sysctl -p /etc/sysctl.d/99-wireguard.conf

```

### Start WireGuard client

```bash
sudo docker compose up -d

```

## 6\. Verify

### 6.1 Check the tunnel on wg-easy

Open the wg-easy Web UI. The `HomeLab` client should show as **connected** with a recent handshake.

![](https://cdn.developerinsider.co/images/wiki/homelab/wireguard/vps-tunnel/wg-easy-clients.webp)

### 6.2 Test from VPS

From inside the container, try pinging your homelab WireGuard IP and a device on your homelab VLAN.

```bash
# HomeLab WireGuard Client IP
sudo docker exec wg-easy ping -c 3 10.8.0.2

# HomeLab VLAN 10
sudo docker exec wg-easy ping -c 3 172.16.10.1

```

### 6.3 Test from other devices

```
# VPS WireGuard IP
ping 10.8.0.1

# HomeLab WireGuard Client IP
ping 10.8.0.2

# HomeLab VLAN 10
ping 172.16.10.1

# Any device on VLAN 10
ping 172.16.10.100

```

If all pings work, you have full access to your HomeLab from anywhere.

## 7\. Troubleshooting

### 7.1 Tunnel won't establish

- Check if VPS firewall allows UDP connection on port 51820.
- Make sure the endpoint IP and port are correct.
- Cross-check all public and private keys in the WireGuard client interface and peer configuration with the keys generated by wg-easy.

### 7.2 Tunnel is up but can't reach Homelab VLANs from other devices

- Make sure **Server Allowed IPs** are set correctly in wg-easy and include your VLAN subnets. Also, restart the container after making changes.
- Verify route exists: `sudo docker exec wg-easy ip route | grep 172`. It should show `172.16.x.x dev wg0`.
- Other devices **AllowedIPs** must includes `172.16.0.0/16`.

### 7.3 VPS can ping 10.8.0.2 but not 172.16.x.x

This means **Server Allowed IPs** were added via the Web UI but system routes are missing. Restart the container:

```bash
sudo docker compose down && sudo docker compose up -d

```

### 7.4 Device shows connected but can't reach anything

Check the **AllowedIPs** in your device's WireGuard config:

- Full tunnel: `0.0.0.0/0, ::/0`
- Split tunnel: `10.8.0.0/24, 172.16.0.0/16`

## 8\. Optional: Access HomeLab from VPS Host

By default, only the wg-easy container can reach HomeLab through the tunnel. If you want to ping or access HomeLab services from the VPS host itself (e.g., `ssh 172.16.10.11` from the VPS CLI), you need three things:

### 8.1\. Add routes on the VPS host

```bash
sudo ip route add 10.8.0.0/24 via 10.42.42.42
sudo ip route add 172.16.0.0/16 via 10.42.42.42

```

### 8.2\. Allow forwarding through Docker

Docker's DOCKER-USER chain needs to allow WireGuard return traffic:

```bash
sudo iptables -I DOCKER-USER -i wg0 -j ACCEPT

```

### 8.3\. Masquerade inside the container

```bash
sudo docker exec wg-easy iptables -t nat -A POSTROUTING -o wg0 -s 10.42.42.0/24 -j MASQUERADE

```

Test it:

```bash
# HomeLab WireGuard IP
ping 10.8.0.2

# HomeLab device
ping 172.16.10.11

```

### Why it doesn't work out of the box

When you add a static route on the VPS host (`ip route add 172.16.0.0/16 via 10.42.42.42`), the traffic reaches the wg-easy container and goes through the tunnel. But the source IP is `10.42.42.1` (the Docker bridge gateway), which isn't in HomeLab's WireGuard AllowedIPs (`10.8.0.0/24`). So HomeLab receives the packet but the reply has nowhere to go and WireGuard drops it because `10.42.42.1` doesn't match any allowed route.

So, masquerade Docker bridge traffic inside the wg-easy container so it appears to come from `10.8.0.1` (the VPS's WireGuard IP, which is in AllowedIPs).

### Making it persistent

**Routes and DOCKER-USER rule** — create a systemd service:

```bash
cat <<'EOF' | sudo tee /etc/systemd/system/wg-routes.service
[Unit]
Description=Routes to HomeLab via wg-easy container
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStartPre=/bin/sleep 5
ExecStart=/sbin/ip route add 10.8.0.0/24 via 10.42.42.42
ExecStart=/sbin/ip route add 172.16.0.0/16 via 10.42.42.42
ExecStart=/sbin/iptables -I DOCKER-USER -i wg0 -j ACCEPT
ExecStop=/sbin/ip route del 10.8.0.0/24 via 10.42.42.42
ExecStop=/sbin/ip route del 172.16.0.0/16 via 10.42.42.42
ExecStop=/sbin/iptables -D DOCKER-USER -i wg0 -j ACCEPT

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now wg-routes.service

```

**Masquerade rule** — persist via wg-easy Hooks. In the Web UI, go to **Admin** → **Hooks**:

- **Post Up:**  
```  
iptables -t nat -A POSTROUTING -o wg0 -s 10.42.42.0/24 -j MASQUERADE  
```
- **Post Down:**  
```  
iptables -t nat -D POSTROUTING -o wg0 -s 10.42.42.0/24 -j MASQUERADE  
```

This ensures the masquerade rule is applied every time wg-easy starts.

## 9\. Optional: Access HomeLab From Other Docker Compose Stacks on Your VPS

Now if you want other Docker containers on the same VPS to reach your HomeLab services (172.16.x.x) through the tunnel, this can be achive by sharing a Docker network between wg-easy and your other stacks. Containers route HomeLab traffic to the wg-easy container, which forwards it through the tunnel.

```
┌──────────────────────────────────────────────────┐
│  VPS Host                                        │
│                                                  │
│  ┌────────────── wg-shared network ───────────┐  │
│  │                 10.42.42.0/24              │  │
│  │                                            │  │
│  │  wg-easy         app1          app2        │  │
│  │  10.42.42.42     10.42.42.x    10.42.42.y  │  │
│  │  (has wg0)                                 │  │
│  └────────────────────────────────────────────┘  │
│                                                  │
│  app-1 & app-2 route 172.16.0.0/16               │
│  via 10.42.42.42 → wg0 tunnel → HomeLab          │
└──────────────────────────────────────────────────┘

```

### 9.1\. Create a Shared Docker Network

Create it manually so it exists independently of any Compose stack:

```bash
docker network create \
  --driver bridge \
  --subnet 10.42.42.0/24 \
  --gateway 10.42.42.1 \
  --ipv6 --subnet fdcc:ad94:bacf:61a3::/64 \
  wg-shared

```

### 9.2\. Update the wg-easy Compose File

```yaml
services:
  wg-easy:
    container_name: wg-easy
    ...
    networks:
      wg:
        ipv4_address: 10.42.42.42
        ipv6_address: fdcc:ad94:bacf:61a3::2a

networks:
  wg:
    external: true
    name: wg-shared

```

Restart wg-easy:

```bash
sudo docker compose down && sudo docker compose up -d

```

### 9.3\. Add the Masquerade Rule in wg-easy

Traffic from other containers has source IPs like `10.42.42.x`. The HomeLab's WireGuard only accepts traffic from `10.8.0.0/24`. A masquerade rule makes the traffic appear as `10.8.0.1` (the WireGuard server IP).

Add this via the wg-easy Web UI:

1. Go to **Admin** → **Hooks**
2. In **PostUp**, append:  
```  
iptables -t nat -A POSTROUTING -o wg0 -s 10.42.42.0/24 -j MASQUERADE;  
```
3. In **PostDown**, append:  
```  
iptables -t nat -D POSTROUTING -o wg0 -s 10.42.42.0/24 -j MASQUERADE;  
```
4. **Save**

### 9.4\. Join Other Stacks to the Shared Network

In any Compose stack that needs HomeLab access, add the `wg-shared` network:

```yaml
services:
  app1:
    ...
    cap_add:
      - NET_ADMIN
    networks:
      wg:
        ipv4_address: 10.42.42.10

  app2:
    ...
    cap_add:
      - NET_ADMIN
    networks:
      wg:
        ipv4_address: 10.42.42.11

networks:
  wg:
    external: true
    name: wg-shared

```

Key points:

- You can add `default` network as well, so the containers can still talk to each other within their own stack. Add `wg` as a second network for HomeLab routing.
- Add `cap_add: NET_ADMIN` to add routes inside the container.

## 9.5\. Add Routes Inside the Containers

Containers need to know that HomeLab traffic goes via `10.42.42.42`. Override the entrypoint to add routes before starting the app:

```yaml
services:
  app1:
    ...
    cap_add:
      - NET_ADMIN
    networks:
      wg:
        ipv4_address: 10.42.42.10
    entrypoint: >
      /bin/sh -c "
        ip route add 10.8.0.0/24 via 10.42.42.42 || true;
        ip route add 172.16.0.0/16 via 10.42.42.42 || true;
        exec your-original-command
      "

```

Replace `your-original-command` with the container's default CMD. Find it with:

```bash
docker inspect your-app-image --format '{{json .Config.Cmd}}'

```

The `|| true` prevents failure if the route already exists. `exec` replaces the shell so signals (SIGTERM, etc.) reach the app correctly.

### 9.6\. Verify

```bash
# Check routes inside the container
docker exec my-app ip route | grep -E "10.8|172.16"
# Should show:
# 10.8.0.0/24 via 10.42.42.42 dev eth1
# 172.16.0.0/16 via 10.42.42.42 dev eth1

# From any container on wg-shared network

# wg-easy container
docker exec my-app ping -c 3 10.42.42.42

# HomeLab WireGuard IP
docker exec my-app ping -c 3 10.8.0.2

# HomeLab device
docker exec my-app ping -c 3 172.16.10.11

```