Docker & ContainersX Β· Production ArchitectureHost design
Docker host design β single host, multi host, edge cases
What you'll learn
- Lay out host storage so a full /var/lib/docker does not take the host with it
- Verify the backing filesystem actually supports the storage driver
- Separate the management plane from the data plane at the interface level
- Write down the recovery for each failure mode before it happens
Prerequisites
None β start here.
Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-12
A Docker host is a Linux machine running dockerd, containerd, runc
and some containers. Designing one well is mostly two decisions: where the
growing data goes, and how you keep reaching the host when the workload
misbehaves.
Hardware sizing, as a starting point
| Workload | CPU | RAM | Disk | Network |
|---|---|---|---|---|
| Small (1β10 services) | 4 cores | 8 GB | 100 GB SSD | 1 GbE |
| Medium (10β50 services) | 16 cores | 64 GB | 500 GB NVMe | 10 GbE |
| Large (50β200 services) | 32+ cores | 128+ GB | 1+ TB NVMe | 10 GbE+ |
| Very large (200+) | Multiple hosts | Multiple hosts | Network storage | 10 GbE+ |
Treat that as an opening bid, not an answer β the next lesson does the
arithmetic properly. Any x86-64 CPU from 2017 or later has the kernel
features Docker needs (cgroup v2, user namespaces, seccomp, and either
AppArmor or SELinux). Kernel version matters more than CPU generation:
overlay2 needs 4.0 or newer, and cgroup v2 as the sole hierarchy wants
5.8 or newer to be pleasant.
Storage layout is the decision that matters
Everything Docker accumulates lives under one directory:
/var/lib/docker. Images, container writable layers, volumes, build cache
and β critically β container logs, all in one place, all growing.
{
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" },
"live-restore": true,
"default-address-pools": [
{ "base": "172.20.0.0/14", "size": 24 }
]
}Three deliberate choices there:
log-optscaps every container at 30 MB unless it overrides them. This applies only to containers created after the change; existing ones keep the settings they were created with.live-restorekeeps containers running whendockerdstops, which turns a daemon upgrade into a control-plane blip instead of an outage.default-address-poolsstops Docker allocating bridge subnets that collide with your corporate network. The default pools include172.17.0.0/16upward, and the day a new Compose network takes the same range as your VPN, every container loses access to an internal service in a way that is very hard to attribute.
Verify the backing filesystem, do not assume it
overlay2 on XFS requires d_type support, which means the filesystem must
have been formatted with ftype=1. Older RHEL and CentOS installers
defaulted to ftype=0, and the failure mode is not a clean refusal β it is
subtle corruption of overlay behaviour that surfaces weeks later.
$ docker info --format 'Driver={{.Driver}} Backing={{index .DriverStatus 0 1}} d_type={{index .DriverStatus 1 1}}' && findmnt -no SOURCE,FSTYPE,SIZE,USE% /var/lib/dockerDriver=overlay2 Backing=xfs d_type=true
/dev/mapper/vg0-docker xfs 500G 38%Illustrative output
d_type=false means reformat, because there is no fix short of it. And
findmnt returning nothing for /var/lib/docker means it is on the root
filesystem β which is the condition the callout above describes.
Changing the storage driver later makes every existing image and container inaccessible. Get this right at build time; there is no in-place migration.
Filesystem choices
- NVMe over SATA SSD for anything doing builds. Layer extraction is metadata-heavy and small-file-heavy, which is where the gap is widest.
noatimeon the Docker filesystem removes a metadata write per file read. Free.- ext4 or XFS. Both are fine.
btrfsandzfshave their own Docker storage drivers with different tradeoffs; if you are not deliberately choosing one, you wantoverlay2. - Network storage for volumes, not for
/var/lib/dockeritself. NFS under the whole directory produces overlayfs behaviour nobody wants. Mount network storage at specific volume paths instead.
Separating the management plane
The point of separating planes is that when the data plane is saturated β by traffic, by a runaway container, by a disk that just filled β you can still reach the host to fix it.
- A dedicated management interface. SSH, monitoring exporters and any out-of-band access bind the management address only, so a flooded public interface does not lock you out.
- Published ports bind an explicit address.
-p 203.0.113.10:443:443, never the bare-p 443:443that binds every interface including the management one. - The daemon is never on TCP.
DOCKER_HOST=ssh://...for remote access; an opentcp://daemon socket is unauthenticated root. - Reserve resources for the host. If the sum of container limits equals
physical RAM, there is nothing left for
sshdto fork with. Leave headroom deliberately rather than discovering it is absent during an incident.
flowchart LR
Internet((Internet)) --> FW[Host firewall<br/>DOCKER-USER rules]
FW --> EXT[eth0 203.0.113.10]
EXT --> Proxy[Reverse proxy<br/>TLS terminates here]
Proxy --> AppNet[app-net bridge]
AppNet --> A[Service A]
AppNet --> B[Service B]
AppNet --> DataNet[data-net<br/>internal: true]
DataNet --> DB[(Database)]
VPN((VPN / jump host)) --> MGMT[eth1 192.0.2.10]
MGMT --> HostSvc[sshd, node_exporter,<br/>dockerd]
Failure planning
For each of these, the deliverable is not the analysis. It is a written recovery step somebody can follow at 03:00.
| Failure | What happens | What you need in place |
|---|---|---|
| Host reboots | Only containers with a restart policy return | restart: unless-stopped everywhere, and a boot-time check |
| Daemon restarts | Containers die, unless live-restore | live-restore: true, and knowing its limits |
| A container leaks memory | Its own cgroup OOM kill, or a host-wide one if unlimited | Memory limits on every service |
/var/lib/docker fills | Every container fails writes at once | Separate filesystem, log caps, alert at 80% |
| Registry unreachable | Redeploys fail; running containers unaffected | Pre-pulled images, a pull-through mirror |
| Public NIC saturated | Host unreachable if management shares it | Separate management interface |
| Disk fails | Everything | Volumes backed up somewhere else, restore tested |
The last row is the one that gets written down and never tested. A backup you have not restored from is a hypothesis.
- Give
/var/lib/dockerits own filesystem, sized from the next lesson. - Verify
docker inforeportsoverlay2withd_type=truebefore deploying anything. - Mount that filesystem
noatime. - Set
log-optswithmax-sizeandmax-fileindaemon.json. - Set
live-restore: true, and know that it covers patch upgrades only. - Set
default-address-poolsso bridge subnets cannot collide with your network. - Bind SSH and exporters to the management address; publish container ports to an explicit address.
- Alert on
/var/lib/dockerat 80%, not at 95%. - Write the recovery step for every row of the failure table, and test the restore.
Knowledge check
Knowledge check Β· 5 questions
Q1. Why should `/var/lib/docker` be on its own filesystem?
Q2. `docker system df` reports 12 GB in use but the filesystem is full at 400 GB. Where is the space?
Q3. With `live-restore: true`, containers survive a major-version upgrade of the Docker daemon.
Q4. Which statements about overlay2 layer accounting are true? Select all that apply.
Q5. The Docker filesystem is at 95% and the service is degraded. Which action is the safest first large reclaim?
Passing score: 75%. Answers are checked in this browser.