Docker Network MTU Tuning: Defaults, Diagnosis, and Production Configuration
25 min read
No hand-waving. Every MTU setting: what it does, the default if you omit it, what breaks when it's wrong, and exactly how to calculate the right value — with real numbers.
MTU problems are maddening because everything looks fine. Ping works. SSH connects. DNS resolves. Then HTTPS hangs on large pages, Docker pulls stall halfway through, and your monitoring shows nothing because small packets still get through.
This guide walks through Docker networking MTU from first principles to production. For every important setting you'll get four things:
- What it does — in plain language
- Default value — what Docker uses if you write nothing
- What goes wrong — if you leave the default or set it wrong
- How to calculate — step-by-step with real numbers
We'll use a concrete reference server throughout:
Ubuntu 22.04 host · Docker 27.x · WireGuard VPN to office (1420 MTU) · 3 bridge networks · Swarm overlay for one cluster · Nginx + API + Postgres stack
Adjust the numbers for your environment. The method stays the same.
How to read this guide (start here)
Every code block has a colored label. Do only what the label says.
- Read Diagram, formula, or explanation — do not paste into a terminal.
- Copy file Create this file on disk (path is in the label or first comment). Edit values for your environment.
- Run Paste into a shell on the machine this section describes.
- SQL Paste into a database client (
mariadb/mysql/ ProxySQL admin), not bash. - Sample output A realistic excerpt immediately after an observation command; compare fields and read the Meaning note before continuing.
Follow parts in order unless a part says you can skip it. Change example IPs, passwords, and paths to match your lab.
Suggested path: symptoms → diagnosis → apply daemon/bridge MTU → verify with ping/MSS checks.
1. What MTU is and why Docker makes it worse
What you should do: read the explanation, then do only blocks labeled COPY / RUN / SQL.
MTU (Maximum Transmission Unit) is the largest IP packet size a network interface can send without fragmentation. Standard Ethernet is 1500 bytes. Every hop along the path must support at least that size — or packets get fragmented or silently dropped.
Docker adds encapsulation overhead. A container on a bridge network doesn't talk directly on the host NIC. Traffic goes through a virtual bridge (docker0 or a custom bridge), often through iptables/NAT, and sometimes through an overlay tunnel (VXLAN adds 50 bytes). Each layer eats into your effective MTU budget.
Reference scenarios — know your path MTU before tuning Docker:
Standard Ethernet (office LAN, bare metal): 1500 MTU end-to-end. Docker bridge default 1500 usually works.
Cloud VPC (AWS, GCP, Azure, Hetzner): 1500 MTU on the host NIC. Same as Ethernet — but check if you're behind a VPN or transit gateway that lowers it.
WireGuard VPN (typical config): Interface MTU 1420. Path MTU to remote peers is ≤ 1420. Docker bridge at 1500 causes black-hole behavior for large packets.
Container sends 1500-byte packet → exceeds WireGuard tunnel → dropped silently
TCP MSS should be: 1420 − 40 = 1380
Docker bridge MTU should be: 1420 (match the narrowest hop)
2. Common symptoms — when MTU is your problem
What you should do: read this section unless a labeled block says otherwise.
These patterns scream MTU mismatch long before you find the root cause:
- SSH works, HTTPS hangs — SSH sends small packets; TLS handshakes and page loads send larger ones. Classic MTU black hole.
- Slow TLS / SSL connections — Certificate chains and ClientHello messages exceed the broken path. Connection eventually succeeds after TCP retries shrink the window — feels like "slow SSL."
- Intermittent timeouts — Small API calls succeed; file uploads,
docker pull, or database dumps fail or stall randomly. - Ping works without payload, fails with size —
ping 8.8.8.8succeeds (64 bytes).ping -s 1472 8.8.8.8fails. - Works on host network, broken in container — Host uses correct path MTU; container bridge still at 1500.
- Works locally, broken across VPN/site-to-site — Remote site has lower MTU; local Docker defaults assume 1500.
3. Diagnosis commands — find the real MTU
What you should do: read the explanation, then do only blocks labeled COPY / RUN / SQL.
Don't guess. Run these before changing anything.
3.1 Check host interface MTU
# All interfaces and their MTU ip link show # Specific interface (e.g. WireGuard) ip link show wg0 | grep mtu # Example output: # 3: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue
Sample output — ip link show eth0 (excerpt)
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
link/ether aa:bb:cc:dd:ee:ff brd ff:ff:ff:ff:ff:ff
Meaning: host Ethernet MTU is 1500. If this host reaches the internet only through WireGuard/VPN with MTU 1420, Docker’s default bridge at 1500 will cause black-hole TCP (SYN works, large transfers stall). Lower Docker network MTU to match the path (often 1420 or 1400), then recreate networks/containers.
3.2 Path MTU discovery with ping (don't fragment)
On the host, Linux ping supports the DF (Don't Fragment) bit:
# Linux host: ping with DF bit set (won't fragment) ping -M do -s 1472 8.8.8.8 # 1472 + 28 headers = 1500 total ping -M do -s 1372 8.8.8.8 # 1372 + 28 = 1400 # Increase -s until you get "Frag needed" or 100% loss # Last working size + 28 = your path MTU
Sample output — ping -M do PMTU boundary test
From 10.0.0.1 icmp_seq=1 Frag needed and DF set (mtu = 1420) 3 packets transmitted, 0 received, +1 errors, 100% packet loss 64 bytes from 8.8.8.8: icmp_seq=1 ttl=116 time=12.4 ms 3 packets transmitted, 3 received, 0% packet loss
Meaning: the 1472-byte payload (1500 total) exceeded a reported 1420 MTU, while 1372 (1400 total) passed. Increase from the working size to find the exact boundary; total PMTU is largest successful payload + 28. Total loss without “Frag needed” may instead be ICMP filtering.
From inside a container: Alpine's default busybox ping does not support -M do on all versions. Install iputils or use tracepath instead:
# Recommended: tracepath (works on Alpine without extra flags) docker run --rm alpine sh -c "apk add -q iputils && tracepath -n 8.8.8.8 | head -3" # Alternative: iputils ping with DF bit docker run --rm alpine sh -c "apk add -q iputils && ping -M do -s 1372 -c 3 8.8.8.8"
Sample output — container tracepath and DF ping (excerpt)
1?: [LOCALHOST] pmtu 1420 1: 172.17.0.1 0.091ms 3 packets transmitted, 3 received, 0% packet loss
Meaning: container PMTU is 1420 and the 1400-byte packet passes. A first-hop PMTU of 1500 when the host path is 1420 exposes the mismatch; package/DNS errors mean the diagnostic container never reached the network test.
Don't trust container ping alone. Always compare hosttracepathvs containertracepathto the same destination. If they differ, Docker network MTU is wrong.
3.3 tracepath — automatic PMTU discovery
# Shows hop-by-hop and estimated path MTU tracepath 8.8.8.8 # From container (install iputils if needed) docker run --rm alpine sh -c "apk add iputils && tracepath 8.8.8.8" # Example output: # 1: 172.18.0.1 0.1ms pmtu 1500 # 1: 172.18.0.1 0.2ms pmtu 1420 ← VPN lowered it
Sample output — tracepath 8.8.8.8 (excerpt)
1?: [LOCALHOST] pmtu 1500 1: 10.0.0.1 0.412ms 2: 10.200.0.1 7.803ms pmtu 1420 3: 8.8.8.8 12.194ms reached
Meaning: the local interface starts at 1500, hop 2 lowers PMTU to 1420, and the destination is reachable. The lowest reported PMTU—not the first line—is the budget. Missing PMTU changes can occur when routers suppress ICMP; confirm with DF ping.
3.4 Inspect Docker network MTU
# List networks
docker network ls
# Check MTU on a specific network
docker network inspect bridge --format '{{json .Options}}'
docker network inspect myapp_web --format '{{range $k,$v := .Options}}{{$k}}={{$v}} {{end}}'
# Full inspect (look for "com.docker.network.driver.mtu" in Options)
docker network inspect myapp_web | grep -i mtu
# Check docker0 bridge MTU on host
ip link show docker0 | grep mtu
Sample output — docker network inspect myapp_web (MTU line)
"Options": {
"com.docker.network.driver.mtu": "1420"
}
Meaning: this user-defined network was created with MTU 1420 (matches a typical WireGuard path). Empty Options or missing the mtu key usually means Docker used the daemon default (often 1500). After changing daemon.json or Compose driver_opts, recreate the network — existing networks keep their old MTU.
3.5 Compare host vs container
# Host path MTU tracepath -n 1.1.1.1 | head -1 # Container path MTU (same destination) docker run --rm --network myapp_web alpine sh -c \ "apk add -q iputils && tracepath -n 1.1.1.1 | head -1" # If host shows pmtu 1420 but container shows 1500 → fix Docker network MTU
Sample output — host versus container tracepath
host: 1?: [LOCALHOST] pmtu 1420 container: 1?: [LOCALHOST] pmtu 1500
Meaning: the host knows the 1420 path but the container network advertises 1500, so TCP can negotiate oversized segments. If both show 1420, investigate routing, firewall ICMP, or application timeouts instead of lowering MTU again.
4. Docker bridge network MTU (driver_opts)
What you should do: read the explanation, then do only blocks labeled COPY / RUN / SQL.
Bridge networks are the default for docker compose stacks. Each user-defined bridge can have its own MTU via driver_opts.
networks.*.driver_opts.mtu (docker-compose)
What it does: Sets the MTU on the Linux bridge interface Docker creates for this network (e.g. br-abc123). All containers attached to this network inherit this MTU on their eth0.
Default if omitted: Docker copies the host default-route interface MTU (usually 1500). On some hosts this matches reality; on VPN/PPPoE hosts it does not.
If you leave default: On a host behind WireGuard (1420) or PPPoE (1492), containers send 1500-byte packets that exceed the real path. Large TCP transfers hang; small requests work fine.
How to calculate — step by step:
- Find path MTU from host:
tracepath 8.8.8.8→ e.g. 1420 - Subtract overlay overhead if using Swarm overlay on same path: VXLAN = 50 bytes
- For plain bridge (no overlay): Docker MTU = path MTU → 1420
- For overlay network: overlay MTU = path MTU − 50 → 1420 − 50 = 1370
Same host, Swarm overlay → mtu: 1370
Standard cloud VPS, path MTU 1500 → omit driver_opts (default is fine)
4.1 Complete docker-compose.yml — WireGuard host example
docker-compose.yml (edit MTU/IPs for your environment).services:
nginx:
image: nginx:1.27-alpine
container_name: nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
networks:
- web
- backend
depends_on:
- api
api:
image: myapp/api:latest
container_name: api
restart: unless-stopped
networks:
- backend
- db
postgres:
image: postgres:16-alpine
container_name: postgres
restart: unless-stopped
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- db
networks:
web:
driver: bridge
driver_opts:
com.docker.network.driver.mtu: "1420"
backend:
driver: bridge
driver_opts:
com.docker.network.driver.mtu: "1420"
db:
driver: bridge
internal: true
driver_opts:
com.docker.network.driver.mtu: "1420"
volumes:
pgdata:
4.2 docker-compose.yml — standard cloud (1500, explicit for documentation)
docker-compose.yml (edit MTU/IPs for your environment).networks:
web:
driver: bridge
driver_opts:
com.docker.network.driver.mtu: "1500"
backend:
driver: bridge
driver_opts:
com.docker.network.driver.mtu: "1500"
4.3 docker-compose.yml — Swarm overlay
driver: overlay in Compose requires Swarm mode (docker swarm init). Plain docker compose up on a single host cannot create overlay networks — use bridge + driver_opts instead.docker-compose.yml (edit MTU/IPs for your environment).networks:
web:
driver: overlay
driver_opts:
com.docker.network.driver.mtu: "1370"
attachable: true
backend:
driver: overlay
driver_opts:
com.docker.network.driver.mtu: "1370"
internal: true
Important: Changing MTU in compose only applies when the network is created. Existing networks keep their old MTU. You must recreate:
docker compose down docker network rm myapp_web myapp_backend # if not removed by down docker compose up -d
These lifecycle mutations are normally quiet apart from Compose progress; success means exit code 0 and no stderr. “Network has active endpoints” means a container still uses it, while “not found” is harmless only when it was already removed.
5. Global default MTU — /etc/docker/daemon.json
What you should do: read the explanation, then do only blocks labeled COPY / RUN / SQL.
mtu (daemon.json)
What it does: Sets MTU on docker0 and on the built-in bridge network. For new user-defined networks created without an explicit MTU option, Docker may use this value as the default — but do not rely on it for Compose stacks.
Default if omitted: 1500
What it does not do: It does not retroactively change existing networks. It does not replace per-network driver_opts in Compose. If your stack defines networks: web: driver: bridge without driver_opts.mtu, behavior depends on Engine version — safest practice is to set MTU explicitly on every Compose network.
When daemon.json mtu is enough: Ad-hoc docker run on default bridge, quick tests, and fixing docker0 itself.
When you still need driver_opts: Every production Compose network — especially multi-network stacks, internal DB networks, and overlay/Swarm.
/etc/docker/daemon.json (edit passwords/IPs first).# /etc/docker/daemon.json
{
"mtu": 1420,
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
# Apply changes
sudo systemctl restart docker
# Verify docker0 MTU after restart
ip link show docker0 | grep mtu
# Expected: mtu 1420
# Verify default bridge network
docker network inspect bridge --format '{{index .Options "com.docker.network.driver.mtu"}}'
# Expected: 1420
Sample output — Docker restart MTU verification
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1420 qdisc noqueue state DOWN 1420
Meaning: Linux docker0 and Docker’s bridge option both report 1420; state DOWN is normal with no attached container. If either remains 1500, daemon JSON was not loaded or an existing network retained its old option.
Caution: Restarting Docker stops all running containers unless you use live-restore. Plan a maintenance window.
6. TCP MSS — the formula that actually fixes TLS slowness
What you should do: read the explanation, then do only blocks labeled COPY / RUN / SQL.
MSS (Maximum Segment Size)
What it does: Limits the largest TCP payload in a single segment. When MSS × segments would exceed path MTU, packets fragment or drop. Correct MSS prevents black holes.
Default if omitted: Derived from interface MTU at connection time — usually 1460 for 1500 MTU (1500 − 40)
If you leave default: Container thinks MSS is 1460 (1500 − 40) but path only supports 1420. First large segment gets dropped. TCP backs off, retries with smaller windows — looks like slow TLS or random timeouts.
How to calculate:
MSS = 1420 − 40 = 1380
Docker overlay MTU 1370, IPv4:
MSS = 1370 − 40 = 1330
Standard 1500 Ethernet, IPv4:
MSS = 1500 − 40 = 1460 (default, no tuning needed)
Linux usually clamps MSS automatically via route MTU (tcp_mtu_probing). But when Docker bridge MTU is wrong, the kernel's MSS clamping is also wrong. Fix the interface MTU first — MSS follows.
sudo sysctl -w net.ipv4.tcp_mtu_probing=1
Sample output — sysctl -w net.ipv4.tcp_mtu_probing=1
net.ipv4.tcp_mtu_probing = 1
Meaning: TCP black-hole probing is enabled for this boot. Permission denied means sudo/capability failure; “cannot stat” means no kernel key. This is a safety net, not a substitute for correct network MTU.
net.ipv4.tcp_mtu_probing = 1
7. Overlay / Swarm MTU considerations
What you should do: read the explanation, then do only blocks labeled COPY / RUN / SQL.
Swarm overlay networks encapsulate container traffic in VXLAN (UDP port 4789). VXLAN adds 50 bytes of overhead per packet.
overlay network MTU
What it does: Sets MTU on the overlay's virtual interface inside each container. Must account for VXLAN wrapper when the packet hits the physical NIC.
Default if omitted: 1500 (same problem — ignores VXLAN overhead)
If you leave default: Effective packet size on wire = 1500 + 50 = 1550 bytes. Exceeds 1500 path → dropped. Multi-node Swarm clusters over VPN are especially vulnerable.
How to calculate:
- Path MTU on host NIC: 1420 (WireGuard example)
- VXLAN overhead: 50
- Overlay MTU = 1420 − 50 = 1370
- Overlay MSS (IPv4) = 1370 − 40 = 1330
Swarm checklist: Set the same overlay MTU on every node in the cluster. Mixed MTU across nodes causes asymmetric routing failures that are hard to debug. After changing, recreate overlay networks: docker network rm then redeploy stack.
# Create overlay manually with correct MTU docker network create \ --driver overlay \ --opt com.docker.network.driver.mtu=1370 \ --attachable \ myoverlay
Successful creation prints a network ID and exits 0. “Not a swarm manager” means overlay creation cannot proceed here; “already exists” means inspect/recreate it because MTU options do not change in place.
8. macvlan and host network modes
What you should do: read the explanation, then do only blocks labeled COPY / RUN / SQL.
network_mode: host
What it does: Container shares the host's network stack directly — no bridge, no NAT. Container uses host NIC MTU as-is.
Default if omitted: N/A — you explicitly choose host mode
If you use host mode: No Docker bridge MTU to tune. Fix MTU on the host interface (wg0, eth0) instead. Container inherits it automatically.
How to calculate: Set host interface MTU = path MTU. No docker-compose network block needed.
ip link set wg0 mtu 1420Container with
network_mode: host automatically gets 1420 MTU
macvlan / ipvlan
What it does: Gives containers a MAC address on the physical LAN — they appear as separate devices on your network.
Default if omitted: Inherits parent interface MTU (usually 1500)
If you leave default: Same black-hole issues if parent is on VPN with lower MTU. macvlan sub-interface doesn't auto-adjust.
How to calculate: macvlan MTU = parent interface MTU = path MTU
docker-compose.yml (edit MTU/IPs for your environment).networks:
macvlan_net:
driver: macvlan
driver_opts:
parent: eth0
com.docker.network.driver.mtu: "1420"
ipam:
config:
- subnet: 192.168.1.0/24
gateway: 192.168.1.1
9. Worked examples — full calculations
What you should do: COPY files; edit for your host; RUN apply/restart as labeled.
Example A: WireGuard VPN host, bridge compose stack
tracepath shows pmtu 1420 · Bridge networks only · No SwarmStep 1 — Path MTU: 1420 (WireGuard tunnel)
Step 2 — Bridge MTU: 1420 (match path, no overlay)
Step 3 — MSS (IPv4): 1420 − 40 = 1380
Step 4 — daemon.json:
"mtu": 1420Step 5 — compose driver_opts:
com.docker.network.driver.mtu: "1420" on every networkVerify:
docker run --rm --network myapp_web alpine ip link show eth0 | grep mtu → mtu 1420ping -M do -s 1392 8.8.8.8 from container → 1392 + 28 = 1420, should succeed
Example B: Cloud VPS (1500), no VPN
tracepath shows pmtu 1500 · Standard bridge stackBridge MTU: 1500 (default — no change needed)
MSS: 1500 − 40 = 1460
Action: Nothing for plain bridge networks.
If you run Swarm overlay on this host: path MTU 1500 − VXLAN 50 = overlay MTU 1450 (not 1500). AWS ECS and many cloud Swarm/K8s CNI setups use 1450 for this reason.
Only tune bridge MTU if you later add WireGuard or site-to-site VPN — then recalculate.
Example C: Swarm cluster over WireGuard
Step 1 — Path MTU: 1420
Step 2 — VXLAN overhead: 50 bytes
Step 3 — Overlay MTU: 1420 − 50 = 1370
Step 4 — Overlay MSS: 1370 − 40 = 1330
Step 5 — daemon.json on every node:
"mtu": 1420 (for bridge/default)Step 6 — Stack overlay networks:
com.docker.network.driver.mtu: "1370"Verify from container on overlay:
tracepath 8.8.8.8 → first hop pmtu should show 1370
Example D: PPPoE home connection (1492)
Path MTU: 1492
Bridge MTU: 1492
MSS: 1492 − 40 = 1452
daemon.json:
"mtu": 1492
10. Master defaults table — quick lookup
What you should do: read the explanation, then do only blocks labeled COPY / RUN / SQL.
Setting │ Default (if omitted) │ WireGuard example │ Cloud example ─────────────────────────────────────┼──────────────────────┼───────────────────┼──────────────── daemon.json mtu │ 1500 │ 1420 │ 1500 (omit) bridge driver_opts mtu │ 1500 │ 1420 │ 1500 (omit) overlay driver_opts mtu │ 1500 (wrong for VXLAN)│ 1370 │ 1450 (1500−50) docker0 bridge MTU │ 1500 │ 1420 │ 1500 container eth0 MTU (bridge) │ inherits network │ 1420 │ 1500 IPv4 TCP MSS │ MTU − 40 │ 1380 │ 1460 VXLAN overhead (overlay) │ not accounted for │ subtract 50 │ subtract 50 host network_mode │ uses host NIC MTU │ 1420 │ 1500
11. Troubleshooting — symptom → cause → fix
What you should do: read the explanation, then do only blocks labeled COPY / RUN / SQL.
Symptom │ Likely cause │ Fix ─────────────────────────────────────┼──────────────────────────────────────┼────────────────────────────── SSH works, HTTPS hangs │ MTU black hole on large packets │ Lower Docker network MTU to path MTU Slow TLS handshake │ TCP retries on oversized segments │ Fix bridge MTU; enable tcp_mtu_probing Small API calls OK, uploads fail │ Payload exceeds path MTU │ tracepath from container; set driver_opts ping works, ping -s 1472 fails │ Path MTU < 1500 │ Match Docker MTU to tracepath result Works on host, broken in container │ Bridge MTU 1500, host path lower │ driver_opts mtu + recreate network Swarm service unreachable cross-node │ Overlay MTU ignores VXLAN overhead │ overlay MTU = path MTU − 50 Fixed MTU but still broken │ Old network not recreated │ docker compose down; docker network rm; up docker pull stalls mid-download │ Registry traffic hits MTU limit │ Fix daemon.json mtu; restart docker Intermittent DB replication lag │ Large WAL packets dropped │ Set db network MTU; verify with tracepath Changes to daemon.json no effect │ Didn't restart Docker │ systemctl restart docker; verify docker0
12. Practical walkthrough — fix MTU on a live host
What you should do: follow COPY → RUN → SQL labels in order.
Use this sequence when symptoms match Section 2. Do not change MTU blindly.
/etc/docker/daemon.json, schedule a maintenance window (systemctl restart docker recreates connections), document current network MTUs with docker network inspect, and plan compose down — connected containers will restart.Step 1 — Confirm path MTU on the host (before Docker)
tracepath -n 8.8.8.8 | head -3 ip link show | grep -E '^[0-9]+:|mtu'
Sample output — live-host path and interface MTU survey
1?: [LOCALHOST] pmtu 1420 1: 10.200.0.1 7.8ms 2: 8.8.8.8 12.2ms reached 2: eth0: <UP,LOWER_UP> mtu 1500 7: wg0: <POINTOPOINT,UP,LOWER_UP> mtu 1420
Meaning: the route is 1420 even though physical Ethernet is 1500; WireGuard is the bottleneck. No route/reached line indicates routing trouble. Choose the lowest MTU on the actual path, not every interface shown.
Write down the lowest pmtu reported. Example: WireGuard → 1420. Plain cloud VPS → 1500.
Step 2 — Check what Docker networks actually use
docker network ls
docker network inspect myapp_web --format '{{index .Options "com.docker.network.driver.mtu"}}'
docker run --rm --network myapp_web alpine ip link show eth0 | grep mtu
Sample output — current Docker network and container MTU
NETWORK ID NAME DRIVER SCOPE 7d81c58a1d31 myapp_web bridge local 1500 2: eth0@if44: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
Meaning: network and container are 1500, mismatching the 1420 path. Blank format output can mean inherited options; trust container eth0. Missing-network errors mean use the deployed Compose project name.
If container eth0 shows 1500 but host path is 1420 → you found the bug.
Step 3 — Set daemon.json (docker0 + safety net)
tee: overwriting /etc/docker/daemon.json wipes existing keys (log-driver, insecure-registries, etc.). Back up and merge.sudo cp /etc/docker/daemon.json /etc/docker/daemon.json.bak.$(date +%F) 2>/dev/null || true
# If file missing, start with {}
echo '{}' | sudo tee /etc/docker/daemon.json >/dev/null
sudo jq '. + {"mtu": 1420}' /etc/docker/daemon.json | sudo tee /etc/docker/daemon.json.new
sudo python3 -m json.tool /etc/docker/daemon.json.new >/dev/null
sudo mv /etc/docker/daemon.json.new /etc/docker/daemon.json
sudo systemctl restart docker
ip link show docker0 | grep mtu
Sample output — daemon JSON validation and docker0 after restart
{
"mtu": 1420
}
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1420 qdisc noqueue state DOWN
Meaning: validation retained numeric MTU and Docker recreated its bridge at 1420. A parse error must stop the move/restart; service failure means inspect Docker journal and restore the backup.
Step 4 — Set Compose driver_opts (the real fix for stacks)
Add to every network in your compose file:
docker-compose.yml (edit MTU/IPs for your environment).networks:
web:
driver: bridge
driver_opts:
com.docker.network.driver.mtu: "1420"
Step 5 — Recreate networks (required)
docker compose down docker network rm myapp_web myapp_backend 2>/dev/null || true docker compose up -d
MTU is applied at network creation. Editing compose without recreating the network does nothing.
Step 6 — Verify from inside the app container
docker exec api ip link show eth0 | grep mtu docker exec api sh -c "apk add -q iputils && tracepath -n 1.1.1.1 | head -2" # Payload test: (path_mtu - 28) for ping headers # 1420 path → ping -s 1392 docker exec api sh -c "apk add -q iputils && ping -M do -s 1392 -c 3 8.8.8.8"
Sample output — in-container MTU, tracepath, and maximum DF ping
2: eth0@if51: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1420 1?: [LOCALHOST] pmtu 1420 3 packets transmitted, 3 received, 0% packet loss
Meaning: container interface and discovered path agree at 1420, and a 1392-byte payload plus 28-byte headers passes unfragmented. A 1500 interface means recreation failed; “Frag needed” at 1392 means the true path is below 1420.
Step 7 — Test real workloads, not just ping
curl -v https://example.com # TLS handshake uses larger packets docker pull nginx:alpine # registry download # Your actual slow endpoint / file upload
Sample output — HTTPS and image-pull workload verification (excerpt)
< HTTP/2 200 < content-type: text/html Status: Downloaded newer image for nginx:alpine
Meaning: TLS completed with a real response and a multi-layer registry transfer finished, exercising packets larger than basic ping. TLS stalls, resets, or pull retries after PMTU checks pass point to proxy/DNS/registry issues or a different route used by that workload.
13. Production checklist
What you should do: read the explanation, then do only blocks labeled COPY / RUN / SQL.
- Measure path MTU on the host —
tracepath 8.8.8.8orping -M do -ssweep before touching Docker config. - Document your scenario — Standard Ethernet (1500), cloud (1500), WireGuard (1420), PPPoE (1492). Write it down in your runbook.
- Set daemon.json mtu — Fixes
docker0and ad-hoc containers. Not a substitute for Composedriver_opts. - Set driver_opts on every compose network — Explicit MTU per network. For overlay: path MTU − 50 (cloud 1500 → overlay 1450).
- Recreate networks after changes — MTU is set at network creation time, not live-updated.
- Verify from inside a container —
ip link show eth0,tracepath,ping -M do -swith calculated payload size. - Enable tcp_mtu_probing — Safety net on hosts with variable paths (mobile VPN, multi-WAN).
- Same MTU on all Swarm nodes — Mixed configs cause asymmetric failures.
- Test large transfers —
docker pull, HTTPS page load, file upload — not just ping. - Monitor after VPN changes — Adding WireGuard or changing tunnel config changes path MTU. Recalculate.
14. Wrapping up
What you should do: read the explanation, then do only blocks labeled COPY / RUN / SQL.
MTU issues hide behind "the network seems fine." The defaults assume 1500-byte Ethernet everywhere — which breaks the moment you add a VPN, PPPoE, or VXLAN overlay.
The fix is mechanical: find your path MTU on the host, subtract overlay overhead if any, set that number in driver_opts on every Compose network (and in daemon.json for docker0), recreate networks, verify from inside a container. MSS = MTU − 40 for IPv4 TCP. That's the whole formula.
Start with tracepath on the host. If it shows anything below 1500, your Docker networks need to match — or you'll keep chasing ghosts in application logs.