Skip to content
View in the app

A better way to browse. Learn more.

Unraid

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.

wireguard on a container, but with local docker network access

Featured Replies

Greetings - I have a working wireguard vpn tunnel set up through unraid's vpn config.

Is there a way with unraid default config (not using something like gluetun) to allow some containers to use that wg tunnel to connect to WAN, while still allowing them to remain on the docker network's subnet?

Currently, if I switch my container's network to wgX, it is no longer visible to my other dockers.

Solved by underw0rld

  • Community Expert

Brandon Martino - Personal Site
No image preview

Guide-DockerNetworks

Brandon Martino - Personal Site

you will need to implemnt #6
6) --network=container:<name> (share another container’s network)

and use a CLI to Connect container to network

I would recoemnd using compose where we can define more then 1 network added to a docker.

Edited by bmartino1

  • Community Expert

you can do this with policy routing + marks on the Unraid host so only the containers you choose egress over wg0, while they stay on a normal Docker bridge (so they’re still reachable by your other containers).

Overview (what we’re doing)

  1. Keep containers on a user-defined Docker bridge (not wgX). That preserves service discovery and reachability.

  2. Give VPN-bound containers static IPs on that bridge (easy to target with firewall rules).

  3. Add policy routing on the host:

    • Mark packets with those source IPs only when they go to the internet (exclude RFC1918 etc).

    • Route marked packets out the WireGuard interface (wg0).

    • Masquerade out wg0 so replies return properly.

  4. Do not touch intra-Docker / LAN traffic, so containers still see each other on the bridge.

If I understand you correctly.

When dealing with vpn docker on unraid Prefer to use docker copoase

image.png

Docker Compose (example) to go over and reveiw...

#version: "3.8"

networks:
  appnet:
    driver: bridge
    ipam:
      config:
        - subnet: 172.23.0.0/24
          gateway: 172.23.0.1

services:
  qbittorrent:
    image: lscr.io/linuxserver/qbittorrent:latest
    container_name: qbittorrent
    networks:
      appnet:
        ipv4_address: 172.23.0.10   # <-- this one will be routed via wg0
    ports:
      - "8080:8080"                 # example; expose if you actually need host access
    environment:
      - PUID=99
      - PGID=100
      - TZ=America/Chicago
    restart: unless-stopped

  prowlarr:
    image: lscr.io/linuxserver/prowlarr:latest
    container_name: prowlarr
    networks:
      appnet:
        ipv4_address: 172.23.0.11   # <-- also via wg0
    restart: unless-stopped

  plex:
    image: lscr.io/linuxserver/plex:latest
    container_name: plex
    networks:
      appnet: {}                    # dynamic IP (not via wg0)
    restart: unless-stopped

This creates a dedicated user bridge appnet (172.23.0.0/24) and pins a couple of “via-VPN” app IPs so we can match them.

  • All 3 are on the same Docker bridge → they can reach each other by container name/IP.

  • Only the IPs you list (e.g., 172.23.0.10, .11) will be policy-routed out wg0.

If you already have a bridge you like, reuse it — just choose static IPs from that subnet for the “over-VPN” containers.

Double-check your Wiregurad unraid host settings:

My understanding is that your using urnaid vpn manger for your wire guard VPN
image.png


WireGuard & host routing glue

A. Make sure WireGuard can NAT out

If you used Unraid’s WG “Remote access to server” or similar, it likely added MASQUERADE already. If not, we’ll add it in our script below.

If your /etc/wireguard/wg0.conf is managed by Unraid, don’t hand-edit it. We’ll add rules at array start using the User Scripts plugin so they persist.

some can be made in the web UI, not a fan of unraids web ui... and hard to share and give examples with our security PHI issues...

B. Policy routing script (drop into User Scripts → “At Startup of Array”)

This marks packets from the selected container IPs to the WAN (not local ranges) and sends them through table 51820 (our WG table).

user script:

#!/bin/bash
set -e

WG_IF="wg0"
WG_TABLE="51820"
MARK_HEX="0xC8E4"        # arbitrary mark value
MARK_DEC=$((0xC8E4))

# List the container IPs that must egress via WireGuard
VPN_SRC_IPS=(
  172.23.0.10
  172.23.0.11
)

# --- Set up routing table for WireGuard ---
# Add default route via wg0 in our custom table
ip -t route show table ${WG_TABLE} | grep -q "default" || ip route add default dev ${WG_IF} table ${WG_TABLE} || true

