Home VPN: WireGuard in a Proxmox container for my phone (rebuild guide) 05OCT2026

Posted by on 5th Oct 2026

A small WireGuard VPN server in its own Proxmox container, reached as vpn.503mail.com, so my Android phone can get to everything on my home network from anywhere. It replaced a SoftEther server in a 4 GB Windows VM whose management window had no dark mode. This post is the complete rebuild guide, and every code block has a Copy button.

How it works

  1. The phone runs the free WireGuard app with two tunnels that share one key:
    • home sends only home-network traffic (the LAN and the tunnel subnet) through the VPN. Normal internet goes direct, which saves battery.
    • home-all sends all traffic through home, using the home router for DNS. Good on public Wi-Fi. Only one can be on at a time, so picking one in the app acts as the "all traffic" toggle.
  2. The phone sends encrypted UDP packets to vpn.503mail.com port 51820. That name is covered by the wildcard DNS record for 503mail.com, which points at the home public IP.
  3. The home router forwards UDP 51820 to the WireGuard container.
  4. The container decrypts the packets and NATs them out of its LAN interface (iptables MASQUERADE), so the rest of the network sees them coming from the container and no LAN routes are needed.
  5. Connecting takes under a second. The phone sends a keepalive every 25 seconds, so the tunnel survives switching between Wi-Fi and mobile data.

Containers involved

Container Role LAN IP
CT 302 (wireguard) WireGuard server, Debian 12, unprivileged x.x.0.236 (static)

Sizing: 1 core, 256 MB RAM, 4 GB disk. It uses about 20 MB of RAM. The tunnel subnet is 10.8.0.x: the server is .1, the phone .2, and each new device gets the next number.

DNS: vpn.503mail.com needed no new record, because 503mail.com has a wildcard A record (DNS is on AWS Route 53). If the home IP changes, that record has to be updated by hand for now.

Where to run each command

  • Proxmox host shell: in the Proxmox web UI click pve → Shell (or SSH to the host as root).
  • Inside CT 302 (wireguard): Proxmox web UI → 302 (wireguard) → Console, or pct enter 302 from the host shell.

Rebuild, step by step

1. Make a working folder on the host

Proxmox host shell:

mkdir -p /root/wireguard-vpn && cd /root/wireguard-vpn

2. Create every file

Paste each block into the Proxmox host shell. Each one writes one file into /root/wireguard-vpn/.

create-ct.sh: runs on the host. Loads the WireGuard kernel module, creates the container, copies in the other two scripts, runs them and copies the phone's tunnel files back out.

cat > /root/wireguard-vpn/create-ct.sh <<'EOF'
#!/bin/bash
# Run on the Proxmox host: creates the WireGuard container and sets it up.
# Usage: bash create-ct.sh <ctid> <ip/cidr or dhcp>
#   e.g. bash create-ct.sh 302 <free LAN address>/24
set -e
CTID=${1:?usage: create-ct.sh <ctid> <ip/cidr or dhcp>}
IPCIDR=${2:?usage: create-ct.sh <ctid> <ip/cidr or dhcp>}
HERE=$(cd "$(dirname "$0")" && pwd)
TEMPLATE=local:vztmpl/debian-12-standard_12.12-1_amd64.tar.zst

# WireGuard runs in the host kernel; the container only creates the interface.
modprobe wireguard
echo wireguard > /etc/modules-load.d/wireguard.conf

if [ "$IPCIDR" = dhcp ]; then
  NET="ip=dhcp"
else
  GW=$(ip -4 route show default | awk '{print $3; exit}')
  NET="ip=$IPCIDR,gw=$GW"
fi

pveam list local | grep -q debian-12-standard_12.12-1 || pveam download local debian-12-standard_12.12-1_amd64.tar.zst

pct create "$CTID" "$TEMPLATE" \
  --hostname wireguard --cores 1 --memory 256 --swap 256 \
  --rootfs local-lvm:4 --net0 "name=eth0,bridge=vmbr0,$NET" \
  --unprivileged 1 --features nesting=1 --onboot 1 --ostype debian
pct start "$CTID"
sleep 10

pct push "$CTID" "$HERE/setup-wg.sh" /root/setup-wg.sh
pct push "$CTID" "$HERE/add-peer.sh" /usr/local/bin/add-peer --perms 755
pct exec "$CTID" -- bash /root/setup-wg.sh
pct exec "$CTID" -- /usr/local/bin/add-peer phone 2

