01Introduction
Default Docker configurations prioritize compatibility over performance. With some tuning, you can significantly reduce resource usage, speed up container startup, and improve application responsiveness. This guide covers practical optimizations for self-hosted setups.
02Optimize Your Images
Smaller images start faster and use less disk I/O. Choose minimal base images and remove unnecessary components.
1# BAD: Full OS images2services: 3 app: 4 image: ubuntu:22.04 # 77MB56# GOOD: Minimal images7services: 8 app: 9 image: alpine:3.19 # 7MB1011# BETTER: Distroless (for compiled apps)12# FROM gcr.io/distroless/static # ~2MB1314# Compare image sizes15docker images --format "table {{.Repository}}: {{.Tag}} {{.Size}}"1617# Common lightweight alternatives:18# python:3.11 -> python:3.11-slim or python:3.11-alpine19# node:20 -> node:20-slim or node:20-alpine20# postgres:15 -> postgres:15-alpine21# nginx:latest -> nginx:alpineAlpine-based images are typically 10x smaller than Debian-based. But test compatibility—some apps need glibc instead of musl.
03Set Resource Limits
Without limits, one container can starve others. Set appropriate limits based on actual usage.
1services: 2 app: 3 image: myapp:1.04 deploy: 5 resources: 6 limits: 7 cpus: '2' # Max 2 CPU cores8 memory: 1G # Max 1GB RAM9 reservations: 10 cpus: '0.5' # Guaranteed 0.5 cores11 memory: 256M # Guaranteed 256MB RAM1213 # Database with higher memory14 postgres: 15 image: postgres:15-alpine16 deploy: 17 resources: 18 limits: 19 cpus: '2'20 memory: 2G21 reservations: 22 memory: 512M23 # Postgres-specific tuning in postgresql.conf04Establish Resource Baseline
Before tuning, understand current resource usage. Monitor over time to set appropriate limits.
1# Real-time container stats2docker stats34# Container-specific detailed stats5docker stats --no-stream --format "table {{.Name}} {{.CPUPerc}} {{.MemUsage}} {{.NetIO}} {{.BlockIO}}"67# Historical monitoring with ctop8docker run --rm -it \9 -v /var/run/docker.sock:/var/run/docker.sock \10 quay.io/vektorlab/ctop1112# Or use Prometheus + Grafana stack13services:14 cadvisor:15 image: gcr.io/cadvisor/cadvisor:latest16 volumes:17 - /:/rootfs:ro18 - /var/run:/var/run:ro19 - /sys:/sys:ro20 - /var/lib/docker/:/var/lib/docker:ro05Storage Optimization
Docker's storage driver affects performance significantly. Use the right driver and optimize volume mounts.
1# Check current storage driver2docker info | grep "Storage Driver"3# overlay2 is recommended for most setups45# For database workloads, use named volumes with proper mount options6services: 7 postgres: 8 volumes: 9 - type: volume10 source: postgres_data11 target: /var/lib/postgresql/data12 volume: 13 nocopy: true # Skip initial copy, faster start1415volumes: 16 postgres_data: 17 driver: local18 driver_opts: 19 type: none20 device: /mnt/fast-ssd/postgres # Put on fast storage21 o: bind2223# Avoid storing database data on overlay filesystem24# Use bind mounts to fast local storageFor databases, use local NVMe/SSD storage with bind mounts. Don't rely on Docker's storage driver for I/O-heavy workloads.
06Network Optimization
Docker's default bridge network adds overhead. For performance-critical services, consider host networking or optimized bridge settings.
1# For maximum network performance: host mode2# (Removes network isolation!)3services: 4 high-perf-app: 5 network_mode: host67# For bridge mode, use custom networks with optimal settings8networks: 9 optimized: 10 driver: bridge11 driver_opts: 12 com.docker.network.driver.mtu: 9000 # Jumbo frames if supported1314# Enable IPv6 if needed (can improve certain workloads)15# In /etc/docker/daemon.json:16{17 "ipv6": true,18 "fixed-cidr-v6": "fd00::/64"19}07Logging Optimization
Default JSON logging is slow for high-volume applications. Choose the right logging driver.
1services: 2 high-volume-app: 3 logging: 4 driver: "local" # Faster than json-file5 options: 6 max-size: "10m"7 max-file: "3"8 compress: "true"910 # For apps where you don't need logs in Docker11 batch-job: 12 logging: 13 driver: "none" # Completely disable1415# Global configuration in /etc/docker/daemon.json16{17 "log-driver": "local",18 "log-opts": {19 "max-size": "10m",20 "max-file": "3"21 }22}The 'none' driver means no logs via docker logs. Only use for containers with external logging.
08Faster Container Startup
Reduce container startup time with proper dependencies and health checks.
1services: 2 app: 3 depends_on: 4 db: 5 condition: service_healthy # Wait for DB to be ready6 # Don't wait for everything, just essentials78 db: 9 image: postgres:15-alpine10 healthcheck: 11 test: ["CMD-SHELL", "pg_isready -U postgres"]12 interval: 2s # Check frequently13 timeout: 1s # Fail fast14 retries: 10 # Allow slow starts15 start_period: 5s1617# Parallel startup where possible18# Services without dependencies start simultaneously1920# Avoid unnecessary restarts21restart: unless-stopped22# Not 'always' - prevents restart loopsUse start_period in healthchecks to give slow-starting containers time to initialize before health checks begin.
09Database-Specific Tuning
Databases benefit from specific tuning. Here are common optimizations for popular databases.
1services: 2 postgres: 3 image: postgres:15-alpine4 environment: 5 # Basic performance tuning6 - POSTGRES_INITDB_ARGS=--data-checksums7 volumes: 8 - ./postgresql.conf:/etc/postgresql/postgresql.conf:ro9 command: postgres -c config_file=/etc/postgresql/postgresql.conf1011# postgresql.conf tuning for 4GB RAM server:12# shared_buffers = 1GB # 25% of RAM13# effective_cache_size = 3GB # 75% of RAM14# work_mem = 64MB15# maintenance_work_mem = 256MB16# max_connections = 1001718 redis: 19 image: redis:alpine20 command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru2122 mysql: 23 image: mysql:824 command: --innodb-buffer-pool-size=1G --innodb-log-file-size=256M10Add Caching Layers
Reduce load on databases and APIs with caching. Redis is the go-to choice for Docker deployments.
1services: 2 app: 3 environment: 4 - REDIS_URL=redis://redis:63795 depends_on: 6 - redis78 redis: 9 image: redis:alpine10 command: >11 redis-server12 --maxmemory 512mb13 --maxmemory-policy allkeys-lru14 --appendonly no15 volumes: 16 - redis_data:/data17 deploy: 18 resources: 19 limits: 20 memory: 768M # More than maxmemory for overhead2122 # Or use Varnish for HTTP caching23 varnish: 24 image: varnish:stable25 volumes: 26 - ./default.vcl:/etc/varnish/default.vcl:ro27 environment: 28 - VARNISH_SIZE=256M2930volumes: 31 redis_data: 11Docker Daemon Tuning
Global Docker settings that affect all containers.
1# /etc/docker/daemon.json2{3 "storage-driver": "overlay2",4 "log-driver": "local",5 "log-opts": {6 "max-size": "10m",7 "max-file": "3"8 },9 "default-ulimits": {10 "nofile": {11 "Name": "nofile",12 "Hard": 65536,13 "Soft": 6553614 }15 },16 "live-restore": true,17 "userland-proxy": false,18 "default-shm-size": "128M"19}2021# Apply changes22sudo systemctl restart docker2324# Verify25docker infolive-restore keeps containers running during Docker daemon restarts—great for updates.
12Continuous Monitoring
Performance tuning is ongoing. Set up monitoring to catch issues early.
1# Lightweight monitoring stack2services: 3 prometheus: 4 image: prom/prometheus:latest5 volumes: 6 - ./prometheus.yml:/etc/prometheus/prometheus.yml7 - prometheus_data:/prometheus8 command: 9 - '--config.file=/etc/prometheus/prometheus.yml'10 - '--storage.tsdb.retention.time=15d'1112 grafana: 13 image: grafana/grafana:latest14 volumes: 15 - grafana_data:/var/lib/grafana16 environment: 17 - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}1819 node-exporter: 20 image: prom/node-exporter:latest21 pid: host22 volumes: 23 - /proc:/host/proc:ro24 - /sys:/host/sys:ro25 command: 26 - '--path.procfs=/host/proc'27 - '--path.sysfs=/host/sys'2829volumes: 30 prometheus_data: 31 grafana_data: