Container images can carry every OpenVPN binary and configuration file they need and still fail at the first network call. The error names /dev/net/tun because the application asked the kernel for a TUN interface and the container had neither the device node nor the capability to create one. Those are two separate permissions, and fixing one leaves the other exactly where it was.

Separating them takes one command each. The error message afterwards tells you which boundary you were actually standing at.

The Device Node and the Capability Are Two Doors

The device node is a filesystem object, so a container needs to see it in its own mount namespace. The capability is a permission, so the process needs CAP_NET_ADMIN in the namespace where the interface will be created. Passing the device through without the capability gives you a node that opens and an ioctl that fails. Granting the capability without the device leaves the application unable to open anything at all, which is why the two usually appear together in examples.

Docker's devices setting maps the host device into the container, while cap_add adjusts the capability set. A bind mount of the same path puts the file into view without applying device access semantics.

Reproduce It Outside the Compose File

Before editing the service, check that the host has the device and that a bare container can create an interface. The command below is deliberately minimal, and its only purpose is to separate a host problem from a compose problem. If it fails, no compose change will help.

ls -l /dev/net/tun
docker run --rm --cap-add NET_ADMIN --device /dev/net/tun:/dev/net/tun alpine sh -c 'apk add -q iproute2 && ip tuntap add dev tun9 mode tun && ip link show tun9'

A missing node on the host means the tun module is not available, which is a host-side fix rather than a container one. A node that exists but cannot be opened inside the container points at device access, and the message will say permission denied rather than no such file. The order of those two failures is the whole diagnosis. Write down which one you saw before changing a flag.

The Errors After TUN Are Different Problems

Once the interface appears, the next failure has a different owner. Authentication errors mean the tunnel reached the server and the credentials were rejected, so capability changes will not help. A tunnel that establishes while applications still fail points at routes, forwarding, DNS resolution inside the container, or the container's own network layout. Each of those is an ordinary networking question that the TUN permission cannot answer. Repeating the original test after each change keeps the failures from merging into one impression.

Copying every setting from a working example is how a minimal fix becomes an unreadable service definition. Bring over the ones this deployment needs and record why each is there.

Verify the Interface Inside the Container

Check the interface and the capability set from inside the running container rather than from the host. The commands below show the device node, the capability list, and whether an interface can be created at all. Run them in the same container that runs the client, and install the tooling if the image is minimal.

ls -l /dev/net/tun
capsh --print | grep -i net_admin
ip link add dev tun9 type tun && ip link show tun9

A capability present in the bounding set but absent from the effective set behaves differently from one that was never granted, and capsh shows both. If the node exists and the capability is there while creation still fails, the runtime is enforcing something else, such as a seccomp profile or an AppArmor policy that restricts the operation. That is a third boundary, documented by the runtime rather than by the application.

Record the container flags, the image and the exact error from each attempt. The next person to read that note can tell which door was closed without repeating the whole investigation.

Akụkọ ndị metụtara ya