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:

ContainerPurposeImage
webRails application server (Puma) — serves the UI and REST APIghcr.io/mastodon/mastodon:v4.5.7
streamingNode.js WebSocket server — real-time timeline updatesghcr.io/mastodon/mastodon-streaming:v4.5.7
sidekiqBackground job processor — federation, email, media processingghcr.io/mastodon/mastodon:v4.5.7
dbPostgreSQL databasepostgres:14-alpine
redisCache and job queueredis: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 SizeRAMCPUDisk
Personal (1-10 users)4 GB minimum2 cores50 GB
Small community (10-100 users)8 GB4 cores200 GB
Medium (100-1,000 users)16 GB8 cores500 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.

Comments