1

I have four services in a docker-compose file. vpn_img1 and vpn_img2 are behind a vpn (configured as described below), host_img1 and host_img2 are not.

I am able to access all four 'img' services on the host machine via http://localhost:<port_num> but can only access the 'host_img' images on other machines on the local network; the 'vpn_img' images time out. I get the same result with other, similar vpn images. Does anyone know why? Are there any setting on the vpn service that would allow access to the 'vpn_img' images? Is there something else I need to do to enable that kind of access?

Also, host_img1 needs to talk to the vpn_imgs, but host_img2 does not.

version: "3.4"
services:    
  vpn:
    container_name: vpn
    image: ghcr.io/bubuntux/nordlynx
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
    environment:
      - PRIVATE_KEY=xxx
      - QUERY=filters\[country_id\]=228
      - NET_LOCAL=192.168.1.0/24
      # - ALLOWED_IPS=192.168.0.0/24
    ports:
      - 1234:1234 # vpn_img1
      - 2345:2345 # vpn_img2
    sysctls:
      - net.ipv6.conf.all.disable_ipv6=1

  vpn_img1:
    container_name: vpn_img1
    image: vpn_img1
    restart: unless-stopped
    network_mode: service:vpn # run on the vpn network
    depends_on:
      - vpn

  vpn_img2:
    container_name: vpn_img2
    image: vpn_img2
    restart: unless-stopped
    network_mode: service:vpn # run on the vpn network
    depends_on:
      - vpn

  host_img1:
    container_name: host_img1
    image: host_img1
    restart: unless-stopped
    network_mode: host

  host_img2:
    container_name: host_img2
    image: host_img2
    restart: unless-stopped
    network_mode: host
taynaron
  • 704
  • 1
  • 10
  • 23

1 Answers1

2

The crux of the issue should be that the VPN service only allows traffic that originates from the host machine (where the VPN client is running): other machines on the local network cannot reach the services behind the VPN.
Their network accessibility is limited to the same scope as the VPN service.

The VPN (Virtual Private Network) service itself, by its nature, tends to limit the network traffic to only go through the VPN tunnel for security reasons, making services behind it unreachable from the outside, unless specifically configured otherwise.

When a container uses another's network stack, it effectively becomes "hidden" behind that service. All incoming and outgoing network traffic for that container goes through the main service's network, which, in this case, is the VPN service. That is why you can access the vpn_img1 and vpn_img2 services from the host machine (where the VPN client is running), but not from other machines in the local network.


What you can try is to use a reverse proxy that can be accessed from the local network and forwards requests to the VPN-protected services.

Set up a new service in your Docker Compose file for the reverse proxy. You can use something like Nginx or Traefik. That service should not be behind the VPN.
Configure the reverse proxy to forward requests to vpn_img1 and vpn_img2. For Nginx, you might use the proxy_pass directive. And ensure the proxy's ports are forwarded correctly, so you can access it from the local network.

Note that vpn_img1 and vpn_img2 are still behind the VPN, and any connections they make will go through it.
However, other services can reach them through the reverse proxy, which is not behind the VPN.

For instance, using NGiNX:

version: "3.4"
services:    
  vpn:
    container_name: vpn
    image: ghcr.io/bubuntux/nordlynx
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
    environment:
      - PRIVATE_KEY=xxx
      - QUERY=filters\[country_id\]=228
      - NET_LOCAL=192.168.1.0/24
    sysctls:
      - net.ipv6.conf.all.disable_ipv6=1
    network_mode: bridge

  vpn_img1:
    container_name: vpn_img1
    image: vpn_img1
    restart: unless-stopped
    network_mode: service:vpn
    depends_on:
      - vpn

  vpn_img2:
    container_name: vpn_img2
    image: vpn_img2
    restart: unless-stopped
    network_mode: service:vpn
    depends_on:
      - vpn

  host_img1: 
    container_name: host_img1
    image: host_img1
    restart: unless-stopped
    network_mode: service:vpn # needs access to vpn_img2, so it needs to route through the VPN too

  host_img2:
    container_name: host_img2
    image: host_img2
    restart: unless-stopped
    network_mode: host

  nginx:
    container_name: nginx
    image: nginx
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    ports:
      - 8000:80
    depends_on:
      - vpn
    network_mode: bridge

And, the nginx.conf file located in the same directory as your docker-compose.yml file, should be:

events {}

http {
    server {
        listen 80;

        location /vpn_img1/ {
            proxy_pass http://vpn_img1:1234/;
        }

        location /vpn_img2/ {
            proxy_pass http://vpn_img2:2345/;
        }

        location /host_img1/ {
            proxy_pass http://host_img1:3456/;
        }
    }
}

