How to Install Mastodon on Ubuntu 24.04 — Self-Host a Federated Social Server
Running your own Mastodon server means owning your social identity, your moderation policies, and your data -- with no corporate algorithm deciding what your audience sees and no central authority able to suspend you without recourse. This guide walks you through deploying a production-grade Mastodon instance on an Ubuntu 24.04 VPS using Docker Compose, from first SSH connection to a federated server handing out @you@yourdomain handles across the fediverse.
Recommended Plan: Self-hosting Mastodon needs real resources -- Postgres, Redis, the Rails web container, a streaming Node process, and Sidekiq workers all run side by side. We recommend the CloudCore Professional VPS plan for single-admin instances with up to a few hundred active users.
Table of Contents
.env.production FileWhat is Mastodon?
Mastodon is a free, open-source, federated microblogging server. It speaks the ActivityPub protocol, which means any Mastodon server can exchange posts, follows, likes, and replies with any other ActivityPub server -- not just other Mastodon instances, but also Pleroma, Friendica, Misskey, Pixelfed, PeerTube, and every other piece of federated software in the fediverse. A user registered on your server can follow someone on mastodon.social, who can in turn follow a video creator on a PeerTube instance, who can boost a photo from a Pixelfed user -- all transparently, from whichever app they happen to use.
The architecture is deliberately decentralised. Each server (commonly called an instance) is independently administered. The admin controls moderation, federation rules, custom emoji, character limits, and privacy policy. Users get a handle that looks like an email address: @[email protected]. The combination of username plus domain makes every fediverse identity globally unique without needing a central registry.
Under the hood Mastodon is a Ruby on Rails application with a Node.js streaming service, backed by PostgreSQL for durable storage and Redis for feeds and background-job queues. Background work -- delivering posts out to thousands of followers across the network, pulling in remote media, sending email, generating preview cards -- runs in Sidekiq worker processes. Elasticsearch is optional and enables full-text search. For a single-admin server, the whole stack fits comfortably on one VPS.
Why Self-Host Federated Social?
Running your own Mastodon instance gives you things that no commercial social network can offer:
- No corporate control -- No shareholders, no ad inventory, no growth-hacked algorithm deciding your reach. Your timeline is strictly chronological. Your posts never get demoted for linking externally or for not hitting an engagement threshold.
- Your moderation, your rules -- You decide what's allowed, what gets boosted, and which remote servers your instance federates with. If an instance is full of spam or abuse, you can defederate it with a single admin action. If you want a stricter code of conduct than the default, write it.
- Own your identity -- Your handle is
@[email protected]. Nobody can revoke it, nobody can squat it, nobody can sell your name to someone else. If you ever migrate to different software (Pleroma, GoToSocial, Akkoma), you keep the domain and your followers can come with you. - Data sovereignty -- Every post, every follow, every DM lives on a database you control. You can back it up, encrypt it, inspect it, export it. No third party can mine your social graph or train a model on your DMs without your knowledge.
- No advertising, no tracking -- Mastodon has no ad system. There are no trackers, no pixels, no third-party scripts injected into your timeline. Your users' attention is not a product.
- Federation without lock-in -- Even though you host it yourself, your users are not isolated. They can follow and be followed by millions of fediverse users on thousands of other servers.
- Cost scales with you, not the platform -- A single VPS at a predictable monthly rate can host a personal, family, or small-community instance indefinitely. No "free tier" that changes terms, no sudden price hikes.
Self-Hosted vs. Hosted Mastodon vs. Commercial Social
| Factor | Self-Hosted Mastodon | masto.host / Hosted Mastodon | Twitter / Threads |
|---|---|---|---|
| Admin control | Full | Limited | None |
| Moderation rules | You write them | Provider terms apply | Platform decides |
| Custom domain | Yes | Yes | No |
| Ads / tracking | None | None | Yes |
| Data export | Full DB access | Export tool | Limited |
| Monthly cost | EUR ~19.99 VPS | EUR 9-90/mo hosted | "Free" (attention) |
| Federation | Full fediverse | Full fediverse | Walled garden |
Prerequisites
Before you start, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or sudo access and at least 6 vCPU, 12 GB RAM, 200 GB SSD
- A registered domain name pointed at your VPS IP (A record for
social.yourdomain.com, or whichever subdomain you pick) - Ports 80 and 443 open on your firewall
- Basic familiarity with SSH, Docker, and editing config files
- An SMTP provider for transactional email (Postmark, Amazon SES, Mailgun, Resend, or similar)
- Optional but strongly recommended: an S3-compatible object storage bucket (Contabo Object Storage, Wasabi, Backblaze B2, or AWS S3) for media
Recommended Plan: CloudCore Professional>
A moderately active single-admin instance with a few hundred users needs enough RAM for Postgres, Redis, the Rails web container, a Node streaming process, and two to three Sidekiq workers. The CloudCore Professional plan gives you:>
- 6 vCPU cores
- 12 GB RAM
- 200 GB NVMe SSD
- Unmetered bandwidth
- Ideal for single-admin or small-community Mastodon instances
Pick a subdomain before you start and set its A record to your VPS IP. In this guide we'll use social.example.com -- substitute your own throughout.
Connect to your server:
ssh root@your-server-ipStep 1: Update the System and Install Docker
Update the base system first:
sudo apt update && sudo apt upgrade -yInstall Docker Engine and the Compose plugin from Docker's official repository (the Ubuntu-packaged Docker is often outdated):
sudo apt install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \ sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpgecho "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Verify both are working:
docker --version
docker compose versionExpected output:
Docker version 27.3.1, build ce12230
Docker Compose version v2.29.7Enable Docker at boot:
sudo systemctl enable --now dockerStep 2: Create the Mastodon Directory and User
Mastodon containers run as UID 991 internally, so we need a matching directory layout with correct ownership to avoid permission issues.
sudo mkdir -p /opt/mastodon/{postgres,redis,public/system}
sudo chown -R 991:991 /opt/mastodon/public
cd /opt/mastodonThe public/system directory is where uploaded media lives if you're not using S3. Even if you plan to use S3 later, having it correctly owned prevents the container from failing on startup.
Step 3: Write the Docker Compose File
Create /opt/mastodon/docker-compose.yml with the core five services: Postgres, Redis, the Rails web app, the Node streaming service, and a Sidekiq worker.
sudo tee /opt/mastodon/docker-compose.yml > /dev/null <<'EOF' services: db: image: postgres:15-alpine restart: always shm_size: 256mb networks: - internal_network healthcheck: test: ['CMD', 'pg_isready', '-U', 'mastodon'] volumes: - ./postgres:/var/lib/postgresql/data environment: - POSTGRES_USER=mastodon - POSTGRES_DB=mastodon_production - POSTGRES_PASSWORD=CHANGE_ME_STRONG_DB_PASSWORDredis: image: redis:7-alpine restart: always networks: - internal_network healthcheck: test: ['CMD', 'redis-cli', 'ping'] volumes: - ./redis:/data
web: image: ghcr.io/mastodon/mastodon:v4.3 restart: always env_file: .env.production command: bundle exec puma -C config/puma.rb networks: - external_network - internal_network healthcheck: test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:3000/health || exit 1'] ports: - '127.0.0.1:3000:3000' depends_on: - db - redis volumes: - ./public/system:/mastodon/public/system
streaming: image: ghcr.io/mastodon/mastodon-streaming:v4.3 restart: always env_file: .env.production command: node ./streaming/index.js networks: - external_network - internal_network healthcheck: test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health || exit 1'] ports: - '127.0.0.1:4000:4000' depends_on: - db - redis
sidekiq: image: ghcr.io/mastodon/mastodon:v4.3 restart: always env_file: .env.production command: bundle exec sidekiq depends_on: - db - redis networks: - external_network - internal_network volumes: - ./public/system:/mastodon/public/system healthcheck: test: ['CMD-SHELL', "ps aux | grep '[s]idekiq\\ 6' || false"]
networks: external_network: internal_network: internal: true EOF
A few notes on this layout:
shm_size: 256mbon Postgres -- the default/dev/shmof 64 MB is too small for Postgres parallel workers on a moderately busy instance.networks: internal_networkondbandrediswithinternal: truemeans they have no outbound internet access and cannot be reached from outside the compose project. This is important defence-in-depth.ports: '127.0.0.1:3000'-- the web container is bound only to the loopback interface. Nginx (running on the host) will reverse-proxy to it over TLS. We never expose port 3000 or 4000 publicly.- Pinned tag
v4.3rather thanlatest-- upgrades are a deliberate act.
CHANGE_ME_STRONG_DB_PASSWORD now and remember what you set it to; you'll repeat it in .env.production.Step 4: Generate the .env.production File
Mastodon needs several cryptographic secrets before it will start. Use the official image to generate them -- this saves you from installing Ruby on the host.
Generate three secrets:
cd /opt/mastodondocker run --rm -e OTP_SECRET= -e SECRET_KEY_BASE= \ ghcr.io/mastodon/mastodon:v4.3 bundle exec rake secret
copy output -> SECRET_KEY_BASE
docker run --rm -e OTP_SECRET= -e SECRET_KEY_BASE= \ ghcr.io/mastodon/mastodon:v4.3 bundle exec rake secret
copy output -> OTP_SECRET
docker run --rm ghcr.io/mastodon/mastodon:v4.3 \ bundle exec rake mastodon:webpush:generate_vapid_key
copy output -> VAPID_PRIVATE_KEY and VAPID_PUBLIC_KEY
Now write the environment file:
sudo tee /opt/mastodon/.env.production > /dev/null <<'EOF'
Federation
LOCAL_DOMAIN=social.example.com
SINGLE_USER_MODE=false
ALTERNATE_DOMAINS=Secrets (paste the values you generated above)
SECRET_KEY_BASE=PASTE_SECRET_KEY_BASE_HERE
OTP_SECRET=PASTE_OTP_SECRET_HERE
VAPID_PRIVATE_KEY=PASTE_VAPID_PRIVATE_KEY_HERE
VAPID_PUBLIC_KEY=PASTE_VAPID_PUBLIC_KEY_HEREDeployment
RAILS_ENV=production
NODE_ENV=production
RAILS_SERVE_STATIC_FILES=true
RAILS_LOG_LEVEL=warnDatabase
DB_HOST=db
DB_PORT=5432
DB_USER=mastodon
DB_NAME=mastodon_production
DB_PASS=CHANGE_ME_STRONG_DB_PASSWORDRedis
REDIS_HOST=redis
REDIS_PORT=6379Limits
MAX_TOOT_CHARS=500
MAX_POLL_OPTIONS=4
MAX_POLL_OPTION_CHARS=50SMTP (filled in Step 9)
SMTP_SERVER=
SMTP_PORT=587
SMTP_LOGIN=
SMTP_PASSWORD=
[email protected]
SMTP_AUTH_METHOD=plain
SMTP_OPENSSL_VERIFY_MODE=peerS3 media (filled in Step 8)
S3_ENABLED=false
EOFTighten permissions -- this file holds every secret your instance has:
sudo chmod 600 /opt/mastodon/.env.productionThe three keys you generated do specific things:
SECRET_KEY_BASEis used by Rails to sign cookies and session tokens. Rotating it forcibly logs out every user and invalidates password-reset tokens.OTP_SECRETencrypts two-factor authentication secrets in the database. If you lose this, every user loses their 2FA device.VAPID_PRIVATE_KEY/VAPID_PUBLIC_KEYsign Web Push notifications so that browsers can verify they came from your server. Rotate them and existing push subscriptions break.
.env.production to somewhere safe (encrypted password manager, offline storage) immediately.Step 5: Bootstrap the Database and Admin
With the compose file and secrets in place, run the Mastodon setup rake tasks. Start just Postgres and Redis first so the web container can reach them:
cd /opt/mastodon
sudo docker compose up -d db redis
sleep 10Run the schema creation task:
sudo docker compose run --rm web bundle exec rake db:setupThis creates the schema, loads the initial seed data, and prepares the database. Expect output like:
Created database 'mastodon_production'
... (many migrations) ...
Seeded Mastodon defaultsIf you see a FATAL: password authentication failed message, double-check that DB_PASS in .env.production matches POSTGRES_PASSWORD in docker-compose.yml.
Now create the first admin user. Replace the username and email with your own:
sudo docker compose run --rm --no-deps web \
bin/tootctl accounts create admin \
--email [email protected] \
--confirmed \
--role OwnerExpected output:
OK
New password: 4d8f2a6b3c9e7f1a0b5c8d2e4f6a9b3cCopy that password immediately -- it's shown once, and logging in with it is how you claim the account. You'll change it at first login.
Step 6: Start Mastodon
Bring up the whole stack:
sudo docker compose up -dCheck that every container is healthy:
sudo docker compose psExpected output:
NAME IMAGE STATUS
mastodon-db-1 postgres:15-alpine Up (healthy)
mastodon-redis-1 redis:7-alpine Up (healthy)
mastodon-web-1 ghcr.io/mastodon/mastodon:v4.3 Up (healthy)
mastodon-streaming-1 ghcr.io/mastodon/mastodon-streaming:v4.3 Up (healthy)
mastodon-sidekiq-1 ghcr.io/mastodon/mastodon:v4.3 Up (healthy)Tail the logs to confirm there are no startup errors:
sudo docker compose logs -f webPress Ctrl+C to stop tailing. If the web container is restarting in a loop, sudo docker compose logs web | tail -100 is your friend.
The web app is now listening on 127.0.0.1:3000 and streaming on 127.0.0.1:4000. It is not yet reachable from the internet -- that's Nginx's job.
Step 7: Configure Nginx as a TLS Reverse Proxy
Install Nginx and Certbot:
sudo apt install -y nginx certbot python3-certbot-nginxCreate the site configuration:
sudo tee /etc/nginx/sites-available/mastodon > /dev/null <<'EOF' map $http_upgrade $connection_upgrade { default upgrade; '' close; }upstream backend { server 127.0.0.1:3000 fail_timeout=0; }
upstream streaming { server 127.0.0.1:4000 fail_timeout=0; }
proxy_cache_path /var/cache/nginx/mastodon levels=1:2 keys_zone=CACHE:10m inactive=7d max_size=1g;
server { listen 80; listen [::]:80; server_name social.example.com; root /opt/mastodon/public; location /.well-known/acme-challenge/ { allow all; } location / { return 301 https://$host$request_uri; } }
server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name social.example.com;
ssl_certificate /etc/letsencrypt/live/social.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/social.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains"; add_header X-Frame-Options DENY; add_header X-Content-Type-Options nosniff; add_header Referrer-Policy same-origin;
keepalive_timeout 70; sendfile on; client_max_body_size 99m;
root /opt/mastodon/public; gzip on; gzip_disable "msie6"; gzip_vary on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml image/x-icon;
location / { try_files $uri @proxy; }
location ~ ^/(emoji|packs|system/accounts/avatars|system/media_attachments/files) { add_header Cache-Control "public, max-age=31536000, immutable"; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains"; try_files $uri @proxy; }
location /sw.js { add_header Cache-Control "public, max-age=604800, must-revalidate"; try_files $uri @proxy; }
location @proxy { 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 https; proxy_set_header Proxy "";
proxy_pass_header Server; proxy_pass http://backend; proxy_buffering on; proxy_redirect off; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade;
proxy_cache CACHE; proxy_cache_valid 200 7d; proxy_cache_valid 410 24h; proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; add_header X-Cached $upstream_cache_status;
tcp_nodelay on; }
location /api/v1/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 https; proxy_set_header Proxy "";
proxy_pass http://streaming; proxy_buffering off; proxy_redirect off; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade;
tcp_nodelay on; }
error_page 500 501 502 503 504 /500.html; } EOF
sudo ln -s /etc/nginx/sites-available/mastodon /etc/nginx/sites-enabled/ sudo rm -f /etc/nginx/sites-enabled/default
Get a free Let's Encrypt certificate. Certbot will edit the HTTP block to solve the ACME challenge:
sudo certbot --nginx -d social.example.com \
--non-interactive --agree-tos -m [email protected]
sudo nginx -t && sudo systemctl reload nginxCertbot installs a systemd timer that renews certificates automatically every 60 days. Check it with sudo systemctl list-timers | grep certbot.
Now visit https://social.example.com in a browser. You should see the Mastodon landing page. Log in as admin with the one-time password from Step 5 and change it immediately from Settings -> Account.
Step 8: Configure S3-Compatible Media Storage
Storing every avatar, header, video, and preview card on your VPS disk is fine for a handful of users but becomes unmanageable at scale. Mastodon has first-class support for S3-compatible object storage through the Paperclip attachment library.
The configuration works with any S3-compatible provider: AWS S3, Contabo Object Storage, Wasabi, Backblaze B2, DigitalOcean Spaces, MinIO, and others.
Create a bucket on your provider, then create an access key with read/write permissions scoped to that bucket. Edit /opt/mastodon/.env.production:
sudo nano /opt/mastodon/.env.productionReplace the S3_ENABLED=false line with:
S3_ENABLED=true
S3_BUCKET=your-mastodon-bucket
S3_REGION=eu-central
S3_PROTOCOL=https
S3_HOSTNAME=eu2.contabostorage.com
S3_ENDPOINT=https://eu2.contabostorage.com
AWS_ACCESS_KEY_ID=YOUR_ACCESS_KEY
AWS_SECRET_ACCESS_KEY=YOUR_SECRET_KEYServe uploads directly from the bucket via its own domain
S3_ALIAS_HOST=media.social.example.comNotes on each variable:
S3_HOSTNAMEandS3_ENDPOINT-- the provider's API endpoint. Values above are Contabo Object Storage (EU region). For AWS it would bes3.eu-central-1.amazonaws.com.S3_REGION-- provider-specific; match exactly what the provider displays.S3_ALIAS_HOST-- a CNAME on your domain that points to the bucket's public URL. Using an alias host means URLs in federated posts look likehttps://media.social.example.com/...instead of exposing the provider. Set a CNAME in DNS pointingmedia.social.example.comto the bucket's public hostname, then add it to your Nginx config or the provider's custom-domain settings.
cd /opt/mastodon
sudo docker compose restart web sidekiqFrom now on every new upload lands in the S3 bucket. Existing local uploads continue to be served from public/system -- to migrate them, use tootctl media sync-storage inside the web container.
Step 9: Configure SMTP for Confirmation Emails
Mastodon sends email for account confirmation, password resets, notifications, and direct-message alerts. Deliverability matters -- messages that land in spam translate directly into signup drop-off. Use a reputable transactional email provider rather than trying to run your own MTA on the same VPS.
Popular choices:
- Postmark -- excellent deliverability, simple pricing
- Amazon SES -- cheapest at scale, requires sandbox-removal request
- Mailgun -- mature, good EU region
- Resend -- developer-friendly, modern dashboard
.env.production:SMTP_SERVER=smtp.postmarkapp.com
SMTP_PORT=587
SMTP_LOGIN=your-postmark-server-token
SMTP_PASSWORD=your-postmark-server-token
SMTP_FROM_ADDRESS='Mastodon <[email protected]>'
SMTP_AUTH_METHOD=plain
SMTP_OPENSSL_VERIFY_MODE=peer
SMTP_ENABLE_STARTTLS=autoRestart the containers that send mail:
sudo docker compose restart web sidekiqTest delivery by triggering a password-reset email for a test user, or use the built-in rake task:
sudo docker compose run --rm web bin/tootctl email_domain_blocks list sudo docker compose run --rm web bundle exec rails consoleinside the console:
UserMailer.confirmation_instructions(User.first, 'test').deliver_now
If email doesn't arrive, check your provider's message log first -- most show rejected messages with a reason (missing DKIM, bad From address, unverified domain).
Step 10: Media Cache Cleanup Cron
Every time someone on your instance views a remote post with an image, Mastodon downloads and caches that media locally (or in your S3 bucket). Over months this cache grows without bound -- hundreds of gigabytes is typical for an instance following active accounts on large servers. Mastodon ships a cleanup command that removes remote media older than N days.
Create a cron job that runs nightly:
sudo tee /etc/cron.d/mastodon-cleanup > /dev/null <<'EOF'Mastodon cache maintenance -- run at 04:00 UTC daily
0 4 * root cd /opt/mastodon && /usr/bin/docker compose run --rm web bin/tootctl media remove --days=14 >> /var/log/mastodon-cleanup.log 2>&1 30 4 * root cd /opt/mastodon && /usr/bin/docker compose run --rm web bin/tootctl preview_cards remove --days=30 >> /var/log/mastodon-cleanup.log 2>&1 0 5 0 root cd /opt/mastodon && /usr/bin/docker compose run --rm web bin/tootctl statuses remove --days=90 >> /var/log/mastodon-cleanup.log 2>&1 EOF
sudo chmod 644 /etc/cron.d/mastodon-cleanup sudo touch /var/log/mastodon-cleanup.log
What each line does:
media remove --days=14-- deletes cached copies of remote media older than 14 days. If someone scrolls back in an old thread, the media is re-fetched on demand.preview_cards remove --days=30-- removes Open Graph preview images for link cards older than 30 days.statuses remove --days=90-- removes cached copies of remote posts older than 90 days that no local user has interacted with. Runs weekly on Sundays because it's expensive.
--days=7 for media; a quiet one can keep 30 days without trouble.Post-Install: First Login and Federation Check
With everything running, give your instance a quick end-to-end test.
Finish admin setup: log in at https://social.example.com as admin, change the password, enable 2FA (Settings -> Account -> Two-factor authentication), and fill in the site metadata (Preferences -> Administration -> Server Settings: name, description, contact, rules, privacy policy).
Verify federation: from your admin account, follow @[email protected] (Eugen Rochko, Mastodon's founder). Within a few seconds his profile should load and his posts should start appearing in your home timeline. If federation is broken, posts never arrive.
Check Sidekiq health: go to https://social.example.com/sidekiq (logged in as Owner) and confirm the default, push, pull, mailers, ingress, and scheduler queues all have recent processed jobs and no stuck retries.
Run the built-in self-check:
sudo docker compose run --rm web bin/tootctl self-destruct --help
just to verify the binary runs; don't actually self-destruct
sudo docker compose run --rm web bin/tootctl feeds buildTroubleshooting
| Problem | Cause | Solution |
|---|---|---|
Web container crashes on startup with SECRET_KEY_BASE must be set | Secrets not generated or .env.production not readable | Re-run the rake secret commands in Step 4, paste into .env.production, chmod 600, restart |
502 Bad Gateway from Nginx | web or streaming container not running | sudo docker compose ps to check, then sudo docker compose logs web |
| Posts from other servers never appear | Firewall blocking outbound 443, or DNS misconfigured | Test: sudo docker compose exec sidekiq wget -qO- https://mastodon.social/.well-known/nodeinfo. Check Sidekiq pull queue for retries |
| Media uploads return 500 error | S3 credentials wrong, bucket policy blocks uploads, or S3_HOSTNAME wrong | Test credentials with aws s3 ls s3://bucket using the same keys. Check web container logs during upload |
| Emails never arrive | SPF/DKIM/DMARC not set, domain unverified with provider | Check provider message log. Verify DNS records with dig TXT social.example.com. Send from notifications@your-verified-domain, not gmail.com |
| Sidekiq queue builds up, posts slow to federate | Not enough Sidekiq worker processes | Scale: sudo docker compose up -d --scale sidekiq=3. Ensure RAM headroom |
| Disk fills up | Media cache not being pruned | Run bin/tootctl media remove --days=7 manually, verify cron job in /etc/cron.d/mastodon-cleanup is present |
Your connection is not private at first visit | Certbot hasn't run, or DNS not propagated | Confirm A record resolves to VPS IP, re-run sudo certbot --nginx -d social.example.com |
Viewing Logs
Follow all container logs:
cd /opt/mastodon
sudo docker compose logs -fFollow one service:
sudo docker compose logs -f sidekiqNginx access and error logs:
sudo tail -f /var/log/nginx/access.log /var/log/nginx/error.logFAQ
How much RAM and disk does a Mastodon instance really need?
A personal single-user instance runs comfortably on 4 GB RAM and 40 GB disk, but you'll hit memory pressure fast once federation picks up -- a handful of follows to busy accounts pulls in thousands of daily posts and their cached media. For a single-admin server with up to ~200 active users, plan for 12 GB RAM and 200 GB disk with S3 offload, or 500 GB+ if you keep media on local storage. Postgres and Redis are the memory-hungry services; Sidekiq workers scale horizontally with traffic.
Is Mastodon the only option, or should I look at Pleroma, Misskey, or GoToSocial?
Mastodon is the most polished and widely-adopted ActivityPub server, but it's not the only choice. Misskey and its forks (Sharkey, Firefish) have richer UIs with emoji reactions and more customisation, at the cost of higher resource usage. Pleroma and Akkoma are much lighter on RAM (~1 GB) and work well on small VPS plans but have smaller communities and fewer admin tools. GoToSocial is a single-binary Go implementation aimed at tiny instances -- perfect for a personal-use server. All of them federate with each other; your choice affects your experience, not who you can follow. Mastodon is the safest default for anyone hosting for others.
Can I federate with Pixelfed and PeerTube from my Mastodon server?
Yes -- that's the point of the fediverse. Any account on any ActivityPub server can be followed from Mastodon. Someone posting photos on Pixelfed, videos on PeerTube, or long-form writing on WriteFreely appears in your home timeline like any other Mastodon user. Replies and boosts flow in both directions. The UX varies -- Pixelfed videos play inline in Mastodon, PeerTube videos embed a player -- but the underlying follow-graph is shared.
Do I need Elasticsearch?
No. Elasticsearch enables full-text search of your own toots and toots from accounts that opt into discoverability. Without it, search is limited to usernames, hashtags, and URLs. Running Elasticsearch adds 1-2 GB of RAM usage and non-trivial ops burden. Most single-admin instances skip it until users specifically request full-text search.
How do I back up my instance?
Two things matter: the Postgres database and media storage. For the database, run pg_dump nightly from outside the container: sudo docker compose exec -T db pg_dump -U mastodon mastodon_production | gzip > /backups/mastodon-$(date +%F).sql.gz. Keep 30 days, encrypted, off the server. For media, if you use S3 the provider handles durability; if you use local storage, rsync /opt/mastodon/public/system to another machine or object storage. Also back up .env.production -- losing it means losing every user's 2FA seed. Test restores quarterly.
How do I upgrade Mastodon to a new version?
First, read the release notes at github.com/mastodon/mastodon/releases -- some versions require a db:migrate, and major versions sometimes add new secrets or environment variables. Back up the database. Update the image tags in docker-compose.yml from, say, v4.3 to v4.4, then:
cd /opt/mastodon
sudo docker compose pull
sudo docker compose run --rm web bundle exec rake db:migrate
sudo docker compose up -dAlways pin to a specific minor version rather than latest. If the upgrade fails, roll back by restoring the previous database dump and changing the image tag back.
How do I migrate from another Mastodon instance without losing followers?
Mastodon supports account migration natively. On your old account, set an "alias" pointing at your new @[email protected] handle (Preferences -> Account -> Account aliases). Then on your new account, trigger the move (Preferences -> Account -> Move to a different account). Your followers are notified and followed automatically on the new account within a few hours. Your old posts don't move -- ActivityPub has no mechanism for that -- but your follow graph does.
Next Steps
Now that your Mastodon server is running, here are common follow-up tasks:
- Add custom emoji -- Preferences -> Administration -> Custom Emojis. Upload PNGs or animated GIFs. Instance-specific emoji are one of the most loved features of fediverse culture.
- Write server rules and a privacy policy -- Preferences -> Administration -> Server Settings. Good rules reduce moderation load later. Link to your plan and Acceptable Use Policy.
- Configure federation filters -- block known-bad servers proactively. The oliphant.social blocklist aggregates widely-accepted bans.
- Deploy Grafana + Prometheus monitoring -- track Sidekiq queue depth, Postgres connection count, Redis memory, and HTTP latency. A stuck
pushqueue is the earliest sign of federation trouble. - Install a backup agent -- Restic or Borgbackup with encrypted off-site storage. Schedule nightly full database dumps and weekly media sync.
- Deploy Misskey or Pixelfed alongside -- run a Pixelfed instance on a different subdomain for photos, or a PeerTube instance for video. They all federate with each other and your Mastodon server.
- Read the official admin docs -- docs.joinmastodon.org is comprehensive and updated with every release. Bookmark the Admin CLI reference.
Self-Hosting the Fediverse>
A Professional VPS gives you the headroom for Mastodon, plus room to add Pixelfed or PeerTube on the same server later. Full root, predictable billing, EU-hosted -- everything you need to own your social presence.