Skip to main content
RunBook Academy

Docker & ContainersX Β· Production ArchitectureHost design

Docker host design β€” single host, multi host, edge cases

Advanced⏱ ~30 mindockerlsblk

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

Not yet marked complete on this device.

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

WorkloadCPURAMDiskNetwork
Small (1–10 services)4 cores8 GB100 GB SSD1 GbE
Medium (10–50 services)16 cores64 GB500 GB NVMe10 GbE
Large (50–200 services)32+ cores128+ GB1+ TB NVMe10 GbE+
Very large (200+)Multiple hostsMultiple hostsNetwork storage10 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.

Configuration changedaemon.json
{
"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-opts caps 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-restore keeps containers running when dockerd stops, which turns a daemon upgrade into a control-plane blip instead of an outage.
  • default-address-pools stops Docker allocating bridge subnets that collide with your corporate network. The default pools include 172.17.0.0/16 upward, 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.

Read-only / Safestorage check
$ docker info --format 'Driver={{.Driver}} Backing={{index .DriverStatus 0 1}} d_type={{index .DriverStatus 1 1}}' && findmnt -no SOURCE,FSTYPE,SIZE,USE% /var/lib/docker
Driver=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.
  • noatime on the Docker filesystem removes a metadata write per file read. Free.
  • ext4 or XFS. Both are fine. btrfs and zfs have their own Docker storage drivers with different tradeoffs; if you are not deliberately choosing one, you want overlay2.
  • Network storage for volumes, not for /var/lib/docker itself. 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:443 that binds every interface including the management one.
  • The daemon is never on TCP. DOCKER_HOST=ssh://... for remote access; an open tcp:// daemon socket is unauthenticated root.
  • Reserve resources for the host. If the sum of container limits equals physical RAM, there is nothing left for sshd to 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.

FailureWhat happensWhat you need in place
Host rebootsOnly containers with a restart policy returnrestart: unless-stopped everywhere, and a boot-time check
Daemon restartsContainers die, unless live-restorelive-restore: true, and knowing its limits
A container leaks memoryIts own cgroup OOM kill, or a host-wide one if unlimitedMemory limits on every service
/var/lib/docker fillsEvery container fails writes at onceSeparate filesystem, log caps, alert at 80%
Registry unreachableRedeploys fail; running containers unaffectedPre-pulled images, a pull-through mirror
Public NIC saturatedHost unreachable if management shares itSeparate management interface
Disk failsEverythingVolumes 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.

  1. Give /var/lib/docker its own filesystem, sized from the next lesson.
  2. Verify docker info reports overlay2 with d_type=true before deploying anything.
  3. Mount that filesystem noatime.
  4. Set log-opts with max-size and max-file in daemon.json.
  5. Set live-restore: true, and know that it covers patch upgrades only.
  6. Set default-address-pools so bridge subnets cannot collide with your network.
  7. Bind SSH and exporters to the management address; publish container ports to an explicit address.
  8. Alert on /var/lib/docker at 80%, not at 95%.
  9. Write the recovery step for every row of the failure table, and test the restore.

Knowledge check

Knowledge check Β· 5 questions

  1. Q1. Why should `/var/lib/docker` be on its own filesystem?

  2. Q2. `docker system df` reports 12 GB in use but the filesystem is full at 400 GB. Where is the space?

  3. Q3. With `live-restore: true`, containers survive a major-version upgrade of the Docker daemon.

  4. Q4. Which statements about overlay2 layer accounting are true? Select all that apply.

  5. 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.