# Add policy rule to use that table for marked packets
ip rule show | grep -q "fwmark ${MARK_DEC} lookup ${WG_TABLE}" || ip rule add fwmark ${MARK_DEC} lookup ${WG_TABLE}

# --- iptables chains (idempotent) ---
iptables -t mangle -C PREROUTING -j WG_VPN 2>/dev/null || iptables -t mangle -N WG_VPN
iptables -t mangle -C PREROUTING -j WG_VPN 2>/dev/null || iptables -t mangle -A PREROUTING -j WG_VPN
iptables -t mangle -C OUTPUT -j WG_VPN 2>/dev/null     || iptables -t mangle -A OUTPUT -j WG_VPN

# Exempt local/LAN destinations early (so LAN stays LAN)
for cidr in 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 100.64.0.0/10 127.0.0.0/8; do
  iptables -t mangle -C WG_VPN -d $cidr -j RETURN 2>/dev/null || iptables -t mangle -A WG_VPN -d $cidr -j RETURN
done

# Mark traffic from the chosen container IPs
for ip in "${VPN_SRC_IPS[@]}"; do
  iptables -t mangle -C WG_VPN -s ${ip} -m mark --mark 0x0 -j MARK --set-mark ${MARK_HEX} 2>/dev/null \
    || iptables -t mangle -A WG_VPN -s ${ip} -m mark --mark 0x0 -j MARK --set-mark ${MARK_HEX}
done

# NAT/MASQUERADE out the WireGuard interface for internet return path
iptables -t nat -C POSTROUTING -o ${WG_IF} -j MASQUERADE 2>/dev/null || iptables -t nat -A POSTROUTING -o ${WG_IF} -j MASQUERADE

echo "Selective WireGuard policy routing is active."

What this does

  • Creates routing table 51820 with default via wg0.

  • Adds an ip rule to use that table when packets carry our mark.

  • Uses the DOCKER-friendly path (PREROUTING and OUTPUT via a custom WG_VPN chain) to:

    • Immediately return traffic destined to private/LAN ranges (so local stays local).

    • Mark traffic that originates from the listed container IPs.

  • MASQUERADE out wg0 so replies come back properly through the tunnel.

If you reboot or restart the array, the rules will be re-applied automatically by the User Script.

How to choose which containers use the VPN

  1. Just add their IP to VPN_SRC_IPS in the script and give them that static IP on your Docker bridge in compose.

  2. Remove an IP from the list to stop sending it over WG.

  3. This scales well: you can target a whole subnet (e.g., -s 172.23.0.0/24) if you create a dedicated “vpn-apps” bridge. I usually keep it to a handful of pinned IPs.

Notes & gotchas

  • Containers remain discoverable on the bridge and can talk to other containers exactly as before.

  • Only WAN traffic (non-RFC1918/CGNAT/loopback) from the selected container IPs gets forced over wg0.

  • If a container exposes ports to the host (e.g., -p 8080:8080), that still works — your browser → host → container is LAN and won’t get marked.

  • If your WG config already does NAT or has special firewalling, keep those, but the script’s MASQUERADE is usually necessary.

  • If you run additional WG tunnels, repeat the pattern with a different WG_TABLE/MARK_HEX/interface.

Quick test

# See rules
ip rule
ip route show table 51820
iptables -t mangle -S WG_VPN
iptables -t nat -S | grep MASQUERADE

# Exec into a VPN-bound container and check exit IP: Example
docker exec -it qbittorrent bash
curl -4 https://ifconfig.io
# Should show your WireGuard egress IP

you can use docker network inspect <network name>

and get the docker IP and sue unraid temple the static asign one...

Edited by bmartino1
forum weridness with picture upload.

  • Author

@bmartino1 thank you for your detailed post - As you assumed I already had a wireguard config set up within unraid, so I stuck with that instead of making a new docker network. It is currently set to the mode "VPN access for docker" (let me know if that is problematic).

What is odd is that when the script is running, the unraid server cannot touch the internet. I thought it may be that only the whitelisted vpn IPS could, but even those do not seem to access the wan.

I modified your script in 3 ways:

changing wg0 to wg1

adding a couple IPs to pass through,

and i changed the subnets that are granted local access to my docker network's subnet:

#!/bin/bash

set -e

WG_IF="wg1"

WG_TABLE="51820"

MARK_HEX="0xC8E4" # arbitrary mark value

MARK_DEC=$((0xC8E4))

# List the container IPs that must egress via WireGuard

VPN_SRC_IPS=(

172.18.0.100

172.18.0.101

)

# --- Set up routing table for WireGuard ---

