A container that cannot reach anything behind the corporate VPN usually lives in a different network namespace than the tunnel. The host client's routes, DNS servers and firewall marks belong to the host's stack; a bridge-network container has its own. That single fact explains most of the reports that reach a platform team, and it is also why "the VPN is broken" and "Docker bypasses the VPN" can both be wrong at the same time. Two very different setups hide behind one symptom, so identify which one you have before changing a setting.

Find out whose namespace owns the tunnel

On Docker Desktop for macOS or Windows, containers run inside a Linux virtual machine. A VPN client installed on the desktop creates a tunnel in the desktop operating system, and the virtual machine never sees it. No container configuration repairs that, because the interface the container would need does not exist in its network namespace at all.

On a Linux host the tunnel lives in the host namespace, and bridge containers normally reach it through the host routing table with source NAT applied on the way out. When that path fails, the cause is usually forwarding policy, reverse-path filtering or an overlapping subnet rather than a missing route. Confirm which of the two worlds you are in before reading any further:

docker info --format '{{.OperatingSystem}}'
docker run --rm --net=host alpine ip route | head -5
docker run --rm alpine ip route | head -5

The difference between those two route tables is the whole problem in one screen. If the host view contains the tunnel interface and the bridge view does not, you are looking at namespace isolation, and everything else is a choice about how to cross it.

Host networking is a test, not a design

Running a container with host networking, which Docker documents as sharing the host network namespace, makes it inherit the tunnel interface and its routes directly. If the same request then succeeds, the route theory is confirmed and the failure is namespace isolation rather than credentials, DNS or an access rule.

docker run --rm --net=host curl -sS -o /dev/null -w '%{http_code}\n' https://api.internal/health

Keep it as a diagnostic. Host networking gives up port publishing and most of the isolation you wanted from a container, and on Docker Desktop "host" means the virtual machine, not the desktop, so a success or failure there can mislead you if you forget which host you were addressing.

A tun device and the privileges that come with it

A VPN client inside the container needs a tunnel device and permission to configure it. The common shortcut is --privileged, which grants every device and relaxes security policy across the container. That is far more than this job requires, and it is hard to justify in a review.

sudo modprobe tun
docker run --rm -it --device /dev/net/tun --cap-add NET_ADMIN myvpn-client

The narrower form maps the single device node and adds only the network administration capability. Load the module on the host first so the node exists. If the container starts and the client reports that it cannot open the tunnel device, check the mapping before you suspect the profile, because that error is usually literal.

This design also means the client runs per container. That is the right answer when each workload needs its own tunnel, and the wrong answer when you just wanted the host's tunnel to be visible, which is what the platform team probably assumed you were doing.

Bridge NAT changes the source the VPN sees

With the default bridge, outbound container traffic is translated to the host address. Behind a VPN the corporate side therefore sees the host's tunnel address rather than the container's, per-source access rules behave as if the host made the request, and two containers become indistinguishable to the VPN server.

macvlan or ipvlan hands the container a LAN address instead, which helps with LAN discovery and makes the container look like a peer on the segment. It also bypasses the host tunnel entirely, so such a container will not reach VPN-only prefixes unless the VPN is reachable from that LAN as well.

Both shapes are legitimate; they answer different requirements. If the requirement is "this container must reach a VPN-only service", you are choosing between routing through the host and terminating the tunnel inside the container. Write that choice down before the deployment, not after the first failed request. Whichever you pick, record the source address the far end will see, because that is what its access rules and its logs will show. A design that cannot state that address is not finished.

DNS follows the daemon, not the tunnel

A container gets its resolver configuration when it starts, copied from the daemon or from the image. Addresses pushed by the VPN after it connects arrive too late for that copy, and they are pushed to the host resolver rather than into the container.

Set the resolver explicitly for the service, or point it at an internal resolver that is reachable through the tunnel. A container that resolves a private name to a public address is usually using a public resolver, not suffering from a broken route, and the fix belongs in the compose file or daemon configuration.

The same reasoning applies to the exit address question. If each container must appear from a different VPN address, one host tunnel cannot deliver it because NAT collapses them into one source. The workable shapes are a client in each container's namespace or a sidecar that owns the tunnel and shares its stack, and both change how the application is addressed.

Checklist before you close the ticket: identify the namespace that owns the tunnel, prove the path with host networking, map only the tun device you need, remember the source address after NAT, set the resolver explicitly, and treat per-container exit addresses as an architecture decision rather than a flag.

Nuŋlɔɖi siwo ku ɖe eŋu