The nginx service is in bridge network mode so it can access the local network, and is also capable of reaching the vpn service since it is also in bridge mode. The proxy_pass directives are set to http://vpn:<port>/ because Nginx needs to send requests to the vpn service, which will forward them to the vpn_img services. <host_ip> is the IP address of the machine where Docker is running.

That means you should be able to reach vpn_img1 service at http://<host_ip>:8000/vpn_img1/, vpn_img2 service at http://<host_ip>:8000/vpn_img2/, and host_img1 service at http://<host_ip>:8000/host_img1/ from any machine in your local network.


Unfortunately, nginx fails to start, with the logs throwing host not found in upstream "vpn" in /etc/nginx/nginx.conf:8, which is where the first proxy_pass command is located (I get the same error for line 12, the next proxy_pass command, if I comment out the first location block).
I've ensured that both vpn and nginx have network_mode: bridge, and have tried adding vpn_img1 and vpn_img2 to nginx's depends_on section.

The error host not found in upstream "vpn" generally means that the Nginx container cannot resolve the hostname vpn to an IP address. In this case, it means that Nginx cannot find your vpn service.

That typically happens because the Docker internal DNS server is unable to resolve the service name to its IP address.

The network_mode: service:<name> is a bit special because it places the dependent service in the network stack of the service specified, not in a common network. That is why the nginx service cannot reach the vpn service directly, even if they are both in bridge network mode. The nginx service is in the default bridge network while the vpn service is isolated in its own network stack due to network_mode: service:vpn.

A partial solution may be to create a custom network and make sure all services are on that network, as suggested in this thread. That may still fail to fully resolve.

"no resolver defined to resolve vpn_img1" indicates that Nginx is unable to resolve the hostname vpn_img1 to an IP address. That is because Nginx does not use the system resolver, it uses its own DNS resolution implementation.

To address this, you can explicitly specify a DNS resolver in your Nginx configuration. That can be your local network's DNS server, or a public one like Google's DNS server at 8.8.8.8.

You should add the resolver directive in the http block, just before the server block, in the nginx.conf:

events {}

http {
    resolver 127.0.0.11;  # docker's internal DNS resolver

    server {
        listen 80;

        location /vpn_img1/ {
            proxy_pass http://vpn_img1:1234/;
        }

        location /vpn_img2/ {
            proxy_pass http://vpn_img2:2345/;
        }

        location /host_img1/ {
            proxy_pass http://host_img1:3456/;
        }
    }
}

In the above configuration, resolver 127.0.0.11 is used because Docker has a built-in DNS server that it runs at that IP address, which is used to provide DNS resolution for containers. That should help Nginx to resolve the container names to their respective IP addresses. Illustration/troubleshooting: "Docker Network Nginx Resolver".

See also "How to connect docker containers" from Adriano Galello for more on the Docker DNS service. And "docker-compose internal DNS server 127.0.0.11 connection refused" in case of issue.


Even files at / for a given location are getting a 404; so /index.html or /gettext.js return a 404. The service is instead requesting at a raw / location, so / when they should be asking at vpn_img1/. Guess I need to bone up on my NGiNX. Any thoughts/tips?

The problem here could be related to how the applications you are proxying interpret the incoming requests. Many services that have been dockerized have a method for changing their internal pathing within the container's settings or in the docker configuration itself. If they don't, you can try to fix this in nginx itself.

For example, a request to <ip_address>:8000/vpn_img1/index.html would get forwarded to http://vpn_img1:1234/index.html.
Now, if your vpn_img1 service is set up such that it expects to be at the root (/), it may attempt to load resources from absolute paths (like /styles/main.css or /script/main.js). That might cause nginx to return a 404 because it is not configured to handle requests to these paths.

To work around this, you can strip off the path prefix (/vpn_img1 or /vpn_img2) from the request URI before the request is proxied to the respective services.
See also "Guide on how to use regex in Nginx location block section".

events {}

http {
    server {
        listen 80;

        location ~ ^/vpn_img1/(.*)$ {
            proxy_pass http://vpn_img1:1234/$1;
        }

        location ~ ^/vpn_img2/(.*)$ {
            proxy_pass http://vpn_img2:2345/$1;
        }

        location ~ ^/host_img1/(.*)$ {
            proxy_pass http://host_img1:3456/$1;
        }
    }
}

Here, the regular expression matches the location, and any path after /vpn_img1/ or /vpn_img2/ is captured into the $1 variable. That value is then used in the proxy_pass directive. Now, a request to <ip_address>:8000/vpn_img1/index.html would be forwarded to http://vpn_img1:1234/index.html, and any resources requested by index.html from the root will also be correctly proxied to http://vpn_img1:1234/.

