Skip to content

Deploy Mastodon

Mastodon is a more demanding deployment than most self-host apps — multiple services, ActivityPub federation, media storage. This guide gets you a single-user or small-community instance on dockmesh.

  • Mastodon at social.example.com
  • PostgreSQL database
  • Redis for caching + streaming
  • Elasticsearch for full-text search (optional but recommended)
  • S3-compatible media storage (off-host, essential)
  • SMTP for email confirmations
  • Backups of DB + user uploads
  • Host with 4 GB RAM minimum, 8 GB comfortable
  • S3-compatible bucket (Wasabi, B2, MinIO) for media — typical usage is 20-100 GB
  • SMTP account (Postmark, Mailgun, SendGrid, or your own mail server)
  • DNS: social.example.com pointing to your host

Before deploying, run on any machine with Ruby:

Terminal window
docker run --rm -it ghcr.io/mastodon/mastodon:v4.2 bin/rake secret
# Run 4 times, saving each output as:
# SECRET_KEY_BASE, OTP_SECRET, VAPID_PRIVATE_KEY (from different rake task), VAPID_PUBLIC_KEY

For VAPID keys:

Terminal window
docker run --rm -it ghcr.io/mastodon/mastodon:v4.2 bin/rake mastodon:webpush:generate_vapid_key

Save these — you’ll paste them into environment variables.

Stacks → New stack → name mastodon:

services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: mastodon_production
POSTGRES_USER: mastodon
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- db:/var/lib/postgresql/data
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- redis:/data
es:
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.22
restart: unless-stopped
environment:
discovery.type: single-node
xpack.security.enabled: "false"
ES_JAVA_OPTS: "-Xms512m -Xmx512m"
volumes:
- es:/usr/share/elasticsearch/data
web:
image: ghcr.io/mastodon/mastodon:v4.2
restart: unless-stopped
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
depends_on: [db, redis, es]
env_file: /opt/dockmesh/stacks/local/mastodon/.env.production
volumes:
- uploads:/mastodon/public/system
streaming:
image: ghcr.io/mastodon/mastodon:v4.2
restart: unless-stopped
command: node ./streaming
depends_on: [db, redis]
env_file: /opt/dockmesh/stacks/local/mastodon/.env.production
sidekiq:
image: ghcr.io/mastodon/mastodon:v4.2
restart: unless-stopped
command: bundle exec sidekiq
depends_on: [db, redis]
env_file: /opt/dockmesh/stacks/local/mastodon/.env.production
volumes:
- uploads:/mastodon/public/system
volumes:
db:
redis:
es:
uploads:

Mastodon reads its config from an env file. Create /opt/dockmesh/stacks/local/mastodon/.env.production:

LOCAL_DOMAIN=social.example.com
WEB_DOMAIN=social.example.com
SINGLE_USER_MODE=false
SECRET_KEY_BASE=<generated above>
OTP_SECRET=<generated above>
VAPID_PRIVATE_KEY=<generated above>
VAPID_PUBLIC_KEY=<generated above>
DB_HOST=db
DB_USER=mastodon
DB_NAME=mastodon_production
DB_PASS=<same as DB_PASSWORD in stack env>
DB_PORT=5432
REDIS_HOST=redis
REDIS_PORT=6379
ES_ENABLED=true
ES_HOST=es
ES_PORT=9200
# S3
S3_ENABLED=true
S3_BUCKET=mastodon-media
AWS_ACCESS_KEY_ID=<your S3 key>
AWS_SECRET_ACCESS_KEY=<your S3 secret>
S3_PROTOCOL=https
S3_HOSTNAME=s3.us-west-001.backblazeb2.com
S3_ENDPOINT=https://s3.us-west-001.backblazeb2.com
S3_ALIAS_HOST=media.social.example.com
# SMTP
SMTP_SERVER=smtp.postmarkapp.com
SMTP_PORT=587
SMTP_LOGIN=<your smtp user>
SMTP_PASSWORD=<your smtp pass>
SMTP_FROM_ADDRESS=social@example.com

Before deploying the stack, initialize the database:

Terminal window
# On the dockmesh host
docker run --rm --network mastodon_default \
--env-file /opt/dockmesh/stacks/local/mastodon/.env.production \
ghcr.io/mastodon/mastodon:v4.2 \
bundle exec rails db:setup

(You’ll need to create the network first — or deploy once, let it fail, then run this.)

Deploy the stack. Watch for each container to become healthy:

  • db — immediate
  • redis — immediate
  • es — ~30s (memory-hungry, watch it doesn’t OOM)
  • web — ~60s (Rails boot)
  • streaming — ~15s
  • sidekiq — ~15s

Stack → Proxy → Add route:

  • Domain: social.example.com
  • Target: mastodon_web_1
  • Port: 3000
  • TLS: Automatic

Then a second route for streaming:

  • Domain: social.example.com
  • Path: /api/v1/streaming
  • Target: mastodon_streaming_1
  • Port: 4000
  • TLS: Automatic

Streaming needs WebSocket passthrough — Caddy does this by default.

Terminal window
docker exec -it mastodon_web_1 bin/tootctl accounts create admin \
--email you@example.com --confirmed --role Owner

Save the auto-generated password it prints. Log in at https://social.example.com/auth/sign_in.

Mastodon is multi-volume:

  • db — PostgreSQL data (use pg_dumpall hook)
  • redis — OK to skip (volatile cache)
  • es — OK to skip (rebuilds from DB)
  • uploads — critical if you haven’t gone all-in on S3 (old media)

Plus .env.production — back it up separately (contains secrets).

S3 media is backed up by the S3 provider’s own versioning — enable that on your bucket.

Default memory: 2-3 GB for a small instance. For hundreds of users, scale sidekiq workers:

Scaling tab → sidekiq → set replicas to 3-5. Each worker processes a queue subset.

Terminal window
# Weekly — clean up old cached media from remote instances
docker exec -it mastodon_web_1 bin/tootctl media remove --days=30
# Monthly — remove accounts the instance no longer knows about
docker exec -it mastodon_web_1 bin/tootctl accounts cull
# After federation issues — refresh relationships
docker exec -it mastodon_web_1 bin/tootctl accounts refresh