pct pull "$CTID" /etc/wireguard/clients/phone.zip "$HERE/phone.zip"
echo "Phone configs: $HERE/phone.zip"
EOF

setup-wg.sh: runs inside the container. Installs WireGuard, turns on IP forwarding, makes the server key and wg0.conf, and starts the service.

cat > /root/wireguard-vpn/setup-wg.sh <<'EOF_OUTER1'
#!/bin/bash
# Run inside the WireGuard container: installs WireGuard and writes the server config.
set -e
export DEBIAN_FRONTEND=noninteractive
VPN_NET=10.8.0      # tunnel subnet: server .1, peers .2 and up
PORT=51820

apt-get update -q
# --no-install-recommends: otherwise wireguard-tools pulls in a whole Linux kernel image
apt-get install -y -q --no-install-recommends wireguard-tools iptables qrencode zip

echo 'net.ipv4.ip_forward=1' > /etc/sysctl.d/99-wg.conf
sysctl -p /etc/sysctl.d/99-wg.conf

umask 077
mkdir -p /etc/wireguard/clients
cd /etc/wireguard
[ -f server.key ] || wg genkey > server.key
wg pubkey < server.key > server.pub

[ -f wg0.conf ] || cat > wg0.conf <<EOF
[Interface]
Address = $VPN_NET.1/24
ListenPort = $PORT
PrivateKey = $(cat server.key)
PostUp = iptables -t nat -A POSTROUTING -s $VPN_NET.0/24 -o eth0 -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -s $VPN_NET.0/24 -o eth0 -j MASQUERADE
EOF

systemctl enable --now wg-quick@wg0
wg show
EOF_OUTER1

add-peer.sh: installed in the container as add-peer. Adds one device and makes a zip with its home and home-all tunnel files. The LAN subnet and router address are looked up when it runs.

cat > /root/wireguard-vpn/add-peer.sh <<'EOF_OUTER1'
#!/bin/bash
# Run inside the WireGuard container: adds a device and makes its two tunnel configs.
# Usage: add-peer <name> <number 2-254>
#   <name>-home.conf      only home-network traffic goes through the VPN
#   <name>-home-all.conf  all traffic goes through the VPN
# Both share one key, so only one can be on at a time.
set -e
NAME=${1:?usage: add-peer <name> <number>}
N=${2:?usage: add-peer <name> <number>}
VPN_NET=10.8.0
ENDPOINT=vpn.503mail.com:51820

cd /etc/wireguard
umask 077
LAN=$(ip -4 route show dev eth0 scope link | awk '{print $1; exit}')
GW=$(ip -4 route show default | awk '{print $3; exit}')
wg genkey > clients/$NAME.key
wg pubkey < clients/$NAME.key > clients/$NAME.pub

cat >> wg0.conf <<EOF

[Peer]
# $NAME
PublicKey = $(cat clients/$NAME.pub)
AllowedIPs = $VPN_NET.$N/32
EOF
wg set wg0 peer "$(cat clients/$NAME.pub)" allowed-ips "$VPN_NET.$N/32"

client() {  # $1 = AllowedIPs, $2 = extra [Interface] line
  cat <<EOF
[Interface]
PrivateKey = $(cat clients/$NAME.key)
Address = $VPN_NET.$N/32
$2

[Peer]
PublicKey = $(cat server.pub)
Endpoint = $ENDPOINT
AllowedIPs = $1
PersistentKeepalive = 25
EOF
}
mkdir -p /tmp/$NAME
client "$LAN, $VPN_NET.0/24" "" > /tmp/$NAME/home.conf
client "0.0.0.0/0" "DNS = $GW" > /tmp/$NAME/home-all.conf
(cd /tmp/$NAME && zip -q -j /etc/wireguard/clients/$NAME.zip home.conf home-all.conf)
rm -r /tmp/$NAME
echo "Made /etc/wireguard/clients/$NAME.zip (tunnels: home, home-all)"
EOF_OUTER1

3. Build the container

Proxmox host shell. Replace the address with a free static LAN address and its /24, the way you'd write it for any container (for example the one the router handed out before):

bash /root/wireguard-vpn/create-ct.sh 302 YOUR.LAN.IP.ADDRESS/24

It takes about a minute. The end of the output shows interface: wg0 with listening port: 51820 and one peer, then the path of phone.zip.

4. Forward the port on the router

