Docker Performance Tuning

Why Docker Performance Matters

On a small self-hosting server, resources are limited. A single misconfigured container can monopolize RAM or CPU and take down everything else. Performance tuning ensures all your services coexist reliably and respond quickly.

Prerequisites

Measure Before You Optimize

Container Resource Usage

# Live resource monitoring
docker stats

# Snapshot (non-streaming)
docker stats --no-stream

# Specific containers
docker stats container1 container2

# Output:
# NAME         CPU %   MEM USAGE / LIMIT   MEM %   NET I/O         BLOCK I/O
# nextcloud    2.5%    245MiB / 3.84GiB    6.2%    1.2MB / 500KB   50MB / 10MB
# postgres     0.3%    85MiB / 3.84GiB     2.2%    500KB / 1.5MB   20MB / 100MB

System-Wide Resources

# Overall memory
free -h

# CPU and load
uptime
nproc  # Number of CPU cores

# Disk I/O
iostat -x 1 5  # 5 samples, 1 second apart

# Disk space
df -h
docker system df  # Docker-specific disk usage

Memory Management

Set Memory Limits

Without limits, a single container can consume all available RAM and trigger the OOM killer, which may kill other containers or system processes.

services:
  myapp:
    image: myapp:v1.0
    deploy:
      resources:
        limits:
          memory: 512M    # Hard cap — container is killed if it exceeds this
        reservations:
          memory: 256M    # Guaranteed minimum allocation

Guidelines for common self-hosted apps:

AppRecommended LimitNotes
Nginx/Caddy/Traefik128-256MLow unless handling heavy traffic
PostgreSQL256M-1GDepends on database size and queries
Redis64-256MIn-memory store, size depends on cached data
Nextcloud512M-1GPHP app, increases with users
Jellyfin1-4GTranscoding needs more
Immich1-2GML models use significant memory
Vaultwarden64-128MVery lightweight

Optimize Swap

Swap prevents OOM kills when memory is tight:

# Check current swap
free -h

# Add 4GB swap file
sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

# Set swappiness (how eagerly the kernel swaps)
# Lower = swap less aggressively (better for servers)
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
sudo sysctl vm.swappiness=10

Disable Swap per Container (When Needed)

For latency-sensitive apps:

services:
  redis:
    deploy:
      resources:
        limits:
          memory: 256M
    # Prevent swapping for this container
    memswap_limit: 256M  # Same as memory = no swap

CPU Management

Limit CPU Usage

services:
  myapp:
    deploy:
      resources:
        limits:
          cpus: "1.5"    # Can use up to 1.5 CPU cores
        reservations:
          cpus: "0.5"    # Guaranteed 0.5 cores

Pin to Specific CPU Cores

Useful for isolating heavy workloads:

services:
  jellyfin:
    cpuset: "2,3"  # Only use cores 2 and 3

CPU Priority (Shares)

When multiple containers compete for CPU:

services:
  important-app:
    cpu_shares: 1024  # Default priority

  background-task:
    cpu_shares: 256   # Lower priority — gets less CPU when contested

CPU shares only matter under contention. When CPU is idle, any container can use all available cores.

Storage Performance

Choose the Right Storage Driver

# Check current storage driver
docker info | grep "Storage Driver"
DriverPerformanceBest For
overlay2GoodDefault, works well on ext4 and xfs
btrfsGood with btrfs filesystemServers already using btrfs
zfsGood with ZFSServers already using ZFS

overlay2 is the default and best choice for most self-hosting setups.

Use SSD for Docker Data

Docker’s data directory (/var/lib/docker) benefits massively from SSD storage.

# Move Docker to SSD (if currently on HDD)
sudo systemctl stop docker
sudo mv /var/lib/docker /mnt/ssd/docker
sudo ln -s /mnt/ssd/docker /var/lib/docker
sudo systemctl start docker

Or configure in daemon.json:

{
  "data-root": "/mnt/ssd/docker"
}

Optimize Volume Mounts

