Docker Proxy Process Explained: Why Userspace TCP Proxy is Needed for Published Ports


1 views

When you expose container ports using Docker's -p flag, you'll notice docker-proxy processes appearing in your process list. Each published port gets its own proxy process, as shown in your example:

docker-proxy -proto tcp -host-ip 127.0.0.1 -host-port 22222 -container-ip 172.17.0.2 -container-port 22
docker-proxy -proto tcp -host-ip 127.0.0.1 -host-port 5555 -container-ip 172.17.0.3 -container-port 5555

Docker uses two complementary mechanisms for port forwarding:

  1. iptables NAT rules (primary method)
  2. docker-proxy (fallback method)

The iptables rules handle most traffic efficiently in kernel space, while the userspace proxy serves important edge cases:

Chain DOCKER (2 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 DNAT       tcp  --  !docker0 *       0.0.0.0/0            127.0.0.1            tcp dpt:22222 to:172.17.0.2:22

The userspace proxy handles scenarios where pure iptables would fail:

  • Localhost traffic: iptables NAT rules don't apply to connections from localhost
  • IPv6 support: Some systems need userspace proxy for IPv6 compatibility
  • Complex routing: When packets bypass standard network paths

While userspace proxying adds overhead, Docker's implementation is optimized:

# Compare performance:
$ iperf -s -p 5555  # Native
$ docker run -p 5555:5555 network-test iperf -s -p 5555  # Through proxy

For most workloads, the difference is negligible (1-3% overhead). The proxy only handles traffic that couldn't be routed via iptables.

You can control proxy behavior with these Docker daemon flags:

--userland-proxy=true|false
--userland-proxy-path="/usr/libexec/docker/docker-proxy"

Disabling the proxy (--userland-proxy=false) improves performance but breaks localhost access to containers.

When debugging connection problems:

# Check active proxies:
$ sudo ss -tulnp | grep docker-proxy

# Monitor traffic:
$ sudo strace -p $(pgrep -f "docker-proxy.*5555")

Remember that published ports appear in three places: proxy processes, iptables rules, and Docker's internal state.


When you run containers with published ports (-p flag), Docker creates a docker-proxy process for each port mapping. This becomes visible when you inspect running processes:

$ ps -Af | grep proxy
root      4776  1987  0 01:25 ?        00:00:00 docker-proxy -proto tcp -host-ip 127.0.0.1 -host-port 22222 -container-ip 172.17.0.2 -container-port 22
root      4829  1987  0 01:25 ?        00:00:00 docker-proxy -proto tcp -host-ip 127.0.0.1 -host-port 5555 -container-ip 172.17.0.3 -container-port 5555

The traffic flow works like this:

  1. External request hits host IP:port (e.g., 127.0.0.1:22222)
  2. iptables DNAT rule redirects to docker-proxy
  3. docker-proxy forwards to container (172.17.0.2:22)
  4. Response travels back through the same path

Here are the relevant iptables rules Docker creates:

$ sudo iptables -t nat -L -n -v
Chain PREROUTING (policy ACCEPT 1 packets, 263 bytes)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 DOCKER     all  --  *      *       0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL

Chain OUTPUT (policy ACCEPT 1748 packets, 139K bytes)
 pkts bytes target     prot opt in     out     source               destination         
   32  7200 DOCKER     all  --  *      *       0.0.0.0/0           !127.0.0.0/8          ADDRTYPE match dst-type LOCAL

Chain DOCKER (2 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 DNAT       tcp  --  !docker0 *       0.0.0.0/0            127.0.0.1            tcp dpt:22222 to:172.17.0.2:22
    0     0 DNAT       tcp  --  !docker0 *       0.0.0.0/0            127.0.0.1            tcp dpt:5555 to:172.17.0.3:5555

The userspace proxy becomes crucial in these scenarios:

  • When using --net=host but still needing port publishing
  • On systems without hairpin NAT support
  • When dealing with ICMP or non-TCP/UDP protocols
  • For localhost-bound services (-p 127.0.0.1:8080:80)

While docker-proxy adds some overhead, it's generally minimal for most workloads. You can benchmark the difference:

$ docker run -d -p 8080:80 nginx
$ ab -n 10000 -c 100 http://localhost:8080/
$ docker run --net=host nginx
$ ab -n 10000 -c 100 http://localhost:80/

For high-performance scenarios, consider these alternatives:

# Disable userland proxy globally
dockerd --userland-proxy=false

# Disable for specific container
docker run -p 8080:80/tcp --userland-proxy=false nginx