On the Calix router (http://x.x.0.1 → Advanced → Security → Port Forwarding):

  • Choose IP Address (not Device: the container uses a static address, so it isn't in the device list) and type the container's address.
  • Protocol TCP/UDP (or UDP). WAN and LAN ports both 51820 to 51820. Remote IP All IP Addresses. Click Apply.
  • Check the table at the bottom: the new row has to say UDP or TCP/UDP. My first try saved as TCP only and nothing could connect.

5. Put the tunnels on the phone

  1. Install WireGuard (by WireGuard Development Team) from the Play Store.
  2. Copy /root/wireguard-vpn/phone.zip to the phone's Download folder, for example with adb push phone.zip /sdcard/Download/ from the PC, or by email to yourself.
  3. In WireGuard tap + → Import from file or archive → pick phone.zip. Both tunnels, home and home-all, appear.
  4. Switch home on. Android asks once to allow a VPN connection: tap OK. The switch may flip back off after that first OK; switch it on again.
  5. Delete phone.zip from the phone and from the host afterwards, because it contains the private key.

Proxmox host shell, after the phone has it:

unlink /root/wireguard-vpn/phone.zip

6. Test

Turn off Wi-Fi on the phone so it uses mobile data, switch on home, and open http://x.x.0.195:8006 (the Proxmox login page) in the browser. Then check from the server that the phone connected from a mobile-carrier address. Proxmox host shell:

pct exec 302 -- wg show

7. Add another device later

Inside CT 302 (wireguard). Give each device its own name and the next free number (the phone is 2):

add-peer laptop 3

Then copy the zip out to the host. Proxmox host shell:

pct pull 302 /etc/wireguard/clients/laptop.zip /root/laptop.zip

Troubleshooting: where the logs are

WireGuard status (is the phone connected?)

  • Where: live kernel state in CT 302 (wireguard). WireGuard itself logs almost nothing.
  • Open: Proxmox host shell.
pct exec 302 -- wg show
  • Look for: under the peer, endpoint: (the phone's current public address), latest handshake: N seconds ago (under 2 minutes means connected) and transfer: going up. No latest handshake line at all means no packet ever arrived: check the router port forward (UDP!) and that the tunnel switch on the phone is really on.

Are packets arriving at all?

  • Where: network traffic on the Proxmox bridge.
  • Open: Proxmox host shell. Switch the phone's tunnel off and on while this runs.
timeout 20 tcpdump -ni vmbr0 udp port 51820
  • Look for: lines from an outside address to the container on port 51820. Nothing at all means the router isn't forwarding. Packets in but no handshake means the keys don't match: re-import the zip.

Service log

  • Where: the systemd journal in CT 302 (wireguard), service wg-quick@wg0.
  • Open: Proxmox host shell.
pct exec 302 -- journalctl -u wg-quick@wg0 -n 50 --no-pager
  • Look for: ip link add wg0 type wireguard and the iptables ... MASQUERADE line, then nothing else (healthy). RTNETLINK answers: Not supported means the host's WireGuard module isn't loaded: run modprobe wireguard in the host shell and restart the service.

Phone side

  • In the app, tap the tunnel name: the details page shows Latest handshake and Transfer at the bottom when it's connected.

Quick health check

Proxmox host shell:

pct exec 302 -- wg show wg0 latest-handshakes

Notes

  • apt pulled in a whole Linux kernel. wireguard-tools recommends a kernel image on Debian 12, which put 110 MB of useless kernel into the container on the first try. setup-wg.sh uses --no-install-recommends to avoid it.
  • The container needs the host's kernel module. The container only creates the interface; the WireGuard code runs in the Proxmox kernel. create-ct.sh loads it now and at every boot through /etc/modules-load.d/wireguard.conf.
  • pct exec doesn't search /usr/local/bin, so from the host call /usr/local/bin/add-peer with the full path. In the container console, plain add-peer works.
  • Testing from home Wi-Fi works too, because the Calix router sends traffic for the public IP back inside (hairpin NAT). The real test is still mobile data.
  • Android turns wireless debugging off whenever the phone changes networks, so ADB to the phone stops after a Wi-Fi off/on test until it's switched back on.
  • This replaced SoftEther. SoftEther does run on Linux too, but its management is a Windows GUI (or the vpncmd command line), and the phone needs the OpenVPN app plus a hand-edited profile. WireGuard is one small app and one file to import.