Mastodon Server Setup: Deploy a Decentralized Social Media Node via Docker
Host your own decentralized social media instance. Complete guide to self-hosting Mastodon using Docker Compose and Postgres.
Mastodon Server Setup: Deploy a Decentralized Social Media Node via Docker
Mastodon is the leading decentralized, federated microblogging platform powered by the ActivityPub protocol. Operating your own Mastodon instance gives you complete control over your data, moderation policies, and community standards. However, self-hosting a federated service presents unique infrastructure challenges.
Because Mastodon constantly communicates with thousands of other instances across the fediverse, it requires a robust, distributed architecture to handle background jobs, media delivery, and persistent data streams.
This guide provides a comprehensive, production-ready blueprint for deploying Mastodon on a Virtual Private Server (VPS) using Docker Compose. We will configure Postgres for relational data, Redis for caching and job queuing, an S3-compatible cloud storage provider for federated media assets, and a reverse proxy for SSL termination and websocket routing.
1. Architectural Overview
To host Mastodon reliably under load, you must understand its decoupled multi-service architecture:
graph TD
Client[Web/Mobile Client] -->|HTTPS / WSS| Proxy[Nginx / Caddy Reverse Proxy]
Proxy -->|Port 3000| Web[Mastodon Web - Puma]
Proxy -->|Port 4000| Stream[Mastodon Streaming - Node.js]
Web --> DB[(PostgreSQL)]
Stream --> DB
Web --> Cache[(Redis Cache / Sidekiq Queue)]
Stream --> Cache
Sidekiq[Sidekiq Background Workers] --> DB
Sidekiq --> Cache
Web --> S3[S3-Compatible Object Storage]
Sidekiq --> S3
- Mastodon Web (Puma): A Ruby on Rails application server handling standard HTTP REST API requests and server-side page rendering.
- Mastodon Streaming (Node.js): A lightweight Node.js daemon that handles long-lived WebSocket connections to push real-time timeline updates to clients.
- Sidekiq: A background job processing engine that processes queue-heavy tasks such as media processing, remote profile fetching, and federated delivery.
- PostgreSQL: The persistent relational database storing user records, posts, relationships, and metadata.
- Redis: A fast in-memory store acting as both a transient cache and the job queue coordinator for Sidekiq.
- S3-Compatible Object Storage: External storage (e.g., AWS S3, MinIO, Backblaze B2, Wasabi) to store user uploads and cached media from federated accounts. Self-hosting media locally on the VPS disk will quickly exhaust your storage capacity.
2. Infrastructure Prerequisites
Before deploying, ensure you have:
- A Linux VPS: Recommended minimum 2 vCPUs, 4GB RAM, and 40GB SSD. Mastodon's sidekiq workers and media processing are memory-intensive.
- A Fully Qualified Domain Name (FQDN): Pointing to your VPS IP address (e.g.,
mastodon.example.com). Note: Once configured, changing your Mastodon instance domain is virtually impossible without breaking federation history. - SMTP Relay Credentials: Mastodon requires an email service (e.g., Mailgun, SendGrid, Postmark, or self-hosted SMTP) for signups, password resets, and notifications.
- S3-Compatible Storage Bucket: An active bucket with dedicated API access keys.
3. Directory Layout and Storage
We will establish a predictable and secure directory layout under /opt/mastodon for configuration files and database volumes. Run the following commands on your VPS to create the structure:
sudo mkdir -p /opt/mastodon
sudo mkdir -p /opt/mastodon/postgres
sudo mkdir -p /opt/mastodon/redis
sudo mkdir -p /opt/mastodon/public/system
sudo chown -R 991:991 /opt/mastodon/public
Note: The user ID 991 matches the default non-root mastodon user inside the official Docker container. Granting write permissions to 991 prevents permission errors during local storage operations.
4. The Docker Compose Configuration
Create the docker-compose.yml file in /opt/mastodon/docker-compose.yml. This configuration defines the orchestration for all required services.
version: '3.8'
services:
db:
image: postgres:14-alpine
shm_size: 256mb
restart: always
environment:
- POSTGRES_DB=mastodon_production
- POSTGRES_USER=mastodon
- POSTGRES_PASSWORD=your_secure_db_password
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mastodon -d mastodon_production"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- /opt/mastodon/postgres:/var/lib/postgresql/data
redis:
image: redis:7-alpine
restart: always
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- /opt/mastodon/redis:/data
web:
image: tootsuite/mastodon:v4.2.8
restart: always
env_file: .env.production
command: bundle exec puma -C config/puma.rb
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "127.0.0.1:3000:3000"
volumes:
- /opt/mastodon/public/system:/mastodon/public/system
healthcheck:
test: ["CMD-SHELL", "wget -q --spider --proxy=off http://localhost:3000/health || exit 1"]
interval: 20s
timeout: 10s
retries: 3
streaming:
image: tootsuite/mastodon:v4.2.8
restart: always
env_file: .env.production
command: node ./streaming
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "127.0.0.1:4000:4000"
healthcheck:
test: ["CMD-SHELL", "wget -q --spider --proxy=off http://localhost:4000/api/v1/streaming/health || exit 1"]
interval: 20s
timeout: 10s
retries: 3
sidekiq:
image: tootsuite/mastodon:v4.2.8
restart: always
env_file: .env.production
command: bundle exec sidekiq
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- /opt/mastodon/public/system:/mastodon/public/system
Key Highlights of the Docker Compose Configuration:
shm_size: 256mb: Increases shared memory for the PostgreSQL container, which is critical for complex queries and vacuum operations on large databases.healthcheck: Uses active checks to guarantee that dependent containers do not start until their underlying dependencies are healthy and ready to accept connections.- Port Bindings: Binds ports
3000and4000exclusively to127.0.0.1. This forces all traffic to route through the local reverse proxy, shielding the web application and Node.js socket layers from direct public access.
5. Configuring Mastodon: .env.production
Create the configuration file /opt/mastodon/.env.production. This file contains the environment variables injected into the Mastodon containers.
# Domain Configuration
LOCAL_DOMAIN=mastodon.example.com
# Redis Configuration
REDIS_HOST=redis
REDIS_PORT=6379
# PostgreSQL Configuration
DB_HOST=db
DB_USER=mastodon
DB_NAME=mastodon_production
DB_PASS=your_secure_db_password
DB_PORT=5432
# S3 Cloud Storage Integration (MinIO/AWS S3/Wasabi/Backblaze)
S3_ENABLED=true
S3_PROTOCOL=https
S3_BUCKET=your-mastodon-bucket
S3_REGION=us-east-1
S3_ENDPOINT=https://s3.us-east-1.amazonaws.com
AWS_ACCESS_KEY_ID=your_aws_access_key
AWS_SECRET_ACCESS_KEY=your_aws_secret_key
# Optional CDN alias (highly recommended to save outbound bandwidth)
# S3_ALIAS_HOST=cdn.example.com
# SMTP Email Configuration
SMTP_SERVER=smtp.mailgun.org
SMTP_PORT=587
SMTP_UID=postmaster@mastodon.example.com
SMTP_PASSWORD=your_smtp_password
SMTP_FROM_ADDRESS=notifications@mastodon.example.com
SMTP_SSL=false
SMTP_STARTTLS=true
# Secrets and Keys (Do not leak these!)
SECRET_KEY_BASE=generate_using_instructions_below
OTP_SECRET=generate_using_instructions_below
VAPID_PRIVATE_KEY=generate_using_instructions_below
VAPID_PUBLIC_KEY=generate_using_instructions_below
6. Initializing Databases and Generating Secrets
Do not attempt to start the containers yet. First, we need to generate unique cryptographic secrets and run the database migration scripts.
Step 6.1: Generate Cryptographic Secrets
Run the generator commands inside a temporary container to output the keys:
# Generate SECRET_KEY_BASE
docker compose run --rm web bundle exec rake secret
# Generate OTP_SECRET
docker compose run --rm web bundle exec rake secret
# Generate VAPID Keys
docker compose run --rm web bundle exec rake mastodon:webpush:generate_keys
Copy these generated values and paste them into your /opt/mastodon/.env.production file under the respective fields.
Step 6.2: Run Database Setup and Assets Precompilation
Execute the initialization command. This will create the database schema, load seeds, and build initial assets inside Postgres:
docker compose run --rm web bundle exec rails db:setup
Step 6.3: Start the Instance
With secrets populated and the database seeded, launch your container stack in daemon mode:
docker compose up -d
7. Creating Your First Administrator Account
Since self-registration may not be open yet, you can use Mastodon's CLI tool tootctl inside the running container to bootstrap your admin account:
docker compose exec web tootctl accounts create yourusername \
--email=admin@example.com \
--confirmed \
--role=admin
This command will output a temporary password. Copy it immediately to sign in.
8. Reverse Proxy Routing (Nginx)
We will configure Nginx to proxy external HTTPS traffic to our internal Docker endpoints (3000 for Puma web, and 4000 for Node.js Streaming).
Create a server configuration block in /etc/nginx/sites-available/mastodon.conf:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream mastodon-web {
server 127.0.0.1:3000;
}
upstream mastodon-streaming {
server 127.0.0.1:4000;
}
server {
listen 80;
listen [::]:80;
server_name mastodon.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name mastodon.example.com;
# SSL Certificates (managed via Certbot/Let's Encrypt)
ssl_certificate /etc/letsencrypt/live/mastodon.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mastodon.example.com/privkey.pem;
# Security Hardening Headers
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers HIGH:!aNULL:!MD5;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
root /opt/mastodon/public;
client_max_body_size 99m;
# Proxy Web UI & REST API requests to Rails (Puma)
location / {
proxy_pass http://mastodon-web;
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;
proxy_buffering on;
proxy_redirect off;
}
# Proxy WebSocket connections to Streaming API
location /api/v1/streaming {
proxy_pass http://mastodon-streaming;
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;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 600s;
proxy_buffering off;
}
}
Enable the configuration and reload Nginx:
sudo ln -s /etc/nginx/sites-available/mastodon.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
9. Operations and Maintenance
A self-hosted Mastodon server requires basic operational maintenance to remain healthy and secure.
Automated Cleanup of Federated Media
Because federated instances share posts, your instance will cache media files from users on other servers. Clean up this storage automatically using a cron job.
Edit your cron schedule (crontab -e) and add:
0 3 * * * docker exec -it mastodon-web-1 tootctl media remove --days 7
This command will run daily at 3:00 AM, purging any cached remote media older than seven days. Since the references remain, if a user re-visits an old post, Mastodon will automatically fetch the media again.
Backing up your Database
Relational data is the heart of your server. Back it up regularly:
docker exec mastodon-db-1 pg_dump -U mastodon mastodon_production | gzip > /opt/mastodon/backups/db_backup_$(date +%F).sql.gz
Your Mastodon instance is now fully operational, configured for media offloading, and ready to federate with the global Mastodon network.