Routing Docker traffic through a VPN container

I'm using a VPN for years now but I recently decided to route the traffic of some of my container through a VPN connection to by-pass some country-specific restrictions and to enhance my privacy.

Routing Docker traffic through a VPN container

I'm using a VPN for years now but I recently decided to route the traffic of some of my container through a VPN connection to by-pass some country-specific restrictions and to enhance my privacy.

I found out a docker container which suits perfectly to what I wanted to do and I will show you how to quickly built this setup.

Prerequisites

Folder structure

As a first step, you have to create a folder structure as the following one.

docker-vpn-project/
      |_ docker-compose.yaml 
      |_ vpn
          |_ config
      |_ my-container # If needed
          |_ config

OpenVPN files

To be able to work, this container will need some OpenVPN configurations from my current VPN provider, Windscribe.

On their website, you can easily find an OpenVPN configuration generator which provides you the key elements to continue:

  • An *.ovpn configuration file
  • A certificate and a private key
  • An username/password for the authentication
Windscribe OpenVPN Generator

You have now downloaded and extracted all the files and it looks like this:

Now it's time to copy these files into your VPN configuration folder.

Authentication file

You will need to create a file "vpn.auth" for the credentials. This file must contains only 2 lines of text, the first one is your username and the second line is the password, both provided by your VPN provider.

vpn.auth file

Now you need to open the *.ovpn file to target the authentication file we have create. You just need to add the path of the file inside the container /vpn/vpn.auth following the existing auth-user-pass section (line 7).

*.ovpn configuration with the path to the vpn.auth file

How does it look on my Synology NAS:

docker-vpn-project/
      |_ docker-compose.yaml 
      |_ vpn
          |_ config
                |_ ca.crt  
                |_ ta.key
                |_ vpn.auth
                |_ Windscribe-Zurich-Alphorn-GCM.ovpn
      |_ my-container # If needed
          |_ config

Docker-compose.yaml

Now let's deep-dive into the docker-compose.yaml file. For this example, I  route through VPN a librespeed container but you can easily use this also for Sonarr, Radarr, ruTorrent, qbitTorrent, Jackett, Prowlarr, etc.

version: "2.3" 
services: 
  vpn:
    container_name: my-vpn
    image: dperson/openvpn-client:latest
    restart: unless-stopped
    networks:
      default:
        ipv4_address: 172.51.0.2
    dns:
      - 9.9.9.9
    ports:
      - 1234:80
    cap_add:
      - NET_ADMIN
    sysctls:
      - net.ipv6.conf.all.disable_ipv6=0
    security_opt:
      - label:disable
    environment:
      - PUID=1026
      - PGID=100
      - TZ=Europe/Zurich
    devices:
      - /dev/net/tun:/dev/net/tun
    volumes:
      - ./vpn/config:/vpn
    command: '-f "" -r 192.168.0.0/24'
    healthcheck:
      test: ["CMD-SHELL", "if [ $$(curl --silent https://ifconfig.co/country-iso) = 'DE' ]; then exit 0; else exit 1; fi"]
      start_period: 5s
      interval: 5s
      timeout: 10s
      retries: 3

  my-container:
    image: linuxserver/librespeed:version-5.2.4
    container_name: my-container
    restart: unless-stopped
    network_mode: "service:vpn"
    environment:
      - PUID=1026
      - PGID=100
      - TZ=Europe/Zurich
 
networks:
  default:
    driver: bridge
    ipam:
      config:
        - subnet: 172.51.0.0/16
          gateway: 172.51.0.255

The key section are the following:

  • vpn.networks - Used to define a fixed IP address to your VPN container into the current network range
  • vpn.dns - The DNS address your VPN container is going to use to resolve domains. I'm using Quad9
  • vpn.dns - The ports you need to map. As all the traffic is redirected through the VPN, you need to map all the ports into the VPN container and not your services container
  • my-container.network_mode - No custom port declaration, we are delegating the complete network management to the VPN service, running inside the same docker-compose.yaml file, named vpn.
    Note: If you want to run the VPN container in a separate docker-compose.yaml or docker run command, you will need to set container:vpn instead of service:vpn
  • networks - We are defining the default network of this stack
  • healthcheck - This healthcheck command is going to define my VPN container as unhealthy if its connection is not located in Germany

And now you are ready to test!

Container creation and connectivity

You can now create and start your Docker stack with the command:
sudo docker-compose up -d

You can check your VPN container logs to check if everything is fine. It should look like this:

VPN container started without issues

As soon as both of the containers are created and started, it's time to check your connectivity and especially your IP address.

Let's start with the VPN container, you can run the following command:
sudo docker exec -it my-vpn curl ifconfig.co/json

You can see below that your IP address is now located in Zurich, the VPN is correctly connected!

VPN Container connectivity test

Now, we are going to check the librespeed container, you can run the following command:
sudo docker exec -it my-container curl ifconfig.co/json

The result is exactly the same! Congratulations, you are now routing all your Librespeed container traffic through a VPN container! 🎉

Debug

If you encounter any issue related to the /dev/net/tun device interface, as I did on my Synology NAS, you might need to create it before.
I made a small bash file you can easily run to fix this issue!

  • Create the file
    sudo nano tun.sh
  • Paste this content inside
#!/bin/bash

# Create the necessary file structure for /dev/net/tun
if ( [ ! -c /dev/net/tun ] ); then
    if ( [ ! -d /dev/net ] ); then
        mkdir -m 755 /dev/net
    fi
     mknod /dev/net/tun c 10 200
fi

# Load the tun module if not already loaded
if ( !(lsmod | grep -q "^tun\s") ); then
    insmod /lib/modules/tun.ko
fi

 # Load iptables mangle is not already loaded
if ( !(lsmod |grep -q "^iptable_mangle\s") ); then
    insmod /lib/modules/iptable_mangle.ko
fi
  • Make it executable
    sudo chmod +x tun.sh
  • Execute it
    sudo ./tun.sh
Looking for help?
If you are looking for some help or want a quick chat, please head over to the Discord Community!