NAT (Network Address Translation) #
When a Docker container can curl google.com, it doesn’t have its own public IP. When you can access http://localhost:8080 and reach an application inside a container, that container also has no routable IP. All that “magic” happens because of one mechanism working on the host: NAT (Network Address Translation).
NAT is the foundation that lets Docker apply the one host, many containers, one public IP principle. It’s also what keeps containers safe by default — without port mapping, containers are truly invisible from outside. Understanding NAT helps you solve confusing connectivity problems, choose the right network mode, and design secure architectures.
This article covers NAT in Docker in depth: the concept, iptables implementation, NAT’s two directions (outbound and inbound), the default policies Docker installs, and their implications for application design. After reading it, you’ll understand why curl google.com from a container works without manual configuration, and why containers can’t be pinged from the host without port mapping.
What Is NAT? #
Network Address Translation (NAT) is a technique for translating IP addresses from one space to another as packets pass through a router or gateway. In the Docker context, NAT is performed by the host acting as the gateway between containers (private IPs) and the outside network (host IP / internet).
NAT has three main variants:
- SNAT (Source NAT) — changes the source address. Usually used for outbound traffic (container → internet).
- DNAT (Destination NAT) — changes the destination address. Usually used for inbound traffic (host → container).
- MASQUERADE — an SNAT variant that automatically uses the IP of the outgoing interface (useful when the host IP isn’t static).
Docker uses both: MASQUERADE for outbound traffic, DNAT for port mapping.
flowchart LR
CT[Container<br/>172.18.0.2:8000] -->|1. outbound request| N1{MASQUERADE}
N1 -->|2. source = host IP| NET[Internet]
NET -->|3. reply to host| N2{DNAT}
N2 -->|4. destination = container IP| CT
EXT[Client<br/>203.0.113.5] -->|5. request to host:8080| H[Host:8080]
H -->|6. DNAT to container:80| CT
CT -->|7. response| H
H -->|8. response to client| EXTIn the diagram above, there are two main directions:
- Outbound (1→4): the container requests the internet, its source IP is MASQUERADEd into the host IP, and the reply is DNAT-ed back.
- Inbound (5→8): a client requests the host, DNAT forwards to the container, and the response returns to the client.
Why is NAT so important for Docker? Because containers have private IPs (172.x,192.168.x) that can’t be routed on the internet. Without NAT, containers could never talk to the internet, and the internet could never talk to containers. NAT solves both directions elegantly.
Docker’s Network Architecture for NAT #
To understand NAT in Docker, you first need to understand the default topology. When the Docker Engine runs on Linux, it creates:
| Component | Role |
|---|---|
docker0 (or a custom bridge) | Virtual switch for containers |
| Private subnet | e.g. 172.17.0.0/16 for the default bridge |
| iptables rules | NAT + firewall implementation |
| IP forwarding | The host must have ip_forward=1 (default on modern Linux) |
flowchart TB
subgraph HOST["Docker Host"]
direction TB
BR[Bridge: 172.17.0.1]
FW[iptables NAT rules]
HETH[Host eth0: 192.168.1.10]
BR --- FW --- HETH
end
subgraph CT["Container"]
ETH0[eth0: 172.17.0.2]
end
BR ---|veth| ETH0
HETH --> INET((Internet))Three things happen:
- Containers have private IPs on the bridge subnet.
- iptables on the host manages routing and NAT rules.
- Host eth0 is the only interface visible from the internet.
For outbound traffic, the container sends packets to the bridge → the bridge checks the routing table → routing says “via host eth0” → iptables SNAT/MASQUERADE → out to the internet. For inbound traffic (port mapping), packets enter the host port → iptables DNAT → bridge → container.
Outbound NAT: MASQUERADE #
When a container sends a request to the internet (e.g. apt-get update), Docker adds an iptables MASQUERADE rule for the container subnet.
# View the NAT rules (simplified)
iptables -t nat -L POSTROUTING -n -v
# Chain POSTROUTING (policy ACCEPT)
# pkts bytes target prot opt in out source destination
# 0 0 MASQUERADE all -- * !docker0 172.17.0.0/16 0.0.0.0/0
This rule means: every packet from the 172.17.0.0/16 subnet exiting not through docker0 will be MASQUERADEd (its source IP replaced with the outgoing interface’s IP).
The Detailed Flow #
sequenceDiagram
participant CT as Container
participant BR as Bridge
participant RT as Routing Table
participant IPT as iptables
participant NET as Internet
CT->>BR: GET http://google.com (src: 172.17.0.2)
BR->>RT: Destination not local
RT->>IPT: Via host eth0
IPT->>IPT: MASQUERADE: src becomes 192.168.1.10
IPT->>NET: Packet exits
NET-->>IPT: Response
IPT-->>CT: Reverse-NAT back to 172.17.0.2The 172.17.0.2 container sends a packet with its own source IP. iptables “hides” that IP and replaces it with the host IP before the packet exits to the internet. Google’s server sees the request coming from 192.168.1.10 (the host IP) and replies to the host. When the reply arrives, iptables reverse-NATs and forwards it to the original container.
Why MASQUERADE instead of plain SNAT? Because MASQUERADE automatically reads the IP from the outgoing interface — fitting hosts with dynamic IPs (DHCP). For servers with static IPs, SNAT can be slightly more efficient, but MASQUERADE is Docker’s default.
IP Forwarding Configuration #
For the host to forward packets from containers outward (and back), the Linux kernel needs ip_forward=1:
# Check status
sysctl net.ipv4.ip_forward
# net.ipv4.ip_forward = 1
# Set manually if needed
sudo sysctl -w net.ipv4.ip_forward=1
Docker requires ip_forward=1 and throws an error if it isn’t enabled. On modern distributions, it’s on by default.
Inbound NAT: DNAT and Port Mapping #
The second direction — the one you’ll encounter most often — is port mapping. When you run docker run -p 8080:80 nginx, Docker creates a DNAT rule in iptables.
# View the DNAT rule
iptables -t nat -L PREROUTING -n
# Chain PREROUTING
# -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80
Meaning: every TCP packet entering host port 8080 will be DNAT-ed to 172.17.0.2:80 (the container).
The Detailed Port Mapping Flow #
sequenceDiagram
participant U as User
participant H as Host:8080
participant PR as PREROUTING (DNAT)
participant BR as Bridge
participant CT as Container:80
U->>H: TCP to 192.168.1.10:8080
H->>PR: Packet arrives
PR->>PR: DNAT: dst becomes 172.17.0.2:80
PR->>BR: Forward to bridge
BR->>CT: Reaches the container
CT-->>BR: HTTP response
BR-->>H: Back to the host
H-->>U: Response reaches the userNote: The user never knows the container’s real IP. They only see 192.168.1.10:8080 (the host). The container can be restarted with a different IP and the user won’t know — Docker automatically updates the DNAT rule. That’s the power of Docker’s abstraction.Port Mapping Formats #
# Full format
-p <host_ip>:<host_port>:<container_port>
# Examples
docker run -p 8080:80 nginx # binds to 0.0.0.0:8080
docker run -p 127.0.0.1:8080:80 nginx # binds to localhost only
docker run -p 192.168.1.10:8080:80 nginx # binds to a specific LAN IP
docker run -p 80 nginx # random host port
docker run -p 8080:80/udp nginx # UDP protocol
Anti-pattern:-p 8080:80without a host IP binds to0.0.0.0— meaning every interface, including the public IP if the host has one. For databases (Postgres, MySQL, MongoDB, Redis) that must not be publicly accessible, always bind to127.0.0.1or a specific internal IP.
Docker’s Default Policy: Safe or Problematic? #
Docker adds many iptables rules automatically. Understanding its default policies is important for troubleshooting.
The FORWARD Chain #
Docker changes the default policy of the FORWARD chain from ACCEPT to DROP. This improves security (unknown traffic is denied), but can cause confusion when custom rules are added.
iptables -L FORWARD -n
# Chain FORWARD (policy DROP)
# -A FORWARD -i docker0 -o docker0 -j ACCEPT # container-to-container
# -A FORWARD -i docker0 ! -o docker0 -j ACCEPT # container-to-internet (outbound)
# -A FORWARD ... (reverse) # inbound
Custom User Chains #
Docker creates dedicated chains named DOCKER and DOCKER-USER so its rules stay isolated and easy to reset.
iptables -L DOCKER -n
# Chain DOCKER
# -A DOCKER -p tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80
Best practice: if you need to add custom firewall rules, add them to theDOCKER-USERchain, not theDOCKERchain or directly inFORWARD. Docker adds custom rules at the top ofDOCKER-USER, so your rules get evaluated first — before Docker’s internal rules.
Double NAT: a Rarely Realized Risk #
One scenario that confuses beginners: containers behind Docker, Docker behind cloud NAT. This is called double NAT and has important implications.
flowchart LR
U[User] -->|1. request| CSP[Cloud Provider NAT]
CSP -->|2. dst = VM public IP| VM[VM Public IP]
VM -->|3. DNAT to container| CT[Container]
CT -->|4. response src = container IP| VM
VM -->|5. SNAT to public IP| CSP
CSP -->|6. back to user| UProblems that arise:
- The original source IP is lost — the application in the container doesn’t know the user’s real IP (it’s been SNAT-ed into the VM IP).
- WebSocket / long-lived connections sometimes break because NAT state can time out.
- Port forwarding to containers must be explicit — cloud NAT isn’t automatic.
Solutions: use Host networking or proxy protocol mode if you need the user’s real IP. For most cases, the real IP isn’t critical because a reverse proxy (Nginx, Traefik) adds an X-Forwarded-For header.
Applications that need the user’s real IP (rate limiting, geo-blocking, fraud detection) will lose that information because of double NAT. Solutions: enable proxy protocol on the reverse proxy and read X-Forwarded-For correctly in the application.iptables: an Important Debugging Companion #
When a container can’t connect to the internet, or port mapping doesn’t work, iptables is the first place to check. Here are useful commands.
# View all NAT rules
sudo iptables -t nat -L -n -v
# View rules specific to one container/network
sudo iptables -t nat -L -n -v | grep 172.18.0
# View FORWARD rules
sudo iptables -L FORWARD -n -v
# Trace a single packet (advanced debugging)
sudo iptables -t raw -A PREROUTING -p tcp --dport 80 -j TRACE
sudo iptables -t raw -A OUTPUT -p tcp --dport 80 -j TRACE
# See in dmesg
# View rule counters (did the rule actually match?)
sudo iptables -t nat -L POSTROUTING -n -v
# The "pkts" and "bytes" columns show how many times the rule was hit
# Reset all Docker rules (be careful!)
sudo iptables -t nat -F
sudo iptables -t filter -F
sudo systemctl restart docker
# Docker will rebuild its rules on restart
Never flush iptables in production without a backup.iptables -Fremoves all rules. If this host has important firewall rules, connections can be disrupted. Always back up first:iptables-save > backup.rules.
NAT vs Other Network Modes #
NAT isn’t the only way Docker connects containers to the network. Each network mode has different NAT behavior.
| Network Mode | Outbound NAT? | Inbound NAT? | Isolation |
|---|---|---|---|
| bridge | ✅ MASQUERADE | ✅ DNAT (port mapping) | Per bridge |
| host | ❌ No | ❌ No | None |
| none | ❌ No | ❌ No | Maximum |
| overlay | ✅ (depends) | ✅ | Per network |
| macvlan | ❌ (direct LAN IP) | ❌ (LAN IP) | Layer-2 |
Bridge uses two-way NAT because containers have private IPs. Host has no NAT because containers share the host network. None has no NAT because there’s no network. Macvlan has no NAT because containers have their own LAN IPs.
When Docker NAT Becomes a Problem #
NAT is generally invisible and works well. But there are situations where Docker NAT becomes a limitation:
1. UDP hole punching — some P2P applications need direct UDP connections. NAT makes this hard because iptables state can expire.
2. Real-time multiplayer gaming servers — the extra latency from NAT and the bridge can be felt. Solution: network_mode: host.
3. VoIP / video calling — some real-time protocols struggle with NAT. STUN/TURN servers may be needed.
4. High-throughput database replication — for high-bandwidth replication, NAT overhead can be significant. Solution: a dedicated network or host mode.
5. ICMP / pinging from host to container — without port mapping, you can’t ping a container from the host via the host IP. Solution: ping the container IP directly from inside the host namespace.
# Pinging a container from the host (the way that works)
docker inspect <container> --format '{{.NetworkSettings.IPAddress}}'
ping 172.17.0.2
The most effective debugging method: enter the container’s namespace and test from inside.docker exec -it <container> shthenping,curl, orwgetto the target. This bypasses outbound NAT entirely.
Important NAT Commands #
# View the complete NAT rules
sudo iptables -t nat -L -n -v --line-numbers
# View only Docker-relevant rules
sudo iptables -t nat -S | grep -E 'DOCKER|MASQUERADE'
# Check whether IP forwarding is active
cat /proc/sys/net/ipv4/ip_forward
# or
sysctl net.ipv4.ip_forward
# Monitor NAT traffic in real time (needs iptraf or iftop)
sudo apt install iptraf-ng
sudo iptraf-ng
Learning Resources for Going Deeper on NAT #
For those wanting to dive deeper into NAT and iptables, some classic references worth reading:
- Linux man pages —
man iptables,man iptables-extensions. Very complete official documentation. - “Linux Firewalls” by Steve Suehring — a book covering iptables and NAT systematically.
- Docker networking documentation — the “Understand Docker networking” section at docs.docker.com.
- Cilium eBPF documentation — for the advanced reader wanting to see how modern networking replaces iptables with eBPF.
Industry trend: eBPF is increasingly used in production as an iptables replacement. Tools like Cilium, Calico, and Flannel enable faster, more observable container networking. The NAT concepts are similar, but the implementation lives at the kernel level and is far more flexible.
Summary #
- NAT is the mechanism where the host translates container IPs (private) into the host IP (public/unique) and vice versa. Without NAT, containers can’t access the internet and can’t be reached from outside.
- Outbound NAT uses MASQUERADE (an SNAT variant) — when a container makes an outbound request, its source IP is replaced with the host IP, then reverse-translated when the reply arrives.
- Inbound NAT uses DNAT via port mapping (
-p 8080:80). Packets entering the host port are DNAT-ed to the container IP; responses return via the host.- Docker manages iptables automatically — the
DOCKER,DOCKER-USERchains and MASQUERADE/DNAT rules are added when containers or networks are created.- IP forwarding (
ip_forward=1) is mandatory. Docker requires it and will error if it’s off.- Docker’s FORWARD default policy is DROP (not ACCEPT) for security. Rules for container traffic are added explicitly.
- Double NAT (containers behind cloud NAT) is common in the cloud. The user’s real IP is lost; for applications needing the real IP, use proxy protocol.
- Anti-pattern:
-p 8080:80without a host IP binds to0.0.0.0— databases become publicly exposed. Always bind to127.0.0.1for internal services.- For debugging,
iptables -t nat -L -n -vis your best friend.pktsandbytesshow whether a rule actually matched.- NAT fits regular web applications, APIs, and workloads that don’t need the user’s real IP. Less suitable for: P2P, real-time gaming, high-throughput replication, VoIP.