Self-Hosting Mastodon with Docker Compose
What Is Mastodon?
Mastodon is a decentralized social network built on the ActivityPub protocol. Running your own instance gives you full control over your social media presence — your data, your moderation rules, your community. It federates with thousands of other Mastodon instances and Fediverse-compatible software (Lemmy, Pixelfed, GoToSocial). Official site
Prerequisites
- A Linux server (Ubuntu 22.04+ recommended)
- Docker and Docker Compose installed (guide)
- 4 GB of RAM minimum (8 GB recommended)
- 50 GB of free disk space (media files grow fast)
- A domain name pointed at your server (required for federation)
- SMTP credentials for sending email (required for user registration)
- A reverse proxy (Nginx Proxy Manager, Traefik, or Caddy)
Architecture Overview
Mastodon runs as five containers working together:
| Container | Purpose | Image |
|---|---|---|
| web | Rails application server (Puma) — serves the UI and REST API | ghcr.io/mastodon/mastodon:v4.5.7 |
| streaming | Node.js WebSocket server — real-time timeline updates | ghcr.io/mastodon/mastodon-streaming:v4.5.7 |
| sidekiq | Background job processor — federation, email, media processing | ghcr.io/mastodon/mastodon:v4.5.7 |
| db | PostgreSQL database | postgres:14-alpine |
| redis | Cache and job queue | redis:7-alpine |
Docker Compose Configuration
Create a directory for Mastodon:
mkdir -p ~/mastodon && cd ~/mastodon
Create a docker-compose.yml file:
services:
db:
image: postgres:14-alpine
container_name: mastodon-db
restart: unless-stopped
shm_size: 256mb
networks:
- mastodon-internal
healthcheck:
test: ["CMD", "pg_isready", "-U", "mastodon"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- db-data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=mastodon
- POSTGRES_PASSWORD=change-this-db-password
- POSTGRES_DB=mastodon_production
redis:
image: redis:7-alpine
container_name: mastodon-redis
restart: unless-stopped
networks:
- mastodon-internal
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- redis-data:/data
web:
image: ghcr.io/mastodon/mastodon:v4.5.7
container_name: mastodon-web
restart: unless-stopped
env_file: .env.production
command: bundle exec puma -C config/puma.rb
networks:
- mastodon-external
- mastodon-internal
healthcheck:
test: ["CMD-SHELL", "curl -s --fail http://localhost:3000/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
ports:
- "127.0.0.1:3000:3000"
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- mastodon-media:/mastodon/public/system
streaming:
image: ghcr.io/mastodon/mastodon-streaming:v4.5.7
container_name: mastodon-streaming
restart: unless-stopped
env_file: .env.production
command: node ./streaming/index.js
networks:
- mastodon-external
- mastodon-internal
healthcheck:
test: ["CMD-SHELL", "curl -s --fail http://localhost:4000/api/v1/streaming/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
ports:
- "127.0.0.1:4000:4000"
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
sidekiq:
image: ghcr.io/mastodon/mastodon:v4.5.7
container_name: mastodon-sidekiq
restart: unless-stopped
env_file: .env.production
command: bundle exec sidekiq
networks:
- mastodon-external
- mastodon-internal
healthcheck:
test: ["CMD-SHELL", "ps aux | grep '[s]idekiq 6' | grep -v grep"]
interval: 30s
timeout: 10s
retries: 3
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- mastodon-media:/mastodon/public/system
networks:
mastodon-external:
mastodon-internal:
internal: true
volumes:
db-data:
redis-data:
mastodon-media:
Generate Secrets
Before starting Mastodon, you need to generate several secrets. Run these commands:
# Generate SECRET_KEY_BASE
docker compose run --rm web bundle exec rails secret
# Generate OTP_SECRET
docker compose run --rm web bundle exec rails secret
# Generate VAPID keys (for push notifications)
docker compose run --rm web bundle exec rails mastodon:webpush:generate_vapid_key
Save all the output — you’ll need these values for the .env.production file.
Environment File
Create .env.production alongside your docker-compose.yml:
# Federation — CANNOT BE CHANGED after first federation
LOCAL_DOMAIN=social.example.com
# Database
DB_HOST=db
DB_PORT=5432
DB_NAME=mastodon_production
DB_USER=mastodon
DB_PASS=change-this-db-password
# Redis
REDIS_HOST=redis
REDIS_PORT=6379
# Secrets — paste the values you generated above
SECRET_KEY_BASE=paste-your-generated-secret-here
OTP_SECRET=paste-your-generated-otp-secret-here
VAPID_PRIVATE_KEY=paste-your-vapid-private-key
VAPID_PUBLIC_KEY=paste-your-vapid-public-key
# SMTP — required for user registration and notifications
SMTP_SERVER=smtp.example.com
SMTP_PORT=587
SMTP_LOGIN=[email protected]
SMTP_PASSWORD=your-smtp-password
SMTP_FROM_ADDRESS=[email protected]
# Performance tuning
WEB_CONCURRENCY=2
MAX_THREADS=5
SIDEKIQ_CONCURRENCY=5
DB_POOL=25
# Optional: Elasticsearch for full-text search
# ES_ENABLED=true
# ES_HOST=es
# ES_PORT=9200
Critical: LOCAL_DOMAIN is permanent. Once your instance federates with other servers, changing this domain breaks all existing federation links. Choose your domain carefully before launching.
Initialize the Database
# Create database schema
docker compose run --rm web bundle exec rails db:setup
# Create your admin account
docker compose run --rm web bin/tootctl accounts create \
admin \
--email [email protected] \
--confirmed \
--role Owner
The second command outputs a randomly generated password. Save it — you’ll use it for your first login.
Start all services:
docker compose up -d
Reverse Proxy
Mastodon requires a reverse proxy that forwards both the web UI (port 3000) and the streaming API (port 4000). Here’s a Caddy configuration:
social.example.com {
handle /api/v1/streaming* {
reverse_proxy localhost:4000
}
handle {
reverse_proxy localhost:3000
}
}
For Nginx, add these locations:
location /api/v1/streaming {
proxy_pass http://127.0.0.1:4000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_buffering off;
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
For more details, see Reverse Proxy Setup.
Configuration
Registration Policies
After logging in as admin, go to Administration > Server Settings > Registrations:
- Open: Anyone can sign up (good for public instances)
- Approval required: New signups need admin approval
- Closed: No new registrations (personal/invite-only instances)
Server Rules and Custom Fields
Set instance rules at Administration > Server Settings > About. These show on the About page and during registration.
Content Moderation
Mastodon provides moderation tools at Administration > Moderation:
- Domain blocks: Silence or suspend entire instances
- User reports: Handle reported content
- IP blocks: Block registration from specific IP ranges
- Email domain blocks: Prevent signups from disposable email providers
Media Storage with S3
For instances with more than a few users, user-uploaded media will consume significant disk space. Configure S3-compatible object storage:
# Add to .env.production
S3_ENABLED=true
S3_BUCKET=mastodon-media
S3_REGION=us-east-1
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
S3_HOSTNAME=s3.amazonaws.com
S3_PROTOCOL=https
Works with any S3-compatible provider (AWS, Wasabi, MinIO, Backblaze B2).
Backup
Database
docker compose exec db pg_dump -U mastodon mastodon_production > mastodon-db-$(date +%Y%m%d).sql
Media Files
Back up the media volume. If using local storage:
docker run --rm -v mastodon_mastodon-media:/data -v $(pwd):/backup alpine tar czf /backup/mastodon-media-$(date +%Y%m%d).tar.gz /data
Critical Files
Also back up:
.env.production(contains all secrets)docker-compose.yml
Without SECRET_KEY_BASE and OTP_SECRET, you cannot restore user sessions. For a comprehensive backup strategy, see Backup Strategy.
Troubleshooting
Federation Not Working
Symptom: Posts from your instance don’t appear on other servers. Remote users can’t find your profile.
Fix: Verify that your LOCAL_DOMAIN resolves to your server and HTTPS is working. Check that your reverse proxy forwards WebFinger requests correctly (the /.well-known/webfinger path must be accessible). Run docker compose logs web | grep -i webfinger to check for errors.
Sidekiq Queue Growing
Symptom: The admin dashboard shows thousands of queued jobs. Notifications and federation are delayed.
Fix: Check Sidekiq health with docker compose exec sidekiq ps aux. If it’s running but slow, increase SIDEKIQ_CONCURRENCY in .env.production. For large instances, run multiple Sidekiq containers with different queue assignments.
Media Uploads Failing
Symptom: Users can’t upload images or videos. Error messages about storage.
Fix: Check disk space with df -h. If using local storage, the mastodon-media volume may be full. Consider migrating to S3-compatible object storage. Verify file permissions inside the container: docker compose exec web ls -la /mastodon/public/system/.
Emails Not Sending
Symptom: Registration confirmations and notifications don’t arrive.
Fix: Verify SMTP settings in .env.production. Test with: docker compose exec web bin/tootctl email send-test [email protected]. Check the Sidekiq mailers queue in the admin dashboard — failed email jobs appear there with error details.
High Memory Usage
Symptom: Server becomes sluggish or OOM-killed.
Fix: Reduce WEB_CONCURRENCY (Puma workers) and SIDEKIQ_CONCURRENCY. Each Puma worker uses ~300-500 MB. Two workers with 5 threads handles most small instances. Clear cached remote media: docker compose exec web bin/tootctl media remove --days=7.
Resource Requirements
| Instance Size | RAM | CPU | Disk |
|---|---|---|---|
| Personal (1-10 users) | 4 GB minimum | 2 cores | 50 GB |
| Small community (10-100 users) | 8 GB | 4 cores | 200 GB |
| Medium (100-1,000 users) | 16 GB | 8 cores | 500 GB+ |
Media storage is the primary disk consumer. Remote media from federated instances is cached locally and can accumulate rapidly. Use tootctl media remove to clean old remote media, or configure S3 for unlimited storage.
Verdict
Mastodon is the most mature and feature-complete self-hosted social network. It’s the de facto standard for running your own Fediverse instance — well-documented, actively maintained, and supported by a massive ecosystem of apps and tools.
The downside is resource requirements. Five containers, PostgreSQL, Redis, and potentially Elasticsearch make it one of the heavier self-hosted applications. For personal use or a single-user instance, GoToSocial is dramatically lighter (a single binary, ~100 MB RAM). For a multi-user community, Mastodon’s moderation tools and polished UI justify the overhead.
Run Mastodon if you want a public-facing instance with open registration and full moderation controls. Run GoToSocial if you want a personal Fediverse presence without the resource cost.
Related
Get self-hosting tips in your inbox
Get the Docker Compose configs, hardware picks, and setup shortcuts we don't put in articles. Weekly. No spam.
Comments