Running a Mastodon instance with Docker Compose in a production environment requires more than just copying the default configuration files. Many administrators find their instance becomes slow, unresponsive, or suffers from database connection failures after a few days of live use. These problems usually stem from missing performance tuning, inadequate resource allocation, or incorrect volume and network setup. This article explains the critical production adjustments you must make to Docker Compose files, environment variables, and system settings before going live.
Key Takeaways: Mastodon Docker Compose Production Hardening
- docker-compose.yml resource limits: Set cpu_count and memory limits for the web, streaming, and sidekiq services to prevent resource starvation.
- SECRET_KEY_BASE and OTP_SECRET environment variables: Generate unique, long-lived secrets using
rake secretand never reuse development values in production. - PostgreSQL and Redis volume mounts: Use named volumes or host-mounted directories with proper ownership to avoid data loss on container restarts.
Why Default Docker Compose Settings Fail in Production
The official Mastodon Docker Compose repository provides a development-friendly configuration. In production, the default settings allow unlimited memory usage for the Sidekiq worker pool, which quickly exhausts server RAM when handling image uploads and federation activity. The web process also uses a single-threaded Puma server by default, causing request queuing under moderate traffic. The streaming API service, built on Node.js, can drop connections if its heartbeat timeout is not adjusted. These three factors create a cascade of failures: the database pool fills with stale connections, Redis memory overflows, and the web interface becomes unresponsive. Production tuning addresses each of these layers explicitly.
Resource Limits for Docker Services
Without explicit resource limits, Docker containers compete for the host machine’s CPU and memory. The Sidekiq service, which processes background jobs like media compression and federation delivery, spawns multiple threads. If you do not cap its memory, a single job that processes a large video file can cause the entire instance to swap. Add the following resource reservation blocks under each service in docker-compose.yml:
web:
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
sidekiq:
deploy:
resources:
limits:
memory: 1G
reservations:
memory: 512M
streaming:
deploy:
resources:
limits:
memory: 256M
reservations:
memory: 128M
These values assume a server with at least 2 GB of RAM. Adjust the limits based on your expected concurrent users. The streaming service typically requires less memory because it maintains long-lived WebSocket connections with minimal per-user overhead.
Steps to Convert Docker Compose for Production
- Generate production secrets
RunRAILS_ENV=production bundle exec rake secrettwice inside the Mastodon application directory. Copy the first output toSECRET_KEY_BASEand the second toOTP_SECRETin your.env.productionfile. These values must be at least 128 characters long and contain a mix of letters, numbers, and symbols. Never use the default values from the development.envfile. - Configure the database connection pool
In.env.production, setDB_POOL=25for a single-web-process setup. If you run multiple web replicas, divide the total database connections by the number of replicas. For example, with 5 web containers and a database max_connections of 100, setDB_POOL=20. This prevents PostgreSQL from running out of available connections. - Adjust Sidekiq concurrency
AddSIDEKIQ_CONCURRENCY=10to.env.production. The default value of 25 creates 25 threads per Sidekiq process. On a 2-core server, 10 threads provide better throughput without excessive context switching. Monitor Sidekiq queue latency in the Mastodon admin panel and increase the value only if queues stay above 10 seconds consistently. - Set the streaming API heartbeat interval
AddSTREAMING_API_BASE_URL=https://yourdomain.comandSTREAMING_CLUSTER_NUM=1to.env.production. The heartbeat interval defaults to 30 seconds. For production behind a reverse proxy like Nginx, increase the proxy read timeout to 60 seconds to match. Edit thestreaming/index.jsfile or use an environment variableHEARTBEAT_INTERVAL_MS=45000to reduce disconnections. - Use named volumes for PostgreSQL and Redis
Indocker-compose.yml, replace bind mounts with named volumes. Example:volumes:
postgres_data:
redis_data:
Then reference them in the service definition:volumes:
- postgres_data:/var/lib/postgresql/data
Named volumes persist across container upgrades and are easier to back up usingdocker volume inspect. - Enable automatic restart and health checks
Addrestart: unless-stoppedto every service. For the web and streaming services, add a health check:healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
This allows Docker to restart unresponsive containers automatically without manual intervention. - Limit log file size
Add logging configuration to each service:logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
Without this, container logs can fill the disk partition, causing the instance to crash. The json-file driver rotates logs when they reach 10 MB and keeps three rotated files.
Common Misconfigurations That Break Production Instances
Redis Memory Exhaustion Without Eviction Policy
Mastodon uses Redis for Sidekiq job queues, session caching, and rate limiting. The default Redis configuration in Docker does not set a memory limit. When Redis exceeds the available RAM, the kernel OOM killer terminates the Redis process. Add the following to your redis.conf or pass it as a command argument:maxmemory 256mb
maxmemory-policy allkeys-lru
This limits Redis to 256 MB and evicts the least recently used keys when memory fills. For high-traffic instances, increase to 512 MB.
PostgreSQL Connection Pool Underestimated
Each Mastodon web process opens a fixed number of database connections defined by DB_POOL. If you run multiple web containers behind a load balancer, the total connections equal DB_POOL number_of_web_containers. PostgreSQL has a default max_connections of 100. If your total exceeds 100, new connections are refused. Check the database container logs for FATAL: sorry, too many clients already. Increase max_connections in the PostgreSQL configuration or reduce the number of web replicas.
Missing Reverse Proxy Timeout for File Uploads
Mastodon allows file uploads up to the limit set in MAX_IMAGE_SIZE and MAX_VIDEO_SIZE. If your Nginx or Apache reverse proxy has a default request timeout of 60 seconds, large uploads time out before reaching the Mastodon application. Increase proxy_read_timeout and proxy_send_timeout to 300 seconds in your reverse proxy configuration. Also set client_max_body_size to match your upload limit, for example client_max_body_size 100M.
| Item | Development Default | Production Recommended |
|---|---|---|
| SECRET_KEY_BASE | ChangeMe | 128+ character random string |
| OTP_SECRET | ChangeMe | 128+ character random string |
| DB_POOL | 5 | 20–25 |
| SIDEKIQ_CONCURRENCY | 25 | 10 |
| Redis maxmemory | Unlimited | 256 MB |
| PostgreSQL max_connections | 100 | 150–200 |
| Log driver | json-file (unlimited) | json-file (10 MB, 3 files) |
These production notes cover the essential changes that transform a default Docker Compose Mastodon setup into a stable, self-hosted instance. After applying the resource limits, secret generation, and connection pool adjustments, run docker-compose up -d and verify the health endpoints. Monitor Sidekiq queue latency and database connection counts daily during the first week. For advanced tuning, consider adding a CDN for static assets and enabling Redis Sentinel for high availability.