skip to content

You are designing the Docker network layout for a multi-tier application on a single host: a public-facing reverse proxy, an internal API, and a database. How do you arrange networks so the database is reachable only by the API, what does attaching one container to two networks buy you, and how does Docker's --internal network option fit in?

level: principalimportance: should knowfreq 36%

answer

  1. one network per trust boundary
  2. API multi-homed = only path between tiers
  3. publish only at the edge; loopback-bind for admin access
  4. --internal = no external route (blocks exfiltration, breaks installs)
  5. no intra-network policy, host-scoped, Compose default flattens it

basics

~20 s

Use one network per trust boundary: edge (proxy + API) and data (API + database). The API is multi-homed on both; the proxy never joins data, so it cannot reach the database at all. Publish ports only on the proxy. Mark the data network --internal to remove its external route. Networks, not published ports, are the boundary.

solid answer

~60 s

I model each trust boundary as its own network. `edge` holds the reverse proxy and the API; `data` holds the API and the database. Only the API is multi-homed, so it is the sole path between tiers — the proxy has no interface on `data` and Docker's cross-bridge isolation rules will not forward its packets there, so a compromised proxy cannot even reach the database port. Port publishing is minimal: only the proxy publishes 80/443. The API and database publish nothing; they are reached over their networks by name. Anything published with `-p` is exposed to the host and, by default, to anything that can route to the host. I mark `data` with `--internal`, which strips its external route so containers there cannot initiate outbound traffic — useful against exfiltration and against a database quietly pulling from the internet. The tradeoffs: Docker gives no intra-network policy, so anything on a network reaches everything on it on every port; the granularity is the network, and more networks means more operational surface. I would not push this beyond a few tiers on one host — real policy needs an orchestrator's network policies or a service mesh.

code

yaml · 16 lines
yaml
networks:
  edge:
  data:
    internal: true

services:
  proxy:
    image: nginx:1.27
    ports: ["443:443"]
    networks: [edge]
  api:
    image: myapi:1.0
    networks: [edge, data]
  db:
    image: postgres:16
    networks: [data]

go deeper

for a junior

Know that separate networks isolate containers and that the database should not be published to the host.

for a middle

Draw the two-network layout with a multi-homed API and explain that isolation is enforced by Docker's iptables rules, not by convention.

for a senior

Add --internal for egress containment, loopback-only publishing for admin access, and the Compose default-network trap; state the operational costs.

for a principal

Frame it as trust boundaries versus operational surface, name the model's limits (no intra-network policy, no identity, host-scoped), and say when to move to orchestrator network policy or a mesh.

## The unit of policy is the network Docker has no per-port ACL between containers. Within one bridge network, every member can reach every other member on every listening port. That single fact drives the whole design: if you want A unable to reach B, they must not share a network. So the topology question becomes "what are my trust boundaries?" and each answer becomes a network. For a three-tier app that is typically two networks: - `edge`: reverse proxy + API - `data`: API + database The API is attached to both — multi-homed — and holds one interface and IP per network. It becomes the only bridge between tiers, at the application layer, where you can authenticate and validate. The proxy is not on `data`, and Docker's isolation chains drop forwarded traffic between bridges, so even with a stolen database password and a guessed IP the proxy cannot open a socket to it. That is a genuinely stronger property than "the database has a password". ## Publishing is the other exposure surface Every `-p` mapping punches a hole from the host into a container, and on Linux those NAT rules are evaluated before host INPUT firewall rules, so a published port is often reachable from outside even when a host firewall appears to deny it. The design rule follows: publish only at the edge — the proxy's 80/443 — and never publish the API or database merely for convenience. If a human needs database access, publish it to loopback only (`-p 127.0.0.1:5432:5432`) or tunnel in over SSH. ## The --internal option `docker network create --internal data` creates a network with no external routing: containers on it cannot reach outside the host, and outside traffic cannot reach them. Combined with a multi-homed API (which keeps its `edge` interface for outbound needs), this gives the database no egress path at all. It is a meaningful containment control — it blocks the exfiltration step and blocks a compromised data-tier process from pulling a second-stage payload. Watch the practical consequences: package installs, license checks, NTP or an SMTP notifier from that container will fail, and image pulls are unaffected because they happen on the host, not in the network. Naming the constraint explicitly in the compose file saves an afternoon of debugging later. ## Aliases and multi-homing details A multi-homed container can carry different network-scoped aliases per network, which lets the same service present a different name to each tier. Its default route comes from one attachment, so egress behaviour depends on which network provides it — worth verifying rather than assuming when one attachment is `--internal`. ## Where the model runs out Be honest about the limits: 1. **No intra-network policy.** Two containers that must share a network can always reach each other on any port. Splitting further means more networks, and the number grows with the distinct relationships. 2. **No identity.** Policy is by attachment, not by workload identity; anything that can join the network is trusted equally, and anything that can talk to the Docker socket can join it. 3. **Single host.** Bridge networks are host-scoped. Multi-host needs overlay networking, and at that point you are choosing an orchestrator. 4. **Compose defaults fight you.** A Compose project attaches every service to one shared network by default, which quietly flattens the design unless networks are declared per service. That default is the most common way a carefully drawn topology is lost. The strategic read: this layout is worth doing on a single host because it is nearly free and removes whole classes of lateral movement, but it is a floor, not a security architecture. When requirements grow to per-port or per-identity policy, mutual TLS, or multiple hosts, the answer is an orchestrator with network policies rather than more Docker networks.

  • Why not simply rely on the database password instead of network separation?
    A password is checked by the database after a connection is accepted, so anything that can route to the port can attack authentication, exploit protocol bugs, or brute-force credentials. Network separation removes reachability entirely, so the proxy cannot open the socket at all. They are complementary layers, but reachability is the cheaper and stronger control.
  • What breaks when you mark a network --internal?
    Containers attached only to that network lose all outbound connectivity: package installs, external API calls, license checks, NTP and outbound mail fail. Image pulls still work because they happen on the host. Multi-homed containers keep egress through their other attachment, which is often the intended asymmetry.

Networks are rooms, not door locks: you control who is in the room, but everyone in a room can talk to everyone else.

saying these in an interview costs you the question

  • Publishing the database port so the API can reach it
  • Assuming a host firewall protects published container ports
  • Believing Docker can restrict which ports one container may use on a shared network
  • Letting Compose's single default network flatten every tier together
  • Presenting Docker networks as sufficient policy for a multi-host production system

context