# Add default route via wg0 in our custom table

ip -t route show table ${WG_TABLE} | grep -q "default" || ip route add default dev ${WG_IF} table ${WG_TABLE} || true

# Add policy rule to use that table for marked packets

ip rule show | grep -q "fwmark ${MARK_DEC} lookup ${WG_TABLE}" || ip rule add fwmark ${MARK_DEC} lookup ${WG_TABLE}

# --- iptables chains (idempotent) ---

iptables -t mangle -C PREROUTING -j WG_VPN 2>/dev/null || iptables -t mangle -N WG_VPN

iptables -t mangle -C PREROUTING -j WG_VPN 2>/dev/null || iptables -t mangle -A PREROUTING -j WG_VPN

iptables -t mangle -C OUTPUT -j WG_VPN 2>/dev/null || iptables -t mangle -A OUTPUT -j WG_VPN

# Exempt local/LAN destinations early (so LAN stays LAN)

for cidr in 10.0.0.0/8 172.18.0.0/12 192.168.0.0/16 100.64.0.0/10 127.0.0.0/8; do

iptables -t mangle -C WG_VPN -d $cidr -j RETURN 2>/dev/null || iptables -t mangle -A WG_VPN -d $cidr -j RETURN

done

# Mark traffic from the chosen container IPs

for ip in "${VPN_SRC_IPS[@]}"; do

iptables -t mangle -C WG_VPN -s ${ip} -m mark --mark 0x0 -j MARK --set-mark ${MARK_HEX} 2>/dev/null \

|| iptables -t mangle -A WG_VPN -s ${ip} -m mark --mark 0x0 -j MARK --set-mark ${MARK_HEX}

done

# NAT/MASQUERADE out the WireGuard interface for internet return path

iptables -t nat -C POSTROUTING -o ${WG_IF} -j MASQUERADE 2>/dev/null || iptables -t nat -A POSTROUTING -o ${WG_IF} -j MASQUERADE

echo "Selective WireGuard policy routing is active."

when I run ip rule, I don't see 51820

root@BLADE:~# ip rule

0: from all lookup local

32760: from all fwmark 0xc8e4 lookup 51820

32761: from 172.31.202.0/24 lookup 202

32762: from all lookup main suppress_prefixlength 0

32763: not from all fwmark 0xca6c lookup 51820

32764: from 172.31.201.0/24 lookup 201

32765: from 172.31.200.0/24 lookup 200

32766: from all lookup main

32767: from all lookup default

also, when I run iptables -t mangle -S WG_VPN, for some reason the subnet you specified is still there and not reflecting my change to 172.18.* (ofc I have rebooted the server):

root@BLADE:~# iptables -t mangle -S WG_VPN

-N WG_VPN

-A WG_VPN -d 10.0.0.0/8 -j RETURN

-A WG_VPN -d 172.16.0.0/12 -j RETURN

-A WG_VPN -d 192.168.0.0/16 -j RETURN

-A WG_VPN -d 100.64.0.0/10 -j RETURN

-A WG_VPN -d 127.0.0.0/8 -j RETURN

-A WG_VPN -s 172.18.0.100/32 -m mark --mark 0x0 -j MARK --set-xmark 0xc8e4/0xffffffff

-A WG_VPN -s 172.18.0.101/32 -m mark --mark 0x0 -j MARK --set-xmark 0xc8e4/0xffffffff

any idea what I missed here? thanks again for your help

  • Community Expert

The symptoms you’re seeing line up with two overlapping issues:

a rule/mark collision with Unraid’s “VPN tunneled access for docker” mode, and

your WG_VPN chain is additive (it never deletes the old entries), so your later edits don’t show up—and the broad RFC1918 exemption you saw is from the previous run

Also: you do have a 51820 rule—your ip rule output shows it at priority 32760:

So that part looks right.

What to change in Unraid first

  • In your WireGuard tunnel, turn OFF “VPN tunneled access for docker.”
    That mode installs its own fwmark/table 51820 rules (you can see fwmark 0xca6c in your ip rule dump) and will fight our policy routing. We’ll do the container selection ourselves.

  • Make sure the wg interface you’re using (wg1) is up and the peer allows 0.0.0.0/0 (or at least the destinations you expect) in AllowedIPs, with NAT enabled on the peer side if needed. If the peer doesn’t route 0.0.0.0/0, adding a “default dev wg1” in table 51820 won’t actually get you to the Internet.

    image.png

image.png

Drop-in script (replace your current one)

Use this on array start via the User Scripts plugin.

