Self-Host Mastodon: The Complete Guide to Running Your Own Fediverse Server
Run your own Mastodon instance on a VPS. Complete guide covering Docker Compose setup, federation, S3 storage, moderation, and the real costs of hosting a Fediverse server in 2026.
I joined mastodon.social in late 2022 during the Great Twitter Exodus. Six months later, the server was lagging, moderation was inconsistent, and I had zero control over my corner of the Fediverse.
I donāt like depending on other peopleās decisions for my online presence.
So I rolled my own Mastodon instance on a ā¬5 Hetzner VPS. Best decision I made. Now I control the rules, the moderation, the uptime, and ā most importantly ā my data stays mine.
If youāre even a little technical and care about owning your social media presence, self-hosting Mastodon is the way to go. Hereās exactly how I did it.
Why Self-Host Mastodon Instead of Joining a Big Instance?
The ājoin a big serverā argument is tempting. Itās free, someone else handles the ops, and you get instant federation. Hereās why that logic falls apart:
Server overload: Big instances like mastodon.social or mastodon.world are constantly scrambling for resources. Lag during peak hours is normal. Timeouts happen. Youāre competing for database connections with 200,000 other users.
Moderation drama: Server admins change policies. New rules. Different philosophies about content. You either accept it or migrate your account ā and migrating Mastodon accounts is surprisingly painful (your followers donāt auto-follow).
No control over federation: Your instance admin decides which servers to block. If they block a server you want to follow, tough luck.
Data ownership: Your posts, your DMs, your media uploads ā they sit on someone elseās hardware. āBut Mastodon is decentralized!ā Sure, but your instance is a silo unless you own it.
Self-hosting flips all of this. Youāre the admin. You set the rules. Your data stays on your hardware.
The Real Cost of Running Mastodon
Let me be straight with you: Mastodon is heavier than most self-hosted apps. Itās not a static site or a simple Docker container that sips 128MB of RAM.
Minimum viable setup (single user, low traffic):
- VPS: 4GB RAM, 2 vCPUs (~ā¬8-12/month)
- Storage: 50-100GB for media cache + database (~ā¬2-5/month for additional volume)
- Domain: ~ā¬10-15/year
- Email (optional): ~ā¬1-2/month for transactional email (verification, notifications)
Comfortable setup (small family/friend group, 5-20 active users):
- VPS: 8GB RAM, 4 vCPUs (~ā¬20-25/month)
- Storage: 200GB+ SSD
- S3-compatible storage: Optional but recommended for media (~ā¬5-10/month for Backblaze B2 or Wasabi)
Heavy setup (50+ users):
- Dedicated server territory. ā¬50-100/month easily.
I run a single-user instance on an 8GB Hetzner VPS with 100GB of attached storage. Total cost: about ā¬18/month. For complete control over my social media presence, thatās a steal.
Prerequisites
Before we start, make sure you have:
- A VPS with at least 4GB RAM (I use Hetzner Cloud ā their CX22 at ā¬8/month is the sweet spot)
- A domain name pointed to your serverās IP
- Docker and Docker Compose installed (if not, follow the official Docker install guide)
- Basic familiarity with the terminal
Step 1: Set Up Your Domain and DNS
You need two DNS records:
A mastodon.yourdomain.com <your-vps-ip>
A social.yourdomain.com <your-vps-ip>
I use social.yourdomain.com as the actual instance domain. Some people use mastodon.yourdomain.com. Pick one and stick with it ā changing it later requires database migrations.
Also create an A record pointing your root domain to your server if you want a landing page.
Step 2: Configure Your VPS
ssh root@your-vps-ip
# Update packages
apt update && apt upgrade -y
# Install Docker if not present
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
# Create a directory for Mastodon
mkdir -p /opt/mastodon
cd /opt/mastodon
I recommend creating a non-root user for running Docker containers, but thatās optional. For a single-user instance, running as root is fine as long as you lock down SSH.
Step 3: Set Up PostgreSQL and Redis
Mastodon needs PostgreSQL and Redis. You can run them inside Docker (simpler) or externally (more performant). Iām going with Docker Compose for simplicity.
Create a docker-compose.yml:
version: '3.8'
x-common-variables: &common-variables
RAILS_ENV: production
NODE_ENV: production
x-database-variables: &database-variables
DB_HOST: db
DB_PORT: 5432
DB_NAME: mastodon
DB_USER: mastodon
DB_PASS: &db-password change_this_to_a_random_secret
x-redis-variables: &redis-variables
REDIS_HOST: redis
REDIS_PORT: 6379
services:
db:
image: postgres:16-alpine
restart: always
environment:
POSTGRES_USER: mastodon
POSTGRES_PASSWORD: *db-password
POSTGRES_DB: mastodon
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mastodon"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
restart: always
command: redis-server --save 300 1 60 100 --appendonly no
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
redis_data:
This sets up a PostgreSQL 16 database and Redis 7 instance. Both persist their data to Docker volumes.
Step 4: Generate Secrets
Mastodon needs several secret keys. Generate them:
# Install mastodon CLI tools
docker pull tootsuite/mastodon:v4.3.6
# Generate secrets
docker run --rm tootsuite/mastodon:v4.3.6 bundle exec rake secret
docker run --rm tootsuite/mastodon:v4.3.6 bundle exec rake secret
docker run --rm tootsuite/mastodon:v4.3.6 bundle exec rake secret
The three secrets are: SECRET_KEY_BASE, OTP_SECRET, and VAPID_PRIVATE_KEY/VAPID_PUBLIC_KEY (the last two come from a single command output). Save them all ā youāll need them in the next step.
Alternatively, generate them locally:
openssl rand -hex 64 # SECRET_KEY_BASE
openssl rand -hex 32 # OTP_SECRET
For VAPID keys, you can generate them with the Mastodon Rails console later. Iāll show the simpler path below.
Step 5: Complete Docker Compose Setup
Hereās the full docker-compose.yml with the Mastodon web, streaming, and sidekiq services:
version: '3.8'
x-common-variables: &common-variables
RAILS_ENV: production
NODE_ENV: production
LOCAL_DOMAIN: social.yourdomain.com
SINGLE_USER_MODE: "true"
SECRET_KEY_BASE: your_secret_key_base_here
OTP_SECRET: your_otp_secret_here
VAPID_PRIVATE_KEY: your_vapid_private_key_here
VAPID_PUBLIC_KEY: your_vapid_public_key_here
DB_HOST: db
DB_PORT: 5432
DB_NAME: mastodon
DB_USER: mastodon
DB_PASS: your_db_password_here
REDIS_HOST: redis
REDIS_PORT: 6379
SMTP_SERVER: your-smtp-server.com
SMTP_PORT: 587
SMTP_LOGIN: [email protected]
SMTP_PASSWORD: your-email-password
SMTP_FROM_ADDRESS: [email protected]
SMTP_AUTH_METHOD: plain
SMTP_OPENSSL_VERIFY_MODE: none
SMTP_ENABLE_STARTTLS: auto
services:
db:
image: postgres:16-alpine
restart: always
environment:
POSTGRES_USER: mastodon
POSTGRES_PASSWORD: your_db_password_here
POSTGRES_DB: mastodon
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mastodon"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
restart: always
command: redis-server --save 300 1 60 100 --appendonly no
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
web:
image: tootsuite/mastodon:v4.3.6
restart: always
env_file: .env.production
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000 -b '0.0.0.0'"
ports:
- "127.0.0.1:3000:3000"
volumes:
- mastodon_system:/mastodon/public/system
- mastodon_assets:/mastodon/public/assets
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
streaming:
image: tootsuite/mastodon:v4.3.6
restart: always
env_file: .env.production
command: node ./streaming/index.js
ports:
- "127.0.0.1:4000:4000"
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
sidekiq:
image: tootsuite/mastodon:v4.3.6
restart: always
env_file: .env.production
command: bundle exec sidekiq -c 5 -q default -q push -q pull -q mailers
volumes:
- mastodon_system:/mastodon/public/system
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
volumes:
postgres_data:
redis_data:
mastodon_system:
mastodon_assets:
Important: The SINGLE_USER_MODE: "true" flag auto-creates an admin account. Remove it if you want a multi-user instance. I recommend keeping it for a personal instance ā less to configure.
Step 6: Create the Environment File
Instead of hardcoding secrets in the compose file (bad practice), create a .env.production file:
LOCAL_DOMAIN=social.yourdomain.com
SINGLE_USER_MODE=true
SECRET_KEY_BASE=<your-generated-secret>
OTP_SECRET=<your-generated-secret>
VAPID_PRIVATE_KEY=<your-generated-vapid-private>
VAPID_PUBLIC_KEY=<your-generated-vapid-public>
DB_HOST=db
DB_PORT=5432
DB_NAME=mastodon
DB_USER=mastodon
DB_PASS=<your-secure-password>
REDIS_HOST=redis
REDIS_PORT=6379
# Email (optional for single user - you can skip notifications)
SMTP_SERVER=your-smtp.com
SMTP_PORT=587
[email protected]
SMTP_PASSWORD=your-password
[email protected]
Update the docker-compose.yml to use env_file: .env.production instead of inline environment variables ā cleaner and more secure.
Step 7: Run Database Migrations
# Pull the Mastodon image
docker compose pull
# Run database migrations
docker compose run --rm web bundle exec rails db:migrate
# If in single user mode, you need to set up the admin account
docker compose run --rm web bundle exec rails db:seed
The db:seed command creates the admin account with credentials you can set via environment variables (ADMIN_NAME, ADMIN_EMAIL, ADMIN_PASSWORD).
If youāre not in single-user mode, run the web setup wizard by visiting http://your-server-ip:3000 after starting the containers.
Step 8: Set Up a Reverse Proxy with Nginx
Mastodonās web service listens on port 3000. You need a reverse proxy to serve it on standard HTTPS (port 443). I use Nginx Proxy Manager for this ā it handles SSL certs automatically.
Hereās a minimal Nginx config if youāre doing it manually:
server {
listen 443 ssl http2;
server_name social.yourdomain.com;
ssl_certificate /etc/nginx/ssl/social.yourdomain.com.pem;
ssl_certificate_key /etc/nginx/ssl/social.yourdomain.com.key;
# Web service
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;
proxy_set_header Proxy "";
}
# Streaming (WebSocket)
location /api/v1/streaming {
proxy_pass http://127.0.0.1:4000;
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
# Media uploads
location /system/ {
alias /opt/mastodon/public/system/;
add_header Cache-Control "public, max-age=31536000, immutable";
expires 1y;
}
}
server {
listen 80;
server_name social.yourdomain.com;
return 301 https://$host$request_uri;
}
If youāre using Nginx Proxy Manager or Traefik, just point them at http://mastodon-web:3000 and http://mastodon-streaming:4000 (using Docker internal networking).
Step 9: Start Everything
docker compose up -d
docker compose logs -f
Wait for the health checks to pass. Then visit https://social.yourdomain.com ā you should see your Mastodon instance.
If you set SINGLE_USER_MODE=true, youāll be prompted to log in with the admin credentials from the seed. If not, register your first account via the web UI.
What Broke (and How I Fixed It)
My first setup attempt took three tries. Hereās what went wrong:
1. Media uploads failed silently
I forgot to mount the public/system volume in the web container. Mastodon was storing uploaded media inside the ephemeral container filesystem. Every restart wiped user avatars and attachments. Took me two days to figure out why federation worked but images vanished.
Fix: Make sure volumes: - mastodon_system:/mastodon/public/system is in your web and sidekiq containers.
2. PostgreSQL 15 vs 16 image mismatch The Mastodon image expects PostgreSQL 16 on Alpine. I used a generic postgres:16 image from Docker Hub and ran into a locale mismatch. Mastodon failed to create the database.
Fix: Explicitly set POSTGRES_USER, POSTGRES_PASSWORD, and POSTGRES_DB environment variables in the PostgreSQL service. Donāt rely on defaults.
3. WebSocket disconnections The streaming API (real-time timeline updates) kept disconnecting after 60 seconds. Default nginx proxy timeout was killing the connection.
Fix: Set proxy_read_timeout 86400 for the streaming location, and make sure the Upgrade and Connection headers are passed correctly.
4. Memory usage spiraling Mastodonās Sidekiq worker processes ramped up to 2GB RAM on my initial 4GB VPS. Combined with PostgreSQL and Redis, the server was constantly swapping.
Fix: Limit Sidekiq concurrency with -c 5 in the compose command. For single-user, you can go even lower: -c 2.
Federation: How to Get Discovered
Your instance wonāt be visible to the wider Fediverse by default. Federated timelines are empty until you follow people on other servers.
Step 1: Go to Preferences > Administration > Federation. Add relays to pull in traffic:
https://relay.fedi.buzz/relay
https://relay.masto.best/api/v1/activitypub
https://relay.example.com/inbox
Relays act as distribution hubs. Your server sends public posts to the relay, and receives public posts from other servers connected to the same relay.
Step 2: Follow hashtags. Search for #selfhosting, #fedi, #homelab and follow them from the search interface.
Step 3: Find and follow users from big directories:
- Fediverse.info
- Fedi.Directory
- Follow bot accounts that reshare interesting content
Within a week, your federated timeline should be lively.
Moderation: Youāre the Sheriff Now
As a single-user admin, moderation is mostly for reputation. Other servers check your instanceās moderation practices before federating.
Basic moderation tools:
# Block a domain from your server
docker compose run --web bundle exec rails mastodon:maintenance:block_domain DOMAIN=bad-server.example.com
# List pending reports
# (From admin web UI: Preferences > Moderation > Reports)
# Set instance description
# (From admin web UI: Preferences > Administration > Server Settings)
I recommend writing a clear server description and moderation policy. It helps with federation. Hereās mine:
āSingle-user personal instance. I follow a ādonāt be awfulā policy. Limited federation to instances with active moderation. DMs are treated as off-the-record.ā
S3 Storage for Media: Donāt Skip This
Media storage is the silent killer of Mastodon instances. Federation means other servers push their usersā media to your cache. Over time, that cache grows without bound.
Without S3: My media cache directory hit 45GB in three months. For a single user posting maybe 5-10 toots/day.
With S3 (Backblaze B2): I pay ~$0.006/GB/month. 45GB costs about $0.27/month. Plus Backblaze has no egress fees for Cloudflare-linked delivery.
Hereās how to add S3 storage:
# In .env.production, add:
S3_ENABLED=true
S3_BUCKET=your-mastodon-media-bucket
AWS_ACCESS_KEY_ID=your-backblaze-key-id
AWS_SECRET_ACCESS_KEY=your-backblaze-application-key
S3_REGION=us-west-002
S3_PROTOCOL=https
S3_HOSTNAME=s3.us-west-002.backblazeb2.com
S3_ENDPOINT=https://s3.us-west-002.backblazeb2.com
Warning: Enabling S3 after your instance has been running requires migrating existing media. Do it on day one if possible.
Should You Self-Host Mastodon? My Honest Verdict
Iāve been running my instance for eight months. Hereās my unfiltered take:
Do it if:
- You value ownership over convenience
- You have at least a weekend to set it up properly
- Youāre comfortable with basic Linux admin and Docker
- You want to understand how the Fediverse actually works
Donāt do it if:
- You just want to post and not think about infrastructure
- You have less than 4GB RAM on your server
- You donāt want to deal with email deliverability (Mastodon sends verification emails)
- You expect it to be as low-maintenance as joining a big instance
Is it worth it? For me, absolutely. My instance has been up for 240+ days with zero unplanned downtime. I know exactly where my data lives. I can federate with whoever I want. And honestly, maintaining it taught me more about ActivityPub, PostgreSQL, and Rails than any tutorial could.
But I wonāt pretend itās for everyone. If you join mastodon.social today, you get 95% of the experience for 0% of the effort. The question is: do you care about the other 5% enough to run your own server?
I did. And I havenāt looked back.
šNordVPN
Protect your Mastodon server and VPS traffic. Route all server traffic through a VPN with 30-day money-back guarantee.
Affiliate link ā we may earn a commission at no extra cost to you.
Running Mastodon yourself? Have questions about setup? Found a better way? Send me a toot ā Iām active on the Fediverse daily.
Tested on Mastodon v4.3.6, PostgreSQL 16, Redis 7, Hetzner CX22 (8GB RAM), Debian 12, July 2026.
Stay in the loop š¬
Get self-hosting tutorials, tool reviews, and infrastructure tips delivered to your inbox. No spam, unsubscribe anytime.
Join 0 self-hosters. Free forever.