Self Hosting With Wireguard and Docker

Hosting a service in your home or office can save you a few bucks and give you more control and privacy. The obvious problem with that is the lack of a public IP address or your ISP filtering inbound traffic. Even if you have a public IP, it might be dynamic and change regularly – and dynamic DNS isn’t always reliable or updated quickly.

Wireguard to a tiny VM with a static public IP address running a reverse-proxy for your private server can be a simple solution. However, you probably don’t want to expose your entire home or office network with a Wireguard VPN terminated at your firewall. You probably don’t even want Wireguard terminated on your home server!

Using Wireguard terminated in a docker container providing connectivity for an isolated Docker network seems like a good compromise.

I couldn’t find a complete and trivial example of this setup, so I’ve documented a very simple setup here using Nginx as a reverse proxy on a public VM connecting with Wireguard to a Wireguard docker container sharing a network with an Nginx “Hello” container.

Public Server Wireguard

We’ll setup the public server’s Wireguard first, but you will need the public key you generate on the private server to complete the public server setup. I like to change the ListenPort from default. You will need to allow this port through the host firewall.

dnf install wireguard-tools -y
wg genkey | tee privatekey | wg pubkey | tee publickey

Create /etc/wireguard/wg0.conf:

[Interface]
Address = 10.0.0.1/32
ListenPort = 55555
PrivateKey = <private key generated above>

[Peer]
AllowedIPs = 10.0.0.2/32
PublicKey = <public key from private server>

Enable and start Wireguard:

systemctl enable wg-quick@wg0
systemctl start wg-quick@wg0

Private Server Wireguard

Create the Wireguard container config directory and generate client keys:

mkdir -p config/wg_confs
wg genkey | tee privatekey | wg pubkey | tee publickey

Create config/wg_confs/wg0.conf:

[Interface]
Address = 10.0.0.2/32
PrivateKey = <replace with private key generated above>

[Peer]
PublicKey = <replace with public key from the public server>
Endpoint = publicserver.com:55555
PersistentKeepalive = 10

Note that PersistentKeepalive is critical for this setup, as it will force the private server to keep the connection open behind NAT even as the IP changes.

Private Server Docker

Create a docker compose file in test.yml

services:
  wireguard:
    image: lscr.io/linuxserver/wireguard:latest
    container_name: wireguard
    cap_add:
      - NET_ADMIN
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Etc/UTC
    volumes:
      - ./config:/config
      - /lib/modules:/lib/modules
    sysctls:
      - net.ipv4.conf.all.src_valid_mark=1
    restart: unless-stopped
  hello:
    image: nginxdemos/hello
    network_mode: service:wireguard

Public Server Nginx

Create an Nginx config in /etc/nginx/conf.d:

server {

  server_name test.publicserver.com;

  location / {
    proxy_pass http://10.0.0.4:80;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }

}

Start it up

docker-compose -f test.yml up -d

You should now see the test page generated by the Nginx Hello container displayed on your public server at test.publicserver.com. If you don’t, try using:

  1. ping from either side to the wireguard private IP on the opposite side.
  2. wg to check the connection status
  3. curl to check reachability of the actual Docker service on its port, in this case curl http://10.0.0.2