Docker 97 🐳 High-Performance Docker Host Tuning: Kernel Sysctl Parameters, File Descriptors, and Ephemeral Ports
High-throughput Docker workloads fail at the host layer before they reach the application layer. The default Linux kernel is configured for balanced desktop and general-purpose server use, not for tens of thousands of concurrent connections. The three parameters that most often become the bottleneck are the file descriptor limit, the ephemeral port range, and the TCP connection backlog. Each has a default that is adequate for a single-user workstation and inadequate for a container host running a high-concurrency service.
Key point: The file descriptor limit must be raised in two places: the system-wide limit (fs.file-max) and the per-process limit (LimitNOFILE in the Docker systemd unit). Raising only the system-wide limit does not change the limit that the Docker daemon and its containers inherit.
Why the defaults fail under load
The file descriptor ceiling. Every open socket, every file handle, and every pipe consumes a file descriptor. The default per-process limit on most distributions is 1024. A container running a web server or a message broker with thousands of concurrent connections reaches that limit and fails with “too many open files” . The error is not a bug in the application; it is the kernel refusing to allocate more descriptors.
The ephemeral port exhaustion. When a container opens outbound connections without keep-alive, each closed connection enters the TIME_WAIT state for 60 seconds. The default ephemeral port range is 32768 60999, which provides approximately 28,000 ports. At 1,000 new connections per second, the range is exhausted in under 30 seconds, and new connections fail with “address already in use” . The fix is to widen the range and enable tcp_tw_reuse, which allows a new connection to reuse a TIME_WAIT socket when the timestamp is newer.
The connection backlog. The net.core.somaxconn parameter controls the maximum length of the queue of pending connections for a listening socket. The default is 128 on many systems, which means a burst of incoming connections beyond that number is dropped before the application accepts them. For a container host serving high request rates, the backlog must be raised to match the expected burst size .
a. File Descriptor Limits
The file descriptor limit has three layers: the system-wide limit, the per-user limit, and the per-process limit for the Docker daemon. All three must be raised for containers to inherit the higher limit.
The system-wide limit is set in sysctl:
# /etc/sysctl.d/99-docker-tuning.conf
fs.file-max = 2097152
fs.nr_open = 2097152
The fs.nr_open parameter sets the maximum number of file descriptors a single process can open. It must be at least as high as the per-process limit you intend to set .
The per-process limit for the Docker daemon is set in the systemd unit file. The location depends on the distribution: /lib/systemd/system/docker.service on Ubuntu, /usr/lib/systemd/system/docker.service on CentOS and RHEL .
# Add or modify the LimitNOFILE line in the [Service] section
LimitNOFILE=1048576
LimitNPROC=1048576
# Reload and restart
sudo systemctl daemon-reload
sudo systemctl restart docker
After the restart, verify that the Docker daemon’s process limit reflects the new value:
cat /proc/$(pidof dockerd)/limits | grep files
# Max open files 1048576 1048576 files
Containers inherit the daemon’s limit unless overridden. To set a different limit for a specific container, use the --ulimit flag:
docker run -d --ulimit nofile=524288:524288 myapp
b. Ephemeral Ports and TIME_WAIT
The ephemeral port range determines how many outbound connections a host can initiate. The default range is too narrow for a container host that makes many short-lived outbound connections.
# /etc/sysctl.d/99-docker-tuning.conf
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
The range 1024 65535 provides approximately 64,500 ports, more than double the default. The tcp_tw_reuse parameter allows a socket in TIME_WAIT to be reused for a new outbound connection, provided that the timestamp is newer than the previous connection’s timestamp . This is safe for outbound connections and is the recommended alternative to the deprecated tcp_tw_recycle, which was removed in Linux 4.12 because it dropped valid packets from clients behind NAT .
The tcp_fin_timeout parameter reduces the time a socket remains in FIN-WAIT-2, which shortens the overall connection teardown time.
The conntrack table is a separate limit that matters for high-connection workloads. The default nf_conntrack_max is often 65,536, which is insufficient for a host running thousands of containers or connections.
# /etc/sysctl.d/99-docker-tuning.conf
net.netfilter.nf_conntrack_max = 262144
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 60
c. TCP Backlog and Buffer Tuning
The somaxconn parameter controls the listen backlog. The netdev_max_backlog controls the queue of packets waiting to be processed by the kernel. Both need to be raised for high-throughput workloads.
# /etc/sysctl.d/99-docker-tuning.conf
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535
The TCP buffer sizes determine how much data can be in flight for a single connection. The defaults are adequate for low-latency networks but become a bottleneck on high-bandwidth links. The tcp_rmem and tcp_wmem parameters take three values: minimum, default, and maximum.
# /etc/sysctl.d/99-docker-tuning.conf
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 87380 16777216
The tcp_fastopen = 3 parameter enables TCP Fast Open for both client and server connections, which reduces the latency of the initial handshake by allowing data to be sent in the SYN packet .
Complete Example Session
# ============================================
# PART 1: CREATE THE SYSCTL CONFIGURATION
# ============================================
sudo tee /etc/sysctl.d/99-docker-tuning.conf << 'EOF'
# File descriptors
fs.file-max = 2097152
fs.nr_open = 2097152
# Ephemeral ports and TIME_WAIT
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_max_tw_buckets = 262144
# Connection backlog
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535
# TCP buffers
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 87380 16777216
# TCP Fast Open
net.ipv4.tcp_fastopen = 3
# Conntrack
net.netfilter.nf_conntrack_max = 262144
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 60
EOF
# ============================================
# PART 2: APPLY THE SYSCTL SETTINGS
# ============================================
sudo sysctl --system
# ============================================
# PART 3: RAISE THE DOCKER DAEMON LIMIT
# ============================================
# Ubuntu/Debian
sudo sed -i 's/^LimitNOFILE=.*/LimitNOFILE=1048576/' /lib/systemd/system/docker.service
sudo sed -i 's/^LimitNPROC=.*/LimitNPROC=1048576/' /lib/systemd/system/docker.service
# CentOS/RHEL
# sudo sed -i 's/^LimitNOFILE=.*/LimitNOFILE=1048576/' /usr/lib/systemd/system/docker.service
sudo systemctl daemon-reload
sudo systemctl restart docker
# ============================================
# PART 4: VERIFY THE DAEMON LIMIT
# ============================================
cat /proc/$(pidof dockerd)/limits | grep -E "files|processes"
# ============================================
# PART 5: VERIFY THE KERNEL PARAMETERS
# ============================================
sysctl net.ipv4.ip_local_port_range net.ipv4.tcp_tw_reuse net.core.somaxconn
# ============================================
# PART 6: RUN A CONTAINER WITH EXPLICIT LIMITS
# ============================================
docker run -d \
--ulimit nofile=524288:524288 \
--sysctl net.ipv4.ip_local_port_range="1024 65535" \
--sysctl net.ipv4.tcp_tw_reuse=1 \
--name tuned-app \
myapp:latest
# ============================================
# PART 7: VERIFY THE CONTAINER LIMIT
# ============================================
docker exec tuned-app cat /proc/1/limits | grep files
# ============================================
# PART 8: CHECK FOR PORT EXHAUSTION
# ============================================
ss -s | grep -i time-wait
# If TIME_WAIT is high, tcp_tw_reuse is working
# ============================================
# PART 9: MONITOR CONNTRACK
# ============================================
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max
The ten parts trace the tuning from the sysctl file through the daemon restart to the container-level verification. Part 4 is the critical check: if the daemon’s limit is still 1024, the sysctl changes did not propagate to the Docker process.
Quick Reference
Key Sysctl Parameters
| Parameter | Default | Recommended | Purpose |
|---|---|---|---|
fs.file-max | Varies | 2097152 | System-wide FD limit |
fs.nr_open | 1048576 | 2097152 | Per-process FD ceiling |
net.ipv4.ip_local_port_range | 32768 60999 | 1024 65535 | Ephemeral ports |
net.ipv4.tcp_tw_reuse | 0 | 1 | Reuse TIME_WAIT sockets |
net.ipv4.tcp_fin_timeout | 60 | 15 | FIN-WAIT-2 timeout |
net.core.somaxconn | 128 | 65535 | Listen backlog |
net.core.netdev_max_backlog | 1000 | 65535 | Packet queue |
net.ipv4.tcp_fastopen | 0 | 3 | TCP Fast Open |
Docker Daemon Limit
| File | Parameter | Value |
|---|---|---|
/lib/systemd/system/docker.service (Ubuntu) | LimitNOFILE | 1048576 |
/usr/lib/systemd/system/docker.service (CentOS) | LimitNOFILE | 1048576 |
Container-Level Overrides
| Flag | Purpose |
|---|---|
--ulimit nofile=524288:524288 | Per-container FD limit |
--sysctl net.ipv4.ip_local_port_range=... | Per-container port range |
--sysctl net.ipv4.tcp_tw_reuse=1 | Per-container TIME_WAIT reuse |
Best Practices
✅ Do This:
# Set fs.nr_open at least as high as LimitNOFILE
fs.nr_open = 2097152 # ✅
# Use tcp_tw_reuse instead of the removed tcp_tw_recycle
net.ipv4.tcp_tw_reuse = 1 # ✅
# Verify the daemon limit after restart
cat /proc/$(pidof dockerd)/limits | grep files # ✅
# Set per-container limits with --ulimit for specific workloads
docker run --ulimit nofile=524288:524288 myapp # ✅
❌ Don’t Do This:
# Enable tcp_tw_recycle
net.ipv4.tcp_tw_recycle = 1 # removed in Linux 4.12, drops NAT packets // ❌
# Raise sysctl without restarting the Docker daemon
# The daemon keeps its old limit until restarted // ❌
# Set LimitNOFILE higher than fs.nr_open
# The process cannot open more than fs.nr_open // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| “Too many open files” persists | Daemon limit not raised | Edit systemd unit, restart |
| Port exhaustion after sysctl | tcp_tw_reuse not enabled | Set tcp_tw_reuse = 1 |
| Connections dropped | somaxconn too low | Raise to 65535 |
| Conntrack table full | Default 65536 | Raise nf_conntrack_max |
| NAT packets dropped | tcp_tw_recycle enabled | Remove it; use tcp_tw_reuse |
Real-World Examples
1. System-Wide FD Limit
fs.file-max = 2097152
2. Per-Process FD Ceiling
fs.nr_open = 2097152
3. Ephemeral Port Range
net.ipv4.ip_local_port_range = 1024 65535
4. TIME_WAIT Reuse
net.ipv4.tcp_tw_reuse = 1
5. Listen Backlog
net.core.somaxconn = 65535
6. TCP Buffers
net.ipv4.tcp_rmem = 4096 87380 16777216
7. Docker Daemon Limit
LimitNOFILE=1048576
8. Container Limit
docker run --ulimit nofile=524288:524288 myapp
9. Verify Daemon Limit
cat /proc/$(pidof dockerd)/limits | grep files
10. Verify Container Limit
docker exec myapp cat /proc/1/limits | grep files
Visual
The FD Limit Layers
┌──────────────────────────────────────────────────────────────┐
│ LAYER 1: SYSTEM-WIDE │
│ fs.file-max = 2097152 │
│ (total file descriptors across all processes) │
│ │
│ LAYER 2: PER-PROCESS CEILING │
│ fs.nr_open = 2097152 │
│ (maximum a single process can open) │
│ │
│ LAYER 3: DOCKER DAEMON │
│ LimitNOFILE=1048576 │
│ (limit inherited by containers) │
│ │
│ LAYER 4: CONTAINER OVERRIDE │
│ --ulimit nofile=524288:524288 │
│ (per-container limit) │
│ │
│ All four layers must be raised. Raising one is insufficient.│
│ │
└──────────────────────────────────────────────────────────────┘
Ephemeral Port Exhaustion
┌──────────────────────────────────────────────────────────────┐
│ DEFAULT RANGE: 32768 60999 (~28,000 ports) │
│ │
│ Connections/sec: 1,000 │
│ TIME_WAIT duration: 60 seconds │
│ Ports needed: 60,000 │
│ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ 28,000 ports / 1,000 per sec = 28 seconds to exhaust ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ TUNED RANGE: 1024 65535 (~64,500 ports) │
│ tcp_tw_reuse = 1 (reuse TIME_WAIT sockets) │
│ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ The range is doubled, and TIME_WAIT sockets are reused ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ Never use tcp_tw_recycle. It is removed and drops packets. │
│ │
└──────────────────────────────────────────────────────────────┘
The tcp_tw_recycle Trap
┌──────────────────────────────────────────────────────────────┐
│ tcp_tw_recycle = 1 │
│ │
│ Deprecated in Linux 4.10 │
│ Removed in Linux 4.12 │
│ │
│ Why it was removed: │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ It used per-host timestamps to detect old packets. ││
│ │ Behind NAT, clients share an IP but have different ││
│ │ timestamps. The kernel dropped valid SYN packets from ││
│ │ clients whose timestamps were older than the last seen ││
│ │ from that IP. ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ Use tcp_tw_reuse = 1 instead. │
│ It is safe for outbound connections and is not removed. │
│ │
└──────────────────────────────────────────────────────────────┘
High-Throughput Tuning Checklist
┌──────────────────────────────────────────────────────────────┐
│ FILE DESCRIPTORS │
│ [ ] fs.file-max raised │
│ [ ] fs.nr_open raised │
│ [ ] LimitNOFILE in docker.service raised │
│ [ ] Docker daemon restarted │
│ [ ] Container limit verified │
│ │
│ NETWORK │
│ [ ] ip_local_port_range widened │
│ [ ] tcp_tw_reuse = 1 │
│ [ ] tcp_fin_timeout reduced │
│ [ ] somaxconn raised │
│ [ ] netdev_max_backlog raised │
│ [ ] TCP buffers raised │
│ [ ] conntrack max raised │
│ │
│ Verify with sysctl and ss -s. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| System FD limit | fs.file-max |
| Per-process FD ceiling | fs.nr_open |
| Docker daemon FD limit | LimitNOFILE in systemd unit |
| Default ephemeral range | 32768–60999 |
| Recommended ephemeral range | 1024–65535 |
| TIME_WAIT reuse | net.ipv4.tcp_tw_reuse = 1 |
| Removed parameter | tcp_tw_recycle (Linux 4.12) |
| Listen backlog | net.core.somaxconn |
| TCP buffers | net.ipv4.tcp_rmem, tcp_wmem |
| Conntrack limit | net.netfilter.nf_conntrack_max |
Key takeaways:
- File descriptor limits are layered. The system-wide limit, the per-process ceiling, the Docker daemon’s
LimitNOFILE, and the container’s--ulimitall must be raised. Raising only one leaves the others as the bottleneck. The daemon must be restarted after the systemd unit change . - The default ephemeral port range is too small for high-connection workloads. At 1,000 connections per second with a 60-second
TIME_WAIT, the default 28,000 ports are exhausted in under 30 seconds. Widening the range and enablingtcp_tw_reuseresolves the exhaustion . tcp_tw_recycleis not an option. It was removed in Linux 4.12 because it dropped valid packets from clients behind NAT.tcp_tw_reuseis the safe alternative and is the correct parameter for outbound connection reuse .- The listen backlog and conntrack table are separate limits.
somaxconncontrols pending connections;nf_conntrack_maxcontrols the connection tracking table. Both need to be raised for high-throughput workloads, and both have defaults that are too low . - Container-level overrides exist but are not a substitute for host tuning. The
--ulimitand--sysctlflags set per-container limits, but the host’s kernel parameters and the daemon’s inherited limit remain the ceiling. Host tuning is the foundation; container flags are refinements.
Remember: Docker does not remove the host kernel from the I/O path. Every connection a container accepts or initiates is a socket in the host’s kernel, subject to the host’s file descriptor limit, port range, and backlog. The defaults are tuned for a single-user workstation, not for a container host serving thousands of concurrent clients. The tuning is not optional for high-throughput workloads; it is the prerequisite for the application to reach its own capacity. The parameters are few, the defaults are known, and the verification is a single command against the running daemon. Tuning the host is the first step; tuning the container is the second.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!