Network Isolation #

One of Docker’s main strengths that’s often overlooked is network isolation — the ability to keep containers truly isolated from each other and from the outside network, unless explicitly allowed. Without good isolation, Docker just becomes a “lightweight VM” without meaningful protection.

Containers without network isolation = all containers can talk to each other, every service is exposed to the host network, and one small security hole can be fatal. This is the scenario often seen in production with sloppy setups: one container gets compromised, and the attacker can scan internal services, access databases, and exfiltrate data within minutes.

This article covers network isolation in depth: the concept, OS-level mechanisms, common segmentation patterns, internal networks, defense in depth, and best practices for multi-tenant environments. After reading it, you’ll be able to design secure, scalable container network topologies.

What Is Network Isolation? #

Network isolation is the principle that containers can’t freely communicate with each other unless explicitly allowed. Every container has “network boundaries” that can only be crossed through deliberate configuration.

flowchart TB
    subgraph Host["Docker Host"]
        subgraph NetA["Network A"]
            A1[Container A1]
            A2[Container A2]
        end
        subgraph NetB["Network B"]
            B1[Container B1]
            B2[Container B2]
        end
        subgraph NetC["Network C (internal)"]
            C1[(Database)]
        end
    end
    A1 -.->|can't| B1
    A1 -.->|can't| C1
    A2 <-->|can| A1
    B1 <-->|can| B2
    A2 <-.->|via API gateway| B1

Characteristics of good network isolation:

  • Default deny — a container can’t talk to another container without being on the same network.
  • Explicit allow — connections are only allowed when declared (network, port mapping, or policy).
  • Granular segmentation — frontend, backend, and data are separated into different networks.
  • Internal-only — services that don’t need the internet have no outbound access.
  • Egress control — where a container may talk out to is also controlled.
The main principle: isolation is the default, not an add-on. Containers are isolated from the very first second — you’re the one choosing to open that isolation. When in doubt, keep it isolated.

Network Namespaces: The Isolation Foundation #

At the OS level, the network namespace is the mechanism Docker uses to isolate containers. Each container runs in its own namespace with:

  • Its own interfaces (eth0, lo).
  • Its own routing table.
  • Its own iptables rules.
  • Its own sockets.
flowchart LR
    subgraph HOST["Host Network Namespace"]
        H_ETH[eth0: 192.168.1.10]
        H_BR[docker0]
        H_RT[Host routing]
    end
    subgraph NS1["Container A Namespace"]
        A_ETH[eth0: 172.18.0.2]
        A_LO[lo: 127.0.0.1]
    end
    subgraph NS2["Container B Namespace"]
        B_ETH[eth0: 172.18.0.3]
        B_LO[lo: 127.0.0.1]
    end
    A_ETH <-.->|veth| H_BR
    B_ETH <-.->|veth| H_BR
    H_ETH --- H_RT

Containers A and B are in different namespaces connected to the docker0 bridge via veth pairs. They don’t “see” each other’s interfaces directly. Communication only happens because the bridge forwards packets.

# List all network namespaces on the host
ip netns list

# See a container's namespace
docker run --rm --network bridge alpine sh -c 'cat /proc/self/net/dev'
# The container only sees its own eth0 and lo

# See the host namespace
ip link show
# Many veths and bridges will be visible

Isolation Layers #

Docker network isolation works across several layers:

Layer 1: Network Namespaces #

Containers have their own network namespaces, isolated from the host and other containers. Default for all containers.

Layer 2: Network Drivers #

The choice of bridge, host, none, overlay, macvlan determines the type of isolation. Bridge = isolated within its own bridge. Host = not isolated.

Layer 3: Network Topology #

User-defined networks provide natural boundaries. Containers on network A can’t talk to network B without routing.

Layer 4: Port Mapping #

-p is the exit door from isolation. Without -p, a container is truly hidden from the host. With -p, specific ports are exposed.

Layer 5: iptables Rules #

Docker adds iptables rules for NAT, port mapping, and default-deny. Custom rules can add extra firewall layers.