That should work as long as the applications served by vpn_img1 and vpn_img2 use relative paths to refer to other resources within the application. If they use absolute paths (for example, /styles/main.css instead of styles/main.css), then there might still be issues, and you would need to modify your applications to use relative paths instead.


The OP confirms in the comments:

Adding that resolver and rewrite /vpn_app1(.*) $1 break; proxy_set_header <hostname> "/vpn_app1/"; in the location block (as per documentation) did it!

Your final Nginx configuration would look something like this:

events {}

http {
    resolver 127.0.0.11;

    server {
        listen 80;

        # for a service that had an internal method for path redirection
        location ^~ /vpn_img1/ { 
            proxy_pass http://vpn_img1:1234;
        }

        # for a service without (trickier to get right)
        location ~ ^/vpn_img2/(.*)$ { 
            rewrite /vpn_img2(.*) $1 break;
            proxy_set_header Host $host;
            proxy_pass http://vpn_img2:2345;
        }

        # each service may need additional tweaking
        location ^~ /host_img1/ { 
            proxy_set_header Host $host;
            proxy_pass http://host_img1:3456;

            proxy_set_header   Upgrade $http_upgrade;
            proxy_set_header   Connection keep-alive;
            proxy_cache_bypass $http_upgrade;
            proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header   X-Forwarded-Proto $scheme;
            proxy_set_header   X-Forwarded-Host $http_host;
        }
    }
}

With this configuration, the incoming requests to /vpn_img1/... and /vpn_img2/... are being rewritten to /... before being passed to the corresponding services. And the proxy_set_header Host $host; line makes sure that the Host header from the incoming HTTP request is forwarded to the proxied server.

taynaron
  • 704
  • 1
  • 10
  • 23
VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • Thanks, this looks quite promising! Unfortunately, nginx fails to start, with the logs throwing `host not found in upstream "vpn" in /etc/nginx/nginx.conf:8`, which is where the first proxy_pass command is located (I get the same error for line 12, the next proxy_pass command, if I comment out the first location block). I've ensured that both vpn and nginx have `network_mode: bridge`, and have tried adding vpn_img1 and vpn_img2 to nginx's `depends_on` section. – taynaron Jul 23 '23 at 21:11
  • @taynaron Thank you for your feedback. I have edited the answer to address your comment. – VonC Jul 23 '23 at 21:45
  • Thanks for all your help! Now host_img1 and host_img2 are unable to find vpn_img1 and vpn_img2. The proxy seems to get off to a good start: :8000/vpn_img1/ itself returns a 200, but all its contents and subpages return a 404. – taynaron Jul 24 '23 at 04:43
  • @taynaron OK, I have edited the answer to address your comment. – VonC Jul 24 '23 at 04:51
  • Thanks for all the help! I've given you the check, as while it's still not working for me, this is definitely going to work given enough tinkering. Even files at / for a given location are getting a 404; so /index.html or /gettext.js return a 404. Guess I need to bone up on my nginx. Any thoughts/tips? Thanks! – taynaron Jul 24 '23 at 13:54
  • @taynaron Thank you for your feedback. I have edited the answer to address your 404 issue. – VonC Jul 24 '23 at 17:43
  • I'm sorry, I'm not sure what I'm missing. I'm getting 502s with the latest nginx.conf (and aimilar): `"GET /vpn_img1/ HTTP/1.1" 502 157 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/115.0" [error] 22#22: *1 no resolver defined to resolve vpn_img1, client: 192.168.0.185, server: , request: "GET /vpn_img1/ HTTP/1.1", host: "192.168.0.195:8000"` I can't imagine it being a docker-compose issue, but I have no idea what could be left. Many other sites use your above example just fine: https://github.com/DmitryFillo/nginx-proxy-pitfalls – taynaron Jul 25 '23 at 03:03
  • @taynaron Makes sense actually: I have edited the answer to address the 502 "`no resolver defined to resolve vpn_img1`" issue. – VonC Jul 25 '23 at 05:35
  • Yes! Adding that resolver and `rewrite /vpn_app1(.*) $1 break; proxy_set_header "/vpn_app1/";` in the location block (as per https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_set_header) did it! Thanks so much for your help! I'll rewrite my question in a few days to clarify some of the parts of the question that came up after discussion. Thanks for all your help! – taynaron Jul 26 '23 at 13:52
  • @taynaron Fantastic! I have included your comment in the answer, as well as the final NGiNX configuration: let me know if I missed anything. – VonC Jul 26 '23 at 13:59