VPN-connected Docker container unable to access other containers

Viewed 2301

I have an application container (app) connected to the internet via a VPN-container (vpn). While that connectivity works, the app container is now unable to connect to the database container (db).

Is there a way to have the app container connect to local containers (or any RFC1918 address) while simultaneously also connected through the vpn? I would simply add db to the same network_mode but the db very specifically needs to be accessible by another container not connected through the vpn (A second application that will read from db).

The VPN container is running bubuntux/nordvpn and my docker-compose.yml is as follows:

version: "3.9"

services:
  app:
    build: app/
    depends_on:
      - db
    network_mode: service:vpn
    depends_on:
      - vpn
  db:
    image: postgres:alpine
    restart: always
    network_mode: service:vpn
    environment:
      POSTGRES_USER: PASSWORD
      POSTGRES_PASSWORD: PASSWORD
      POSTGRES_DB: PASSWORD
  vpn:
    image: bubuntux/nordvpn
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    sysctls:
      - net.ipv4.conf.all.rp_filter=2
    devices:
      - /dev/net/tun
    environment:
      - USER=EMAIL@EMAIL.COM
      - "PASS=PASSWORD"
      - CONNECT=United_States
      - TECHNOLOGY=NordLynx
    ulimits:
      memlock:
        soft: -1
        hard: -1

I have attempted to use link, as follows:

app:
   links:
      - db

However, I get a startup error, specifically that container type network can't be used with links.

To restate the question: How do I have a container connected to the internet through a VPN container (In this case bubuntux/nordvpn) that is also accessible by non-VPN connected containers?

Thank you!

3 Answers

When you use network_mode: service:vpn in app's config, the app and vpn services appear to run in the same container network-wise (specifically, they are in the same network namespace). They share interfaces, routing rules, ports, etc. Notably, they also share /etc/resolv.conf and /etc/hosts. Since links just adds a relevant entry to /etc/hosts, you can link from app to db by (counter-intuitively) adding a db link to the vpn service.

Try the following config:

version: "3.9"

services:
  app:
    build: app/
    depends_on:
      - db
    network_mode: service:vpn
  db:
    image: postgres:alpine
    network_mode: bridge
    restart: always
    environment:
      POSTGRES_USER: USER
      POSTGRES_PASSWORD: PASSWORD
      POSTGRES_DB: DB
  vpn:
    image: bubuntux/nordvpn
    network_mode: bridge
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    sysctls:
      - net.ipv4.conf.all.rp_filter=2
    devices:
      - /dev/net/tun
    environment:
      - USER=EMAIL@EMAIL.com
      - "PASS=PASSWORD"
      - CONNECT=foo
      - TECHNOLOGY=NordLynx
    links:
      - db # <----- This makes `db` accessible to `app`
    ulimits:
      memlock:
        soft: -1
        hard: -1

As per the documentation, this should work:

version: "3.9"

services:
  app:
    build: app/
    links:
      - vpn:db     
    depends_on:
      - db
  db:
    image: postgres:alpine
    depends_on:
      - vpn
    restart: always
    network_mode: service:vpn
    environment:
      POSTGRES_USER: PASSWORD
      POSTGRES_PASSWORD: PASSWORD
      POSTGRES_DB: PASSWORD
  vpn:
    image: bubuntux/nordvpn
    network_mode: bridge 
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    sysctls:
      - net.ipv4.conf.all.rp_filter=2
    devices:
      - /dev/net/tun
    environment:
      - USER=EMAIL@EMAIL.COM
      - "PASS=PASSWORD"
      - CONNECT=United_States
      - TECHNOLOGY=NordLynx
    ulimits:
      memlock:
        soft: -1
        hard: -1
    ports:
      - 5432:5432

You should access the postgres from you machine on localhost:5432

UPDATE:

I made this simulation because I don't have access to a paid nordvpn acciut, so vpn service is just an ubuntu sleeping 36000 seconds and the db service is using that network, however this way I don't know what the real vpn service is doing on the host with those added capabilities (NET_ADMIN, SYS_MODULE), but try to give this setup a try.

With this setup app service (that is adminer, a db admin gui) is listening on my machine localhost:8080 and I can login on it and make it connect to the postgres db with the host name db (server name setting on gui) that is listening on its default 5432 postgres port. Also I can reach postgres from my machine localhost:54320 address because it is forwarded to the app service 5432 port that is using the same network as the db service that is using the vpn service network, that would presumably mean using the nordvpn network.

version: "3"

services:
  app:
    image: adminer
    restart: always
    ports:
      - 8080:8080
      - 54320:5432
    links:
      - vpn:db     
    depends_on:
      - db
  db:
    image: postgres:alpine
    depends_on:
      - vpn
    restart: always
    network_mode: service:vpn
    environment:
       - POSTGRES_USER=root
       - POSTGRES_PASSWORD=root
       - POSTGRES_DB=mydb
  vpn:
    image: ubuntu
    command: ["/bin/sh","-c","echo started && sleep 36000"]

The issue here is how network_mode: service:$name works:

It creates an implicit dependency from your app service to your vpn service so docker-compose will start the vpn container first with its own network namespace and virtual network interfaces connected to a virtual network (in this case the <project>_default network created by docker_compose).

Then it will start the app container with the same network namespace as the vpn service, i.e. localhost in the vpn container is the same as localhost in the app container - so far this is actually what you want.

Now, since you set network_mode: service:vpn on the db service, too, it will also be added to the same network namespace - which is not what you want because the container will now not be discoverable as db via DNS as it is attached to the network namespace of vpn instead of its own.

To achieve what you want you with one of the following:

  • First option: remove network_mode: service:vpn from db so the service will get its own network namespace / internal IP address which can be resolved via DNS - i.e. you can then use "db" as the hostname for your database connection
  • Second option: use "localhost" as the hostname for your database connection since app and db share the same network namespace, i.e. localhost in both refers to the same

For the first option the docker-compose.yml would look like this:

version: "3.9"

services:
  app:
    build: app/
    depends_on:
      - db
    network_mode: service:vpn
  db:
    image: postgres:alpine
    restart: always
    environment:
      POSTGRES_USER: PASSWORD
      POSTGRES_PASSWORD: PASSWORD
      POSTGRES_DB: PASSWORD
  vpn:
    image: bubuntux/nordvpn
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    sysctls:
      - net.ipv4.conf.all.rp_filter=2
    devices:
      - /dev/net/tun
    environment:
      - USER=EMAIL@EMAIL.COM
      - "PASS=PASSWORD"
      - CONNECT=United_States
      - TECHNOLOGY=NordLynx
    ulimits:
      memlock:
        soft: -1
        hard: -1

Also note:

  • links is deprecated and shouldn't be used for new applications
  • depends_on will ensure that the depended-on services are started before the depending service - but it is not necessary for DNS resolution to work, so you could omit it
  • you can use aliases if you want to change/assign specific DNS names to a service

For more see Networking in Compose.

Related