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=your-email@example.com
SMTP_PASSWORD=your-smtp-password
SMTP_FROM_ADDRESS=notifications@social.example.com
# 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 admin@example.com \
--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 admin@example.com. 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.
FAQ
Can I change my domain after launching?
No. LOCAL_DOMAIN is permanent. Once your instance federates with other servers, all ActivityPub URLs include your domain. Changing it breaks every federation link, follower relationship, and post reference. Choose your domain carefully before launching.
How much does it cost to run a Mastodon instance?
A single-user instance runs on a $5-10/month VPS (4 GB RAM). Storage is the main ongoing cost — federated media from other instances accumulates rapidly. Budget 50-200 GB for the first year. Using S3-compatible storage (Wasabi at $6/TB/month) keeps costs predictable for multi-user instances.
Can I import my followers from another Mastodon instance?
Followers can’t be transferred automatically. However, you can redirect your old account to your new one, and followers see a notification to re-follow. Export your following list, blocks, and mutes from the old instance and import them on the new one. Posts don’t migrate.
How does Mastodon compare to GoToSocial for personal use?
GoToSocial is dramatically lighter — a single binary using ~100 MB RAM vs Mastodon’s 4+ GB across five containers. GoToSocial lacks a built-in web UI (uses third-party clients only) and has fewer features. For a personal Fediverse presence, GoToSocial is the better choice. For a community instance with moderation tools, use Mastodon. See Mastodon vs GoToSocial.
Do I need Elasticsearch?
Not for small instances. Elasticsearch enables full-text search of posts — without it, you can only search by hashtags and users. For instances under 1,000 users, the resource cost (8+ GB additional RAM) usually isn’t worth it. Add it later if search becomes important.
How do I clean up disk space from federated media?
Use tootctl media remove to delete cached media from remote instances: docker compose exec web bin/tootctl media remove --days=7 removes media older than 7 days. This is safe — remote media is re-fetched if needed. Schedule this as a cron job for automatic cleanup.
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