flowchart TB
    L1[Layer 1: Network Namespace] --> L2[Layer 2: Network Driver]
    L2 --> L3[Layer 3: Network Topology]
    L3 --> L4[Layer 4: Port Mapping]
    L4 --> L5[Layer 5: iptables]
    style L1 fill:#dfd
    style L2 fill:#dfd
    style L3 fill:#dfd
    style L4 fill:#fdd
    style L5 fill:#fdd

Green = isolated by default, Red = open to host/internet by default. Layers 4 and 5 need explicit configuration to maintain isolation.


Segmentation: The Foundation of Good Isolation #

Segmentation is separating networks by function or security level. It’s the most effective isolation pattern.

The Classic Three-Layer Segmentation #

flowchart TB
    subgraph Edge["Edge (public)"]
        LB[Load Balancer]
    end
    subgraph App["Application (internal)"]
        API1[API 1]
        API2[API 2]
    end
    subgraph Data["Data (most restricted)"]
        DB[(Database)]
        Cache[(Cache)]
    end
    LB --> API1
    LB --> API2
    API1 --> DB
    API1 --> Cache
    API2 --> DB
    API2 --> Cache
# docker-compose.yml
version: "3.9"
services:
  loadbalancer:
    image: traefik
    ports:
      - "443:443"
    networks:
      - edge
  
  api:
    image: my-api
    networks:
      - edge
      - app
    deploy:
      replicas: 3
  
  worker:
    image: my-worker
    networks:
      - app
  
  postgres:
    image: postgres
    networks:
      - data
    # No port mapping!
  
  redis:
    image: redis
    networks:
      - data

networks:
  edge:
  app:
    internal: false  # can reach the internet for API calls
  data:
    internal: true   # NO outbound access

Characteristics:

  • edge network — home of the load balancer, the only publicly exposed surface.
  • app network — home of application logic, can talk out (e.g. to third-party APIs).
  • data network — internal only, no internet access. The database is truly hidden.
  • Service placementapi sits on both edge and app (as a bridge). postgres only on data (hidden from the edge).
internal: true is a powerful feature. A network with internal: true has no outbound gateway to the internet. Containers on this network are truly off-grid. Perfect for databases, caches, internal services — anything that doesn’t need (and must not have) outbound access.

Multi-Tenant: Tenant A vs Tenant B #

For multi-tenant environments (many users/customers), per-tenant segmentation is a must.

services:
  tenant-a-app:
    image: tenant-a-app
    networks:
      - tenant-a
  
  tenant-b-app:
    image: tenant-b-app
    networks:
      - tenant-b

networks:
  tenant-a:
    name: tenant-a-net
    internal: true
  tenant-b:
    name: tenant-b-net
    internal: true

Tenant A and Tenant B have different networks. They can’t see each other at all, even on the same host. A compromised tenant won’t threaten another tenant.

flowchart TB
    subgraph TenantA["Tenant A"]
        A1[App A]
        ADB[(DB A)]
        A1 --- ADB
    end
    subgraph TenantB["Tenant B"]
        B1[App B]
        BDB[(DB B)]
        B1 --- BDB
    end
    A1 -.->|X no access| B1
    style A1 fill:#ddf
    style B1 fill:#fdd

Internal Networks: Internet Access = 0 #

Networks with internal: true behave specially:

  • No outbound gateway to the host/internet.
  • No outbound NAT — packets can never leave.
  • Containers can still talk to each other on the same network.
  • Docker internal DNS is still active.
networks:
  internal-db:
    internal: true
# Verify: a container on an internal network can't ping google
docker run --rm --network internal-db alpine ping -c 1 8.8.8.8
# ping: connect: Network is unreachable

Use cases:

  • Databases — don’t need (and must not have) internet access.
  • Internal caches — Redis, Memcached.
  • Message brokers — Kafka, RabbitMQ.
  • Admin tools — pgAdmin, RedisInsight.
  • Internal APIs — service-to-service calls that must not be reachable from the internet.
Don’t use internal: true for services that fetch dependencies at runtime. For example, images that fetch configuration from S3 at startup. Images must already contain all dependencies at build time.