#!/bin/bash
set -euo pipefail

WG_IF="wg1"
WG_TABLE="51820"

# Arbitrary mark value (hex + dec)
MARK_HEX="0xC8E4"
MARK_DEC=$((0xC8E4))

# Container IPs that should egress via WireGuard
VPN_SRC_IPS=(
  172.18.0.100
  172.18.0.101
)

# 1) Make sure replies from policy-routed flows are allowed
#    (wg-quick usually sets this; do it here to be safe)
sysctl -w net.ipv4.conf.all.src_valid_mark=1 >/dev/null

# 2) Ensure a default route in our custom table
if ! ip -4 route show table "${WG_TABLE}" | grep -qE '^default '; then
  ip -4 route add default dev "${WG_IF}" table "${WG_TABLE}" || true
fi

# 3) Install the policy rule for our mark -> table 51820
if ! ip -4 rule show | grep -q "fwmark ${MARK_DEC} lookup ${WG_TABLE}"; then
  ip -4 rule add fwmark "${MARK_DEC}" lookup "${WG_TABLE}"
fi

# 4) Create (or flush+rebuild) the WG_VPN chain so edits take effect
#    We flush to avoid stacking old exemptions/marks forever.
if iptables -t mangle -nL WG_VPN >/dev/null 2>&1; then
  iptables -t mangle -F WG_VPN
else
  iptables -t mangle -N WG_VPN
fi

# 5) Make sure PREROUTING and OUTPUT jump into WG_VPN once
for hook in PREROUTING OUTPUT; do
  iptables -t mangle -C "${hook}" -j WG_VPN 2>/dev/null || \
  iptables -t mangle -A "${hook}" -j WG_VPN
done

# 6) Exempt LAN/RFC1918/bogon/etc so containers still talk to local Docker/LAN directly
#    Keep the full RFC1918 ranges; they already include 172.18.0.0/16.
for cidr in 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 100.64.0.0/10 127.0.0.0/8; do
  iptables -t mangle -A WG_VPN -d "$cidr" -j RETURN
done

# 7) Mark only the selected container sources
for ip in "${VPN_SRC_IPS[@]}"; do
  iptables -t mangle -A WG_VPN -s "${ip}" -m mark --mark 0x0 -j MARK --set-mark "${MARK_HEX}"
done

# 8) Never mark traffic coming *from* the WireGuard interface itself
iptables -t mangle -A WG_VPN -i "${WG_IF}" -j RETURN

# 9) NAT out the WireGuard interface for return path symmetry
iptables -t nat -C POSTROUTING -o "${WG_IF}" -j MASQUERADE 2>/dev/null || \
iptables -t nat -A POSTROUTING -o "${WG_IF}" -j MASQUERADE

echo "Selective WireGuard policy routing active on ${WG_IF} for: ${VPN_SRC_IPS[*]}"

Why these changes fix your issues

  • Unraid VPN-for-Docker collision: Your ip rule showed an extra rule with fwmark 0xca6c pointing to the same table 51820. That’s Unraid’s Docker-over-WG mode, and it can grab traffic you didn’t intend (and also modifies NAT rules). Turning it off removes that interference so only our 0xC8E4 marks use table 51820.

  • Chain flushing: You changed the exemption list (e.g., to 172.18.*), but your earlier run had already inserted 172.16.0.0/12. Because your script only adds and never deletes, the old broader exemption stuck around. Flushing WG_VPN each run makes the chain reflect your current intent.

  • RFC1918 exemptions: Keep 172.16.0.0/12 (it already includes 172.18.0.0/16). Replacing it with 172.18.0.0/12 is not what you want (and is an invalid supernet for a /12 starting at .18). The broad RFC1918 returns ensure inter-container and LAN access stays local, while only Internet egress is policy-routed.

  • Host losing Internet: With the collision removed and the chain fixed, host-originated traffic (which doesn’t match -s 172.18.0.100/101) won’t be marked, so it will use the main table as usual. The OUTPUT -> WG_VPN jump is safe because we only mark on the -s matches. If you still want to be extra cautious, you can remove the OUTPUT jump entirely and rely solely on PREROUTING (containers hit PREROUTING on ingress to the host’s routing decision anyway).

Quick verification steps

ip -4 route show table 51820

# should show: default dev wg1 (plus any on-link routes)

ip -4 rule

# look for: from all fwmark 0xc8e4 lookup 51820

iptables -t mangle -S WG_VPN

iptables -t nat -S | grep MASQUERADE

ip route get 1.1.1.1 from 172.18.0.100

# Should resolve via wg1 (table 51820) if the peer routes 0.0.0.0/0

  • If the 4 step shows it would go via wg1 but the container still can’t reach the WAN, double-check the peer’s AllowedIPs/NAT. Many commercial or upstream peers require you to allow 0.0.0.0/0 and/or enable NAT on their side for full Internet egress.

  • 2 weeks later...
  • Author
  • Solution

For anyone who also wants to achieve this, I found a solution that is much easier (imo).

  • Create a wireguard connection to your vpn, using "VPN Tunneled Access for Docker"

  • Assign any of your desired containers to this wireguard network

  • Create a "shared network" that can bridge your normal docker network with your wg network, but does NOT route to WAN (--internal flag):

    • docker network create --internal [shared-network-name]

  • Join your VPN'd containers, as well as any you want to be able to access them, to this "shared network" (a container can join multiple docker networks!):

    • docker network connect [shared-network-name] [container-name]

that is all you need to do! Now any of your regular docker network containers can communicate with your containers that are on the wireguard network

you can create a user script to always join a list of containers to your shared network:

#!/bin/bash

# Define the target network

TARGET_NETWORK="shared-network"

# Define the list of container names to monitor

CONTAINERS=(

"container-name"

"container2-name"

)

# Function to check and connect container

connect_if_needed() {

local CONTAINER=$1

# Check if container exists

if ! docker inspect "$CONTAINER" &>/dev/null; then

echo "$(date): Container '$CONTAINER' does not exist, skipping..."

return

fi

# Get the networks the container is connected to

NETWORKS=$(docker inspect "$CONTAINER" -f '{{range $key, $value := .NetworkSettings.Networks}}{{$key}} {{end}}')

# Check if container is already connected to the target network

if echo "$NETWORKS" | grep -q "$TARGET_NETWORK"; then

echo "$(date): Container '$CONTAINER' already connected to '$TARGET_NETWORK'"

else

echo "$(date): Connecting '$CONTAINER' to '$TARGET_NETWORK'..."

docker network connect "$TARGET_NETWORK" "$CONTAINER"

if [ $? -eq 0 ]; then

echo "$(date): Successfully connected '$CONTAINER' to '$TARGET_NETWORK'"

else

echo "$(date): Failed to connect '$CONTAINER' to '$TARGET_NETWORK'"

fi

fi

}

# Check if target network exists

if ! docker network inspect "$TARGET_NETWORK" &>/dev/null; then

echo "Error: Network '$TARGET_NETWORK' does not exist"

exit 1

fi

echo "$(date): Starting Docker event monitor for container updates..."

# Monitor docker events for container creates AND starts

docker events --filter 'type=container' --filter 'event=create' --filter 'event=start' --format '{{.Actor.Attributes.name}}' | while read CONTAINER_NAME

do

# Check if this container is in our list

for TARGET_CONTAINER in "${CONTAINERS[@]}"; do

if [ "$CONTAINER_NAME" = "$TARGET_CONTAINER" ]; then

echo "$(date): Detected event for '$CONTAINER_NAME'"

# Small delay to ensure container is fully ready

sleep 2

connect_if_needed "$CONTAINER_NAME"

break

fi

done

done

Edited by underw0rld
adding script

  • Community Expert
17 hours ago, underw0rld said:

For anyone who also wants to achieve this, I found a solution that is much easier (imo).

  • Create a wireguard connection to your vpn, using "VPN Tunneled Access for Docker"

  • Assign any of your desired containers to this wireguard network

  • Create a "shared network" that can bridge your normal docker network with your wg network, but does NOT route to WAN (--internal flag):

    • docker network create --internal [shared-network-name]

  • Join your VPN'd containers, as well as any you want to be able to access them, to this "shared network" (a container can join multiple docker networks!):

    • docker network connect [shared-network-name] [container-name]

that is all you need to do! Now any of your regular docker network containers can communicate with your containers that are on the wireguard network


https://docs.docker.com/reference/cli/docker/network/create/

Requires the command ran any time the docker that's connected to goes down... the ip table's command I gave above is constant once setup... doesn't require interactions or running xyz command for other dockers...

https://bmartino1.weebly.com/guide-dockernetworks.html

OR you can use Docker compose to set 2 networks at once...

  • Author

The network assignments created in my previous post persist after bringing individual containers down/up, shutting down all containers associated with the "shared network", and even after disabling/enabling docker entirely. Just for anyone's future reference.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

Account

Navigation

Search

Search

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.