skip to content

Container A publishes port 8080 on the Docker host. Container B, attached to a user-defined bridge network, tries to reach A by connecting to the host's IP address on port 8080. Does that work, what is the term for that traffic pattern, and what should B do instead?

level: middleimportance: nice to knowfreq 34%

answer

  1. out to the host and back in = hairpin / NAT loopback
  2. served by docker-proxy or hairpin NAT rules
  3. loses real source IP, depends on host address
  4. unpublishing or loopback-binding breaks it silently
  5. fix: shared network + container port, not host port

basics

~20 s

It usually works, going out to the host and back in through the NAT rules; that loop is called hairpin traffic. It is wasteful and fragile: it depends on the host address, loses the real source IP and breaks if the mapping changes. B should join A's network and connect to A's container port directly, with no published port involved.

solid answer

~50 s

It generally does work: B's packet leaves the bridge, hits the host address, is DNATed by the same rules that serve external clients, and comes back into A. The path that leaves and re-enters through the host is called **hairpinning**, and Docker supports it either through the `docker-proxy` relay or through hairpin NAT rules when the userland proxy is disabled. But it is the wrong design. It depends on knowing a host IP, which varies by environment (`host.docker.internal` on Desktop, a bridge gateway or real NIC address on Linux); it doubles the traversal and adds NAT overhead; it masks the true source address so A logs the gateway; and it only works because a published mapping exists, so removing the publication silently breaks internal traffic. The right answer is to put A and B on a shared user-defined network and have B connect to A's **container** port, for example `a:80` — no publishing required, no NAT, real source addresses, and it keeps working when the mapping is removed.

code

bash · 8 lines
bash
# fragile: B reaches A through the host's published port
docker exec b curl -s http://172.17.0.1:8080/health

# preferred: same network, container port, no publishing needed
docker network create appnet
docker network connect appnet a
docker network connect appnet b
docker exec b curl -s http://a:80/health

go deeper

for a junior

Know that containers should talk over a shared network using the container port, not via the host's published port.

for a middle

Name hairpinning, explain that it works through docker-proxy or hairpin NAT, and list why it is fragile.

for a senior

Add source-address loss, the hidden coupling between external exposure and internal calls, and the diagnosis steps when it misbehaves.

for a principal

State the invariant — published ports for outside clients, network membership for inside clients — and enforce it so exposure can be tightened without breaking internal traffic.

## What hairpinning is Hairpin (or NAT loopback) traffic is a connection that leaves a network interface and immediately comes back in through the same NAT device to reach a host on the network it started from. In Docker terms: a container sends a packet to the host's address and published port, the host's DNAT rules rewrite the destination to a container on the same bridge, and the packet returns over the bridge it just left. Docker makes this work in one of two ways. With the userland proxy enabled (the default in many setups), `docker-proxy` is a process listening on the host port; the container's connection terminates there and the proxy dials the target container, so no clever NAT is required. With `"userland-proxy": false`, the kernel path handles it via hairpin NAT plus masquerading, which is why the target sees the bridge gateway as the source rather than the originating container. ## Why it is fragile **Address discovery.** B must know an address for the host. On Docker Desktop that is `host.docker.internal`; on Linux it is typically the bridge gateway or a real NIC address, neither of which is stable across environments. Configuration ends up carrying environment-specific host addresses, which is exactly what container networking is supposed to remove. **Source-address loss.** Whether the path goes through the proxy or through masquerading, the target usually sees the gateway or proxy address instead of B's container IP. IP-based logging, rate limiting or allowlisting in A then sees one indistinguishable client. **Hidden coupling.** The internal call only works because a *public* mapping exists. Someone tightening exposure — unpublishing the port, or rebinding it to `127.0.0.1` — breaks an internal dependency that has nothing to do with external access. Loopback binding in particular kills hairpin access from containers immediately, since the container's traffic does not arrive on the host loopback. **Cost.** Two extra traversals of the host stack and, with the proxy, a userland copy per byte. Trivial at low volume, measurable on hot paths. ## The right pattern Attach A and B to the same user-defined network and connect to the **container** port using A's name. Publishing plays no part: members of a network reach each other on every listening port. Note the port number in use is the container's real port, not the host-side number — `-p 8080:80` means peers use 80, and copying the host number into internal configuration is a frequent bug. This gives environment-independent configuration, no NAT, true source addresses, and a clean invariant: published ports serve clients outside the container world, network membership serves clients inside it. ## Related host-side behaviour A sibling question is why a process **inside** A cannot always reach its own service via the host's published address. Inside A, `127.0.0.1` is A's own loopback and does reach its own service directly, which is fine; the host address route is again a hairpin and is best avoided for the same reasons. From the host itself, `curl 127.0.0.1:8080` works because locally generated packets traverse nat/OUTPUT, which also jumps to Docker's DNAT chain, and because the userland proxy binds the port. If the mapping was made with an explicit host IP, only that address answers — a mapping created as `-p 192.168.1.10:8080:80` will not answer on 127.0.0.1. ## Quick diagnosis If hairpin access misbehaves, check three things: whether the mapping is bound to a specific host address rather than 0.0.0.0, whether the userland proxy is disabled on that daemon, and which address the container is actually using for "the host". Then, rather than tuning the hairpin, move the call onto a shared network — that is the fix that removes the whole class of problem.

  • Which port number does B use when it connects over a shared network?
    The container port, not the host-side number. With -p 8080:80 the service is listening on 80 inside the container, so peers connect to a:80. Copying the host number 8080 into internal configuration is a very common mistake and produces connection refused.

Posting a letter to your neighbour by sending it to the city sorting office instead of walking it next door.

saying these in an interview costs you the question

  • Configuring internal service-to-service calls against the host IP and published port
  • Using the host-side port number for container-to-container connections
  • Assuming hairpin traffic preserves the originating container's source IP
  • Not realising that binding a mapping to 127.0.0.1 breaks container hairpin access
  • Believing containers must publish ports to talk to each other

context