Default Bridge vs User-Defined Networks for Isolation #

Docker’s default bridge network has serious isolation weaknesses:

  • All containers on the default bridge share one subnet.
  • No internal DNS.
  • No segmentation.

User-defined networks provide far more granular isolation:

  • One network = one isolation boundary.
  • Containers on network A don’t talk to network B.
  • Internal DNS = safe service discovery.
  • Custom subnets and gateways possible.
# Anti-pattern: everything on the default bridge
docker run -d --name app my-app
docker run -d --name db postgres
docker run -d --name cache redis
# app, db, cache all on the same bridge
# A compromised cache can scan db

# Best practice: segmentation
docker network create app-net
docker network create data-net
docker run -d --name app   --network app-net my-app
docker run -d --name db    --network data-net postgres
docker run -d --name cache --network data-net redis
# app has no direct access to db/cache
# (unless manually connected)

Containers on Many Networks: Layered Security #

One container can connect to multiple networks. This is used to create layered security — the container becomes a “gateway” you can control.

docker network create edge --internal=false
docker network create app --internal=false
docker network create data --internal=true

docker run -d --name api --network edge nginx
# api also attached to app
docker network connect app api
# api also attached to data
docker network connect data api
flowchart LR
    Edge["edge (public)"] -->|eth0a| API[API]
    App["app (work)"] -->|eth0b| API
    Data["data (internal)"] -->|eth0c| API
    Data --> DB[(db)]
    Data --> Cache[(cache)]

The api container has three interfaces on three different networks. It becomes the single control point between:

  • Internet (via edge) ↔ Application (app)
  • Application (app) ↔ Data (data)

If api is compromised, the attacker can go through api to db. But db can’t be reached directly from the internet — it must go through api. This gives you one audit point, one policy enforcement point.

This is the “jump host” pattern long used in traditional security. The container acts as a bastion host. It’s more exposed, but it’s also the most monitored and audited.

Network Isolation and Production Security #

1. Default Deny: Disable Everything Unnecessary #

# Drop all default network policies
iptables -P FORWARD DROP

# Allow only the traffic needed
iptables -A FORWARD -i docker0 -o docker0 -j ACCEPT  # container-to-container
iptables -A FORWARD -i docker0 -j ACCEPT              # container egress

Docker adds these rules automatically, but some extra rules can strengthen isolation:

# Drop traffic between bridges (already default in Docker, but extra doesn't hurt)
iptables -A FORWARD -i br-a -o br-b -j DROP

2. Custom iptables for an Extra Layer #

Add rules in the DOCKER-USER chain (see the NAT article):

# Only allow traffic from network 10.0.0.0/8 to containers
iptables -A DOCKER-USER -s 10.0.0.0/8 -j ACCEPT
iptables -A DOCKER-USER -j DROP

3. NetworkPolicy (Kubernetes) #

For Kubernetes, NetworkPolicy is the formal way to declare isolation.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-policy
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: web
      ports:
        - protocol: TCP
          port: 8080
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: db
      ports:
        - protocol: TCP
          port: 5432
    # DNS to kube-system
    - to:
        - namespaceSelector:
            matchLabels:
              name: kube-system
      ports:
        - protocol: UDP
          port: 53

api may only receive from web, and may only talk to db and DNS. Default deny for all other traffic.

Without a NetworkPolicy in K8s, every pod can talk to every pod. That’s the K8s default. For production, NetworkPolicy is a must. Tools like Calico, Cilium, or Weave can enforce these policies.

Anti-Patterns That Violate Isolation #

1. Host Networking for Convenience #

# ✗ Anti-pattern: host networking to skip configuration
docker run -d --network host my-app
# No isolation. The container is a first-class host citizen.
# ✓ Solution: bridge + port mapping
docker run -d --network app-net -p 8080:8080 my-app

2. Default Bridge for Multi-Container Setups #

# ✗ Anti-pattern: everything on the default bridge, hardcoded IPs
docker run -d --name db postgres
docker run -d -e DB_HOST=172.17.0.2 my-api
# The IP can change on restart
# ✓ Solution: user-defined network + DNS
docker network create app-net
docker run -d --name db  --network app-net postgres
docker run -d -e DB_HOST=db --network app-net my-api

3. Exposed Databases #

# ✗ Anti-pattern: postgres with -p
docker run -d -p 5432:5432 postgres
# The database is exposed to every host interface
# ✓ Solution: no -p, postgres only on an internal network
docker run -d --network data-net --name db postgres
# data-net internal: true, no port mapping

4. Docker Socket Mounted #

# ✗ Anti-pattern: an ordinary container mounting docker.sock
docker run -d -v /var/run/docker.sock:/var/run/docker.sock my-app
# my-app can control the Docker daemon
# ✓ Solution: restrict to containers that genuinely need it (Portainer, etc.)
# For ordinary apps, DON'T mount docker.sock

5. All Services on One Network #

# ✗ Anti-pattern
docker network create all-in-one
docker run -d --name app --network all-in-one my-app
docker run -d --name db  --network all-in-one postgres
docker run -d --name admin --network all-in-one pgadmin
# admin can access db - even though it shouldn't
# ✓ Solution: separate them
docker network create data-net --internal
docker network create mgmt-net
docker run -d --name db    --network data-net postgres
docker run -d --name admin --network mgmt-net,data-net pgadmin
# admin has db access, but db isn't exposed

Monitoring Network Isolation #

To make sure isolation is working, good monitoring:

# List all networks and their containers
docker network ls
for net in $(docker network ls -q); do
  echo "=== Network: $(docker network inspect $net --format '{{.Name}}') ==="
  docker network inspect $net --format '{{range .Containers}}{{.Name}} {{end}}'
done

# View active iptables rules
sudo iptables -L -n -v | head -50

# View policies in K8s
kubectl get networkpolicy -A
# Prometheus alert: containers mounting docker.sock
docker_container_mounts{type="bind",source="/var/run/docker.sock"} > 0

Network Isolation Best Practices #

MUST:
  ✓ Segment networks per layer (edge, app, data)
  ✓ Internal networks for services that don't need the internet
  ✓ User-defined networks, not the default bridge
  ✓ No port mapping for internal services
  ✓ Designated containers as audited gateways
  ✓ NetworkPolicy in Kubernetes for all pods
  ✓ Monitoring and alerts for topology changes

MUST NOT:
  ✗ Use host networking to "simplify"
  ✗ Put all services on one big network
  ✗ Mount /var/run/docker.sock for ordinary apps
  ✗ Use the default bridge for multi-container setups
  ✗ Hardcode IPs between containers
  ✗ Expose database ports
  ✗ Use the same network for multi-tenant setups

Summary #

  • Network isolation = containers can’t talk to each other by default, only when allowed. The default-deny, explicit-allow principle.
  • Network namespaces are the OS-level foundation. Each container has its own interfaces, routing, and iptables.
  • Five isolation layers: namespace (L1), driver (L2), topology (L3), port mapping (L4), iptables (L5). L1-L3 are safe by default. L4-L5 need explicit configuration.
  • Segmentation = separation per layer (edge/app/data) or per tenant. The edge is publicly exposed, app works internally, data is truly hidden.
  • internal: true = a network with no outbound gateway. Databases and caches must use this mode. Services can never reach the internet.
  • Layered security = containers on many networks at once, becoming auditable gateways/bridges. The jump-host pattern.
  • Anti-patterns: host networking for convenience, default bridge for multi-container setups, exposed databases, careless docker.sock mounts, one network for everything.
  • NetworkPolicy in Kubernetes is the formal declaration of default-deny + explicit-allow. Without a policy, every pod can talk to every pod.
  • Monitoring: audit docker network ls, iptables rules, and NetworkPolicies in K8s. Alert on topology changes.
  • The main principle: isolation by default, open by configuration. When in doubt, keep it isolated and open only what’s needed.
  • For multi-tenancy: one tenant = one internal network. Tenant A and Tenant B should not even know each other exists.

← Previous: Container-to-Container Communication   Next: User-defined Network →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact