Contents

Two independent VPN tunnels: WireGuard + ProtonVPN via Shadowsocks

We’ll add a ProtonVPN container next to an existing WireGuard container on the same VPS, reachable from the PC via a Shadowsocks proxy, I always try to make easily-reversible changes. .

The goal

  • Keep the existing WireGuard container untouched.
  • Add a second, independent tunnel: PC → Shadowsocks → ProtonVPN container → internet.
  • Each container manages its own networking. No shared routes, no host-level iptables changes.
  • Pick either tunnel per-client, per-app, or run both at once.

Server side

The existing wireguard service stays as it is (network_mode: host, its own subnet and peers). We’ll add a gluetun container running the ProtonVPN client with Shadowsocks enabled.

Add a protonvpn service to docker-compose.yml, alongside the existing wireguard service:

  protonvpn:
    image: qmcgaw/gluetun:latest
    container_name: protonvpn
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
    environment:
      - VPN_SERVICE_PROVIDER=protonvpn
      - OPENVPN_USER=${PROTONVPN_USERNAME}
      - OPENVPN_PASSWORD=${PROTONVPN_PASSWORD}
      - SERVER_COUNTRIES=Netherlands
      - SHADOWSOCKS=on
      - SHADOWSOCKS_PASSWORD=${SHADOWSOCKS_PASSWORD}
      - SHADOWSOCKS_METHOD=chacha20-ietf-poly1305
    ports:
      - "8388:8388/tcp"
      - "8388:8388/udp"
    volumes:
      - ./protonvpn:/gluetun
    restart: unless-stopped

gluetun handles the OpenVPN connection to ProtonVPN internally and runs the Shadowsocks server inside its own container network namespace, separate from the host’s routing table.

Create a .env file next to docker-compose.yml with PROTONVPN_USERNAME, PROTONVPN_PASSWORD, and SHADOWSOCKS_PASSWORD. Use your ProtonVPN OpenVPN credentials here, not your account login (get them from https://account.protonvpn.com/account#openvpn ).

SHADOWSOCKS_PASSWORD=<some-password>
PROTONVPN_USERNAME=<openvpn-username-not-protonvpn-account-name>
PROTONVPN_PASSWORD=<openvpn-password-not-your-account-password>

Bring the stack up:

docker-compose up -d

docker logs protonvpn

Confirm the exit IP is ProtonVPN’s, not the VPS’s own:

docker exec protonvpn wget -qO- ifconfig.me

Client side

Clash (or one of its GUI wrappers, like Clash Verge) reads a single YAML config and exposes both an HTTP and a SOCKS5 proxy.

Save the following as config.yaml:

port: 7890
socks-port: 7891
allow-lan: false
mode: Rule

proxies:
  - name: "ProtonVPN-SS"
    type: ss
    server: your-vps-host.example.com
    port: 8388
    cipher: chacha20-ietf-poly1305
    password: "your_shadowsocks_password"
    udp: true

proxy-groups:
  - name: "PROXY"
    type: select
    proxies:
      - ProtonVPN-SS
      - DIRECT

rules:
  - MATCH,PROXY

Run clash from the directory containing config.yaml:

clash
# INFO Start initial compatible provider PROXY
# INFO HTTP proxy listening at: 127.0.0.1:7890
# INFO SOCKS proxy listening at: 127.0.0.1:7891

Clash reads the YAML directly and runs as a regular foreground process.

Wiring into GNOME

With Clash running:

  1. Open SettingsNetworkNetwork Proxy.
  2. Select Manual.
  3. Under SOCKS Host, enter 127.0.0.1 and port 7891.
  4. Click Apply.

Or the equivalent from the command line:

gsettings set org.gnome.system.proxy mode 'manual'
gsettings set org.gnome.system.proxy.socks host '127.0.0.1'
gsettings set org.gnome.system.proxy.socks port 7891

Most GTK apps, and anything that respects the system proxy (including a lot of CLI tools via all_proxy), will route through ProtonVPN. Verify with:

curl --socks5 127.0.0.1:7891 ifconfig.me

Firefox exception

Firefox does not honor org.gnome.system.proxy, even when set to Use system proxy settings. This isn’t specific to this setup, as far as I know, it’s been reported against Firefox on Linux for well over a decade. It’s never really been fixed, just worked around I keep the manual SOCKS5 setting in Firefox’s own network settings. It’s two clicks to flip between “Manual” and “No proxy”, which is fine for how often I actually switch it. I tried FoxyProxy. Worth installing only if you want per-site rules or a toolbar toggle. I also tried the MOZ_GTK_USE_PORTAL=1 environment variable some people suggest, and it was inconsistent in my testing, so I couldn’t rely on it. .

A one-line PAC file gets you the easy Manual/No proxy toggle without opening the manual-config dialog each time, if you’d rather point Firefox at a file you control (for instance, ~/.config/proxy.pac):

function FindProxyForURL(url, host) { return "SOCKS5 127.0.0.1:7891"; }

Then: SettingsNetwork SettingsAutomatic proxy configuration URLfile://${HOME}/.config/proxy.pac.