Bind mount performance tips:

  • Avoid mounting over slow network filesystems (NFS, CIFS) for write-heavy apps
  • Use named volumes for database data (Docker manages them more efficiently)
  • For large media libraries, bind mounts to direct disk paths are fine
# Database: use named volume (Docker manages it on local storage)
services:
  db:
    volumes:
      - db_data:/var/lib/postgresql/data

# Media: bind mount directly (large, sequential reads)
  jellyfin:
    volumes:
      - /mnt/media:/media:ro  # Read-only where possible

Logging Optimization

Limit Container Log Size

By default, Docker keeps unlimited logs. On a busy server, logs can fill your disk.

Per container:

services:
  myapp:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Globally (all containers):

sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}
EOF
sudo systemctl restart docker

See Container Logging for detailed log management.

Clean Up Old Logs

# Check log sizes
sudo du -sh /var/lib/docker/containers/*/  # Per container
sudo du -sh /var/lib/docker/containers/  # Total

# Truncate a specific container's log (keeps the file, empties content)
sudo truncate -s 0 /var/lib/docker/containers/CONTAINER_ID/CONTAINER_ID-json.log

Image Optimization

Remove Unused Images

# See what's taking space
docker system df

# Remove unused images
docker image prune       # Dangling images only
docker image prune -a    # All unused images

# Remove everything unused (images, containers, networks, build cache)
docker system prune -a

Use Smaller Base Images

If you build custom images, the base image matters:

BaseSizeUse When
alpine~5MBMinimal, good for most apps
debian-slim~80MBWhen you need glibc
ubuntu~75MBWhen you need Ubuntu-specific packages
scratch0MBStatic binaries only

Network Performance

Use Bridge Networking

Bridge mode (the default) is the best balance of performance and isolation.

# Default — good performance
services:
  myapp:
    # Uses bridge networking by default

# Host networking — marginal performance gain, less isolation
  myapp:
    network_mode: host

Host networking eliminates NAT overhead but removes network isolation. Only use it when you need it (e.g., for network scanning tools or when dealing with multicast).

Reduce DNS Lookups

If containers frequently resolve external domains:

# Add explicit DNS servers (avoids systemd-resolved latency)
services:
  myapp:
    dns:
      - 1.1.1.1

Docker Daemon Tuning

daemon.json Best Practices

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "storage-driver": "overlay2",
  "dns": ["1.1.1.1", "8.8.8.8"],
  "default-address-pools": [
    {"base": "10.10.0.0/16", "size": 24}
  ]
}

Enable Live Restore

Allows containers to keep running when the Docker daemon restarts:

{
  "live-restore": true
}

Monitoring Performance Over Time

Basic Monitoring with docker stats

Create a simple monitoring script:

#!/bin/bash
# Log container stats every 5 minutes
while true; do
  echo "=== $(date -u +%Y-%m-%dT%H:%M:%SZ) ==="
  docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}"
  echo ""
  sleep 300
done

Better: Use a Monitoring Stack

For proper performance monitoring, deploy a monitoring solution. See Monitoring Basics for options like Uptime Kuma, Grafana, or Netdata.

FAQ

Should I set memory limits on every container?

For production self-hosting, yes. At minimum, set limits on resource-heavy containers (databases, media servers, ML-based apps). Without limits, one misbehaving container can crash everything.

Does Docker have significant performance overhead compared to bare metal?

No. Docker’s overhead is negligible — typically <1% for CPU, zero for memory, and minimal for disk and network I/O. The isolation comes from Linux kernel features (namespaces, cgroups), not virtualization.

My server is slow but no single container is using much CPU or RAM. What’s wrong?

Check disk I/O with iostat -x 1. A slow or failing disk is the most common cause of “everything is slow.” Also check for high system load (uptime) — a load average higher than your CPU core count means processes are waiting.

How much memory overhead does Docker itself use?

The Docker daemon uses 50-100MB. Each container adds minimal overhead (a few MB for the runtime). The container’s application is what uses memory. A server with 4GB RAM can comfortably run 10-15 lightweight containers.

Comments