Container Networking¶
Container networking is surprisingly complex for something that markets itself as "just run it in a container." On Linux, the container runtime creates real bridge interfaces, inserts iptables rules, and runs its own DNS resolver. On macOS, both Podman and Docker Desktop run a Linux VM and proxy traffic through userspace — a fundamentally different model that produces different failure modes. This page covers how each network mode works, how container DNS resolution differs from host DNS, and how container networking interacts with VPNs and host network assumptions.
The framework recommends Podman as the primary container runtime (see
Containers and Devcontainers). Podman's CLI
is intentionally Docker-compatible — podman run, podman network
create, podman compose all work identically. Where this page shows
podman commands, substitute docker if your environment requires
it. Behavior differences between runtimes are called out explicitly.
The two runtime models¶
Linux: native networking¶
On Linux, Podman (and Docker) manipulate the host kernel's network stack directly:
- Bridge interfaces (
podman0,cni-podman0, ordocker0) are real Linux bridges visible inip link. - veth pairs connect each container's network namespace to the bridge.
- iptables/nftables rules handle NAT (masquerade for outbound traffic), port publishing (DNAT for inbound), and inter-container isolation.
- The embedded DNS server (
127.0.0.11on Docker; the network gateway IP on Podman) resolves container names on user-defined networks.
This is actual networking — the same primitives that production
infrastructure uses. Debugging tools (tcpdump, iptables -L,
ip netns exec) work directly.
Podman-specific note: Podman runs rootless by default. Rootless
containers use pasta (the default since Podman 5.0) or the older
slirp4netns for networking rather than manipulating host iptables
directly. (Switch back via default_rootless_network_cmd in
containers.conf.) This avoids the iptables
conflicts described later in this page but introduces its own
tradeoffs (slightly higher latency, different port binding behavior).
macOS: VM-proxied networking¶
Both Podman (via podman machine) and Docker Desktop run containers
inside a lightweight Linux VM. The networking path:
- Container traffic exits the container's network namespace into the VM's bridge.
- The VM's userspace proxy forwards traffic to the macOS host via a shared network socket.
- Port bindings appear on
localhoston the Mac because the proxy listens there.
Implications:
- There is no bridge interface on the macOS host.
ifconfigshows no container-related interfaces. - Container-to-host communication uses a special DNS name
(
host.docker.internalorhost.containers.internalfor Podman) rather than the host's IP, because the container is in a VM with a different network namespace. - Performance is lower because all traffic passes through userspace proxying rather than kernel forwarding.
tcpdumpon the macOS host does not see container traffic — the traffic is inside the VM.
Network modes¶
Bridge (default)¶
Every container gets its own IP on an isolated network. Containers on the same bridge can communicate; the outside world reaches them only through published ports.
# Default bridge (legacy, no embedded DNS between containers)
podman run -p 8080:80 nginx
# User-defined bridge (embedded DNS, containers resolve each other by name)
podman network create mynet
podman run --network mynet --name api myapp
podman run --network mynet --name db postgres
# "api" can resolve "db" by hostname; the reverse is also true
When to use: most development scenarios. Containers are isolated by default, communicate by name on shared networks, and expose only what you publish.
Subnet allocation:
The runtime allocates bridge subnets from a default pool. Typical defaults:
- Default bridge:
10.88.0.0/16(Podman) or172.17.0.0/16(Docker) - User-defined bridges: allocated sequentially from adjacent ranges.
- Compose creates one network per project, each getting its own subnet.
These subnets can conflict with corporate VPN routes that claim RFC 1918 ranges. See "VPN conflicts" below.
Host¶
The container shares the host's network namespace. No isolation, no NAT, no port publishing — the container's processes bind directly to host ports.
When to use:
- Performance-critical networking where NAT overhead matters.
- Tools that need to see host network traffic (packet capture, network debugging tools).
- Services that need to bind to many ports dynamically (some service meshes, mDNS responders).
Platform note: --network host on macOS (both Podman and Docker
Desktop) means the container uses the VM's network namespace, not
the Mac's. This is rarely what you want. On Linux it works as
described.
Overlay¶
Overlay networks span multiple hosts (Docker Swarm mode or Podman with plugins). Traffic between containers on different hosts is encapsulated in VXLAN and routed over the underlying network.
# Docker Swarm example:
docker network create --driver overlay --attachable myoverlay
docker run --network myoverlay --name svc1 myapp
When to use: multi-host container deployments without Kubernetes. Rare in local development.
Interaction with host networking: overlay networks add their own routing rules and VXLAN interfaces. These can conflict with VPN routes to the same subnet ranges, though this is uncommon in practice.
None¶
No networking at all. The container has only a loopback interface.
When to use: security-sensitive workloads that must not have network access (secret processing, offline builds).
DNS inside containers¶
The embedded DNS server¶
On user-defined bridge networks, the runtime provides an embedded DNS
server inside each container. The address differs by runtime: Docker
listens at 127.0.0.11; Podman (netavark + aardvark-dns) runs the
resolver on the network's gateway IP, which is what appears as the
nameserver in the container's /etc/resolv.conf. Either way, this
server:
- Resolves container names to their bridge IP addresses.
- Resolves service names (in Compose) to the set of container IPs behind that service.
- Forwards queries it cannot resolve to the upstream DNS servers configured for the container.
On the default bridge network, this embedded DNS does not exist. Containers on the default bridge can only resolve each other by IP.
Where upstream DNS comes from¶
When the runtime creates a container, it determines the upstream DNS by:
- Checking
--dnsflags on the run command. - Checking runtime configuration (
containers.conffor Podman,daemon.jsonfor Docker). - Copying the host's
/etc/resolv.conf(with filtering — see below).
The 127.0.0.53 problem (Linux):
If the host uses systemd-resolved, /etc/resolv.conf contains
nameserver 127.0.0.53. The runtime detects this and attempts to
substitute the real upstream servers from resolved. This detection
works most of the time but can fail with non-standard configurations,
leaving containers with no working DNS.
The VPN problem:
If the host's DNS changes while a container is running (VPN connects, DNS servers change), the container retains its original DNS configuration. Containers created before VPN connection cannot resolve corporate domains. Containers created after VPN connection may lose DNS when VPN disconnects.
The fix for both: explicit DNS in your Compose file:
Or runtime-wide:
Container-to-host communication¶
A container often needs to reach a service running on the host (a database, a mock server, a proxy). The mechanism differs by platform and runtime:
# Option 1: --network host (container IS on the host network)
podman run --network host myapp
# Option 2: Use the bridge gateway IP
podman inspect bridge | grep Gateway
# Option 3: Add host.docker.internal explicitly
podman run --add-host host.docker.internal:host-gateway myapp
# In a compose file:
services:
api:
extra_hosts:
- "host.docker.internal:host-gateway"
Port binding conflicts¶
When a container publishes a port (-p 8080:80), the runtime binds
that port on the host. Common conflicts:
- Another container already published the same port. The runtime refuses to start the second container.
- A host process already listens on that port. The runtime refuses to bind.
- VPN tunnel interface — on some full-tunnel VPN configurations,
port bindings on
0.0.0.0become unreachable because traffic to the host's IP routes through the tunnel rather than locally.
Podman rootless note: in rootless mode, Podman cannot bind ports
below 1024 without additional configuration (net.ipv4.ip_unprivileged_port_start
sysctl or a port forward helper).
Binding to localhost only¶
By default, -p 8080:80 binds on all interfaces (0.0.0.0:8080).
For development services that should only be reachable locally:
This avoids conflicts with VPN routing (traffic to 127.0.0.1 never
leaves the host) and avoids exposing development services to the
network.
Finding what's using a port¶
VPN conflicts¶
Subnet overlap¶
The most common conflict: the VPN routes 172.16.0.0/12 through the
tunnel, and the container runtime's bridge networks live in that same
range (Docker defaults to 172.17.0.0/16; Podman defaults to
10.88.0.0/16 which is less likely to conflict but can still overlap
with VPN-claimed ranges). Traffic destined for containers routes
through the VPN instead of locally.
Diagnosis:
# Check if the container subnet routes through VPN
# macOS:
route -n get 172.17.0.1
# Linux:
ip route get 172.17.0.1
# If the output shows the VPN interface (utun/tun), there's a conflict
Fix: move the runtime to a subnet outside the VPN's claimed range:
Or per-network: podman network create --subnet 192.168.201.0/24 mynet
Restart the runtime after changing configuration. Existing networks need to be recreated.
DNS resolution from containers through VPN¶
Containers that need to resolve corporate hostnames (internal APIs, databases) face a layered problem:
- The container's DNS points at the embedded resolver
(
127.0.0.11). - The embedded resolver forwards unresolved queries to the
container's upstream DNS (from
/etc/resolv.confat creation time). - That upstream needs to be the VPN's DNS server — but only for corporate domains.
Solution with user-defined networks:
services:
api:
networks:
- default
dns:
- 10.0.0.1 # Corporate DNS (through VPN)
- 8.8.8.8 # Fallback for public DNS
networks:
default:
driver: bridge
ipam:
config:
- subnet: 192.168.200.0/24 # Non-conflicting subnet
Routing container traffic through VPN¶
By default, container outbound traffic exits via the host's default route. If the VPN is split-tunnel and a container needs to reach a VPN-only destination:
- On Linux (rootful): iptables masquerade rules use the host's routing table. If the host has a route to the VPN subnet, container traffic follows it automatically.
- On Linux (rootless/Podman): traffic exits via slirp4netns or pasta, which also follows the host routing table — VPN routes apply.
- On macOS: traffic from containers routes through the VM's network stack to the host, then follows the host's VPN routes. This usually works transparently.
If it doesn't work, verify that the VPN route is present on the host and that the VPN is not filtering by source IP (some corporate VPNs only allow traffic from the tunnel interface's assigned IP).
iptables interaction (Linux, rootful only)¶
Docker (and rootful Podman with the netavark backend) inserts
iptables rules for container networking. Docker uses dedicated chains
(DOCKER, DOCKER-USER, DOCKER-ISOLATION-STAGE-*); Podman/netavark
uses NETAVARK-* chains. These rules:
- NAT outbound container traffic (masquerade).
- DNAT inbound traffic to published ports.
- Isolate networks from each other.
The problem: these rules are inserted with high priority. If you also manage iptables (or nftables) rules for other purposes (firewall, VPN kill switch, port forwarding), the container runtime's rules can shadow yours, or yours can break container networking.
Viewing the rules:
# Docker
sudo iptables -L -t nat --line-numbers | grep -i docker
sudo iptables -L DOCKER-USER
# Podman/netavark
sudo iptables -L -t nat --line-numbers | grep -i netavark
Docker's DOCKER-USER chain: Docker provides this chain specifically for user-added rules that should be evaluated before Docker's own rules. If you need to restrict container traffic (e.g., block containers from reaching certain hosts), add rules here rather than modifying the runtime's chains directly.
# Block containers from reaching a specific IP
sudo iptables -I DOCKER-USER -d 10.0.0.50 -j DROP
# Allow only established connections from containers to VPN range
sudo iptables -I DOCKER-USER -d 10.0.0.0/24 -m state --state NEW -j DROP
Podman rootless avoids all of this. Rootless networking uses slirp4netns or pasta, which operate entirely in userspace without iptables. This is one of the reasons the framework recommends Podman — rootless-by-default means fewer conflicts with host networking.
Compose networking¶
Compose (both podman compose and docker compose) creates a
network per project (<project>_default). All services in the Compose
file join this network and can resolve each other by service name.
services:
api:
build: .
ports:
- "8080:8080"
redis:
image: redis:7
postgres:
image: postgres:16
environment:
POSTGRES_PASSWORD: devonly
In this setup:
- api can connect to redis:6379 and postgres:5432 by service
name.
- Only api is exposed to the host (port 8080).
- redis and postgres are only reachable from within the Compose
network.
Sharing networks between Compose projects¶
Two Compose projects that need to communicate can share a network:
# project-a/compose.yml
services:
api:
networks:
- shared
networks:
shared:
name: shared-dev-network
driver: bridge
# project-b/compose.yml
services:
worker:
networks:
- shared
networks:
shared:
external: true
name: shared-dev-network
Debugging container networking¶
# What network is a container on?
podman inspect <container> | jq '.[0].NetworkSettings.Networks'
# What IP does it have?
podman inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' <container>
# Can the container reach the host?
podman exec <container> ping -c1 host.containers.internal
# Can the container resolve DNS?
podman exec <container> nslookup example.com
# What DNS servers is the container using?
podman exec <container> cat /etc/resolv.conf
# Capture traffic on the container bridge (Linux only)
sudo tcpdump -i podman0 -n port 5432
# List all networks and their subnets
podman network ls
podman network inspect podman | jq '.[0].subnets'