Three Docker Networks, Fixed Subnets, One Reason: Firewall Rules That Don't Move

frontend, socket, and backend are declared with fixed CIDR blocks on vmi2633427 for a reason that has nothing to do with routing and everything to do with what a firewall rule can trust.

Three Docker Networks, Fixed Subnets, One Reason: Firewall Rules That Don't Move

Docker's default bridge network assigns subnets dynamically, and that subnet can change across restarts and Docker daemon upgrades. A firewall rule written against 172.17.0.0/16 today is not guaranteed to describe the same traffic tomorrow. vmi2633427 sidesteps that entirely: every network Traefik touches is declared explicitly, with a fixed subnet, in docker-compose.yml.

Three bridges, three fixed subnets, three different jobs
Three bridges, three fixed subnets, three different jobs

A subnet a firewall rule can reference and trust it will still mean the same thing next week.

The three networks

NetworkSubnetNotes
frontend172.18.2.0/24Traefik ↔ published services. The docker provider only discovers containers here.
socket172.18.1.0/24internal, no masquerade. Traefik ↔ dockerproxy only.
backend172.18.3.0/24internal, no masquerade. Service ↔ database traffic.

Each one has exactly one job. frontend is the only network Traefik's Docker provider watches for label discovery — a container that never joins frontend is invisible to Traefik no matter what labels it carries, which is itself a useful isolation property. socket exists purely so Traefik can reach tecnativa/docker-socket-proxy without exposing that proxy to anything else running on the host. backend is where application containers talk to their databases, deliberately kept off frontend so a compromised public-facing container doesn't automatically have a route to every datastore on the box.

internal: true is doing real work

Two of the three networks are marked internal, which tells Docker not to attach a default route out to the host's external interface — no masquerading, no NAT to the internet. Traffic on socket and backend simply cannot originate an outbound connection to the wider network the way frontend traffic can. That is a meaningful backstop even before any firewall rule is written: a container on backend that gets compromised and tries to exfiltrate data or phone home over the network it's actually attached to has no path out.

Why fixed subnets specifically matter

The practical payoff shows up the first time you write an iptables or nftables rule that references one of these ranges — for example, restricting inbound connections to the database port to 172.18.3.0/24 only, or logging anything from 172.18.1.0/24 that isn't dockerproxy. Docker networks recreated with dynamic subnets would silently invalidate that rule on the next docker compose down && up, and the failure mode is quiet: the rule keeps existing, it just stops matching the traffic you meant it to. Pinning the subnet in the compose file means docker network inspect backend returns the same CIDR block indefinitely, and any rule against it stays correct as containers are recreated, upgraded, or moved.

Segmentation as documentation

There's a secondary benefit that has nothing to do with firewalls: reading the network list in docker-compose.yml tells you the intended data-flow topology of the whole stack without reading a single line of application config. A network named backend marked internal immediately tells you "nothing here should be reachable from outside," and any container declared on it inherits that expectation. On the Nextcloud stack next door, this is exactly the concern that comes up when a third service joins backend — its README notes explicitly that backend is shared, so anything else on it can reach MariaDB directly, and that this is worth auditing with docker network inspect backend whenever a new service is added. Segmentation only stays meaningful if you keep asking who's on which network, not just how many networks there are.

Series: The Self-Hosting Stack. Next: adding a new service to this stack with a single label.