Skip to content

Instantly share code, notes, and snippets.

@bradkovach
Last active January 16, 2026 19:43
Show Gist options
  • Select an option

  • Save bradkovach/e49396ed4313e16b529ea6f47e6e19ec to your computer and use it in GitHub Desktop.

Select an option

Save bradkovach/e49396ed4313e16b529ea6f47e6e19ec to your computer and use it in GitHub Desktop.
Bind Docker Compose containers to VPN service

GitHub Gists do not allow files to be at subdirectories, so if you clone this repo, first...

  • Move content--compose.yml to stacks/content/compose.yml
  • Move delivery--compose.yml to stacks/delivery/compose.yml

You can run chmod +x init.sh && ./init.sh to do this automatically.

mkdir -p stacks/content
mkdir -p stacks/delivery

mv content--compose.yml stacks/content/compose.yml
mv delivery--compose.yml stacks/delivery/compose.yml

You should have the following project structure:

.
├── README.md
├── init.sh
└── stacks/
    ├── content/
    │   └── compose.yml
    └── delivery/
        └── compose.yml

Then start the containers...

cd path/to/stacks/content
docker compose up -d

cd path/to/stacks/delivery
docker compose up -d

Configuring your router

See 10-router-config.md

Configuring Node Proxy Manager

See 20-npm-config.md

Troubleshooting

See `30-troubleshooting.md

Router Setup

Additional router/firewall setup will allow Nginx Proxy Manager to set up and maintain automatic HTTPS/TLS certificates.

The compose port bindings in stacks/delivery/compose.yml accessible outside your local network...

You can add Plex's default port, :32400 in this step if you want.

IPv4

Forward ports :80 and :443 to your Docker host.

IPv6

If you have a global IPv6 address issued via prefix delegation, use a firewall rule to allow the world to connect to ports :80 and :443 on your docker host's IPv6 address.

From your docker host, run curl -6 ifconfig.io. If this returns a value immediately, and you see that value in the output of ip -6 addr show scope global, use that address to create a firewall rule allowing all IPv6 traffic to that IP and ports :80 and :443. Use that value to set your public DNS AAAA record as well.

Recommended setup (manual setup once docker containers are running)

I recommend using an Access List in Nginx Proxy Manager to ensure that you don't expose any private services to the outside world. I use an IP access list to ensure that NPM only allows my local network subnets.

You can get the docker network subnet(s) for 'content' with docker network inspect content | jq -r '.[] | .IPAM.Config | map(.Subnet) | .[]'. Add those subnets to the Rules tab so your containers can communicate. If you get an error because you don't have jq installed, you can just run docker network inspect content and examine the JSON output for .IPAM.Config[*].Subnet.

If you add other networks down the line, just change 'content' to the new network name to print the subnet(s).

Details Tab:
  Name: Local
  Satisfy Any: no
  Pass Auth to Upstream: no
Authorizations Tab:
  # leave empty
Rules Tab:
  # run `ip -4 addr` from docker host

  # v4 address looks like...
  #     => use this subnet value...

  # 192.168.1.x/24
  #     => 192.168.1.0/24
  # 192.168.x.x/16
  #     => 192.168.0.0/16
  # 192.x.x.x/8
  #     => 192.0.0.0/8

  - Allow: 10.1.0.0/16

  # run `ip -6 addr` from docker host

  # v6 address looks like...
  #     => use this subnet value...

  # aaaa:bbbb:cccc:dddd:1111:2222:3333:4444/64
  #     => aaaa:bbbb:cccc:dddd::0/64
  # aaaa:bbbb:cccc::1111:2222:3333:4444:5555/48
  #     => aaaa:bbbb:cccc::0/48

  - Allow: 2001:ffff::0/48

  # Allow other docker networks if necessary
  - Allow: 172.0.0.0/8 # docker IPv4
  - Allow: fdff::0/48  # docker IPv6 subnet(s)

Then, apply this Access List as you configure services in Nginx Proxy Manager GUI...

To set up qBittorrent's Web UI so you can access it, navigate to Hosts > Proxy Hosts and click "Add Proxy Host".

Use this template:

Details Tab:
  Domain Names: qbittorrent.content.example.com
  Scheme: http
  Forward Hostname / IP: content-qbittorrent-1

  # the same port number you chose for services.qbittorrent in stacks/content/compose.yml
  Forward Port: 8080

  # Pick the list you created in the previous step
  Access List: Local

  Cache Assets: Yes
  Block Common Exploits: Yes
  Websockets Support: Yes

Custom Locations Tab:
  # leave empty

SSL Tab:
  SSL Certificate: Request a new Certificate
  Force SSL: Yes
  HTTP/2 Support: Yes

  # Only enable this once you've verified everything works
  HSTS Enabled: No
  HSTS Sub-domains: No

  # Enable if you know your services are unable to be reached
  # from the outside world.
  Use DNS Challenge: No

If this works without error, you should be able to visit qbittorrent.content.example.com from your home network. If you want, you can serve plex through nginx proxy manager, too. Just add your chosen hostname(s) to Settings > Network > Custom server access URLs and include the port. For example: https://plex.content.example.com:443.

Troubleshooting

HTTP Error 502

Nginx Proxy Manager is unable to connect to your containers. Make sure your Access Lists are set up correctly

Nginx Proxy Manager error when adding a new Proxy Host

Make sure your server is externally reachable from port :80 and :443 for IPv4 and IPv6 (if necessary) and that A (and AAAA) records resolve on public DNS.

qBittorrent Blocking/Restricting Traffic

cd stacks/content and run docker compose down. Then, edit stacks/content/config/qbittorrent/qBittorrent/qBittorrent.conf adding/editing these lines to the [Preferences] section

WebUI\ServerDomains="qbittorrent.content.example.com"
WebUI\TrustedReverseProxiesList=<docker host IP>

Then run docker compose up -d to rebuild the content stack.

If you feel like you've made a mess of the qBittorrent config, stop the stack, delete stacks/content/config/qbittorrent and bring the stack up again.

# stacks/content/compose.yml
# optional: provides a namespace for your containers so they're named
# like `${stack.name}-${service.name}-${instanceIndex}`
# for example content-qbittorrent-1
name: content
# creates a docker network that all services belong to
# IPv6 is optional, but recommended, as its networking capabilities
# resolve a lot of issues for self-hosted folks (CGNAT, port forwarding)
networks:
default:
enable_ipv6: true
name: content
services:
# WireGuard VPN Config
gluetun:
image: qmcgaw/gluetun:latest
devices:
- /dev/net/tun:/dev/net/tun
environment:
- VPN_SERVICE_PROVIDER=custom
- VPN_TYPE=wireguard
- WIREGUARD_ENDPOINT_IP=1.2.3.4
- WIREGUARD_ENDPOINT_PORT=51820
- WIREGUARD_PRIVATE_KEY=${AIRVPN_WIREGUARD_PRIVATE_KEY}
- WIREGUARD_PRESHARED_KEY=${AIRVPN_WIREGUARD_PRESHARED_KEY}
- WIREGUARD_ADDRESSES=${AIRVPN_WIREGUARD_ADDRESSES}
# Optional
# - if your VPN forwards a port for you, set that here
- FIREWALL_VPN_INPUT_PORTS=${AIRVPN_FORWARDED_PORT}
# - if you use IPv6, enable dns so bittorrent can resolve trackers and hosts using IPv6
- DNS_UPSTREAM_IPV6=on
cap_add:
- NET_ADMIN
# OpenVPN VPN Config
# needs:
# - stacks/content/config/gluetun/client.crt
# - stacks/content/config/gluetun/client.key
# gluetun:
# image: qmcgaw/gluetun:latest
# devices:
# - /dev/net/tun:/dev/net/tun
# environment:
# - VPN_SERVICE_PROVIDER=airvpn
# - SERVER_COUNTRIES=${AIRVPN_SERVER_COUNTRIES}
# - SERVER_CITIES=${AIRVPN_SERVER_CITIES}
# - FIREWALL_VPN_INPUT_PORTS=${AIRVPN_FORWARDED_PORT}
# - DNS_UPSTREAM_IPV6=on
# volumes:
# - ./config/gluetun:/gluetun
# cap_add:
# - NET_ADMIN
# volumes will be created at
# stacks/content/data/
# stacks/content/config/qbittorrent
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
environment:
<<: *env
TZ: 'America/Denver'
PGID: '${GID}'
PUID: '${UID}'
WEBUI_PORT: '${QBITTORRENT_PORT}'
network_mode: service:gluetun
volumes:
- ./data:/data
- ./config/qbittorrent:/config
name: delivery
networks:
default:
enable_ipv6: true
name: delivery
# Allows services in this `delivery` stack to connect to `content` network.
content:
external: true
name: content
services:
# volumes will be created at
# - stacks/delivery/config/npm
npm:
restart: unless-stopped
image: jc21/nginx-proxy-manager:latest
environment:
DB_SQLITE_FILE: /data/database.sqlite
networks:
- default
- content
ports:
- '80:80'
- '443:443'
- '81:81'
volumes:
- ./config/npm/data:/data
- ./config/npm/letsencrypt:/etc/letsencrypt
- ./config/npm/log:/var/log
# If you use a pi-hole with custom entries for your docker services,
# you may need to use public DNS so your DNS lookups match
# Let's Encrypt's DNS results as it resolves and validates your
# domains are the same. Otherwise, certbot will not be able to
# complete the authorization procedure.
dns:
- 8.8.8.8
- 9.9.9.9
#!/bin/bash
mkdir -p stacks/content
mkdir -p stacks/delivery
mv content--compose.yml stacks/content/compose.yml
mv delivery--compose.yml stacks/delivery/compose.yml
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment