Skip to main content

Self-Host Mastodon with Docker Compose

Mastodon is five processes, not one. A Rails web server (Puma), a Node.js streaming server for live timelines, Sidekiq for background jobs, PostgreSQL and Redis. Sidekiq delivers every post to other servers, so without it nothing federates. Mastodon also needs HTTPS on a real domain, and that domain (LOCAL_DOMAIN) is permanent: it goes into every account handle and post URL, and changing it later breaks federation.

Before using the one-click template, here is a minimal Docker Compose example for self-hosting Mastodon.

If you're new to Docker Compose, check out our guide on how to self-host a Docker Compose app. More stacks are in our Docker Compose library.

Self-host Mastodon with Docker Compose (minimal)​

services:
db:
image: postgres:14-alpine
shm_size: 256mb
environment:
POSTGRES_DB: mastodon_production
POSTGRES_USER: mastodon
POSTGRES_PASSWORD: changeme
volumes:
- pgdata:/var/lib/postgresql/data

redis:
image: redis:7-alpine
volumes:
- redisdata:/data

web:
image: ghcr.io/mastodon/mastodon:v4.5.9
env_file: .env.production
command: bundle exec puma -C config/puma.rb
ports:
- "127.0.0.1:3000:3000"
volumes:
- system:/mastodon/public/system
depends_on:
- db
- redis

streaming:
image: ghcr.io/mastodon/mastodon-streaming:v4.5.9
env_file: .env.production
command: node ./streaming/index.js
ports:
- "127.0.0.1:4000:4000"
depends_on:
- db
- redis

sidekiq:
image: ghcr.io/mastodon/mastodon:v4.5.9
env_file: .env.production
command: bundle exec sidekiq
volumes:
- system:/mastodon/public/system
depends_on:
- db
- redis

volumes:
pgdata:
redisdata:
system:

And the .env.production next to it:

LOCAL_DOMAIN=social.example.com

DB_HOST=db
DB_PORT=5432
DB_NAME=mastodon_production
DB_USER=mastodon
DB_PASS=changeme
REDIS_HOST=redis
REDIS_PORT=6379

SECRET_KEY_BASE=
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=
VAPID_PRIVATE_KEY=
VAPID_PUBLIC_KEY=

SMTP_SERVER=smtp.example.com
SMTP_PORT=587
SMTP_LOGIN=
SMTP_PASSWORD=
SMTP_FROM_ADDRESS=notifications@example.com

This follows the upstream docker-compose.yml and .env.production.sample, without the healthchecks and internal networks. Both images already set BIND=0.0.0.0, and the web image serves static files itself. Upstream runs PostgreSQL with trust auth; this example uses a password instead.

Generate the empty values once, before the first start:

docker compose run --rm web bundle exec rails secret
docker compose run --rm web bin/rails db:encryption:init
docker compose run --rm web bundle exec rails mastodon:webpush:generate_vapid_key

The first command prints the value for SECRET_KEY_BASE. The second prints the three ACTIVE_RECORD_ENCRYPTION_* keys. Never change those three after data exists, or encrypted columns can no longer be read. The third prints the VAPID pair for web push notifications.

Then create the database schema and start everything:

docker compose run --rm web bundle exec rails db:setup
docker compose up -d

Put a reverse proxy with TLS in front, such as nginx or Caddy. Send /api/v1/streaming to port 4000 and everything else to port 3000, on the same domain. The proxy must pass the original Host header, set X-Forwarded-Proto: https, and forward WebSocket upgrades. Mastodon in production redirects every HTTP request to HTTPS and rejects unknown hosts with a 403, so plain HTTP on localhost does not work.

Without SMTP, sign-up confirmation emails never arrive. Create the first account from the command line instead:

docker compose exec web bin/tootctl accounts create admin \
--email you@yourdomain.com --confirmed --approve --role Owner

It prints a generated password. Keep --approve. Without it the account stays pending review and every page redirects to the account settings.


Deploy Mastodon on Hostim.dev (one-click)​

Mastodon is a federated social network. Your server talks to every other Mastodon, GoToSocial, Pixelfed or Misskey server over ActivityPub. On Hostim.dev the template runs the official web and streaming images against a managed PostgreSQL database and a managed Redis, mounts a volume for uploaded media, generates all Rails secrets, and runs the database migrations on every start.

🐘 Your own Mastodon server on EU servers, with HTTPS and no nginx to configure.

Try it Yourself

Guest project runs for 1 hour. Log in to save and extend to 5 days.

Why host Mastodon on Hostim.dev?​

  • One-click Docker deployment, no server setup
  • Managed PostgreSQL and Redis, both wired in
  • Persistent volume for uploaded media and avatars
  • Rails secret key and encryption keys generated at deploy time
  • Automatic HTTPS for the web and streaming servers
  • Health checks, so no traffic reaches Puma before it is ready
  • Real-time logs and metrics

What's included​

ResourceDetails
Web appghcr.io/mastodon/mastodon:v4.5.9, Puma and Sidekiq, port 3000
Streamingghcr.io/mastodon/mastodon-streaming:v4.5.9, port 4000
DatabaseManaged PostgreSQL
RedisManaged Redis
Volume/mastodon/public/system (uploaded media)
DomainFree *.hostim.dev subdomain for each app
SSLLet's Encrypt (auto-enabled)

Sidekiq runs inside the mastodon-web container, next to Puma. That keeps all Rails secrets in one app. The web app gets 2 GB of RAM; a small instance uses about 1 GB.

How to Deploy​

  1. Go to your Hostim.dev dashboard.
  2. Click Create Project β†’ Use a Template.
  3. Select Mastodon.
  4. Choose a resource plan.
  5. Deploy.

First steps after deploy​

  1. Decide on the domain now. To use your own domain, add it to mastodon-web under Networking β†’ Domains, set LOCAL_DOMAIN to it and redeploy. Do this before you create any account.

  2. Connect the streaming server. Copy the domain of the mastodon-streaming app. On mastodon-web, set STREAMING_API_BASE_URL to wss://<that-domain> and redeploy. Without this, timelines do not update live.

  3. Create your admin account. Open a shell into the web app with hostim exec mastodon-web and run:

    RAILS_ENV=production bin/tootctl accounts create admin \
    --email you@yourdomain.com --confirmed --approve --role Owner

    It prints the password. Mastodon checks that the email domain can receive mail, so example.com is rejected.

  4. Set the contact account. Log in, open Administration β†’ Server settings β†’ Branding and set Contact username to your admin. Otherwise the About page links to @undefined.

  5. Add SMTP if other people will sign up. Set SMTP_SERVER, SMTP_PORT, SMTP_LOGIN, SMTP_PASSWORD and SMTP_FROM_ADDRESS on mastodon-web and redeploy. Until then, create accounts with tootctl.

  6. Limit the media cache. Your server stores copies of remote media. Set Media cache retention period under Administration β†’ Server settings β†’ Content retention, for example to 14 days. Upstream recommends at least 14, or link previews stop refreshing.


Frequently asked questions

What does Mastodon need to run in Docker?

PostgreSQL, Redis, and three processes from the Mastodon images: the Puma web server, the Node.js streaming server and Sidekiq. It also needs HTTPS on a public domain, and SMTP if people sign up through the web form. The compose file in the section above has all five services.

Why does my Mastodon timeline not update live?

The browser cannot reach the streaming server. Upstream expects a reverse proxy that sends /api/v1/streaming on the main domain to the streaming container on port 4000. If the streaming server runs on its own domain, as it does on Hostim, set STREAMING_API_BASE_URL on the web app to wss://<streaming-domain> and restart it. You can test the server with curl https://<streaming-domain>/api/v1/streaming/health, which returns OK.

How do I create an admin account on Mastodon without email?

Run RAILS_ENV=production bin/tootctl accounts create <username> --email <address> --confirmed --approve --role Owner inside the web container. It prints a password. --confirmed skips the email confirmation and --approve skips the review queue. The email domain must still resolve in DNS; example.com is rejected because it declares that it accepts no mail.

Can I change LOCAL_DOMAIN after my Mastodon server is running?

No. LOCAL_DOMAIN is part of every account handle and every post URL that other servers have stored. Changing it breaks federation for existing accounts. If you want handles like @you@example.com while Mastodon runs on social.example.com, set LOCAL_DOMAIN=example.com and WEB_DOMAIN=social.example.com before the first account, and redirect /.well-known/webfinger on example.com to the Mastodon host.

Why must Sidekiq run for Mastodon?

Sidekiq does all background work: delivering your posts to followers on other servers, fetching remote posts and media, sending email and processing uploads. Without it the web UI works but nothing federates. Check the queue at /sidekiq when logged in as an admin.

How much RAM does a small Mastodon server need?

Plan for about 2 GB for the web server and Sidekiq together, plus a few hundred MB for the streaming server. A fresh single-user instance on Hostim used about 1 GB in the web container. Media storage grows with the remote content your users follow, so set a media cache retention period.

How do I update Mastodon in Docker?

Read the release notes first; some releases need extra steps. Then change the image tag, run bundle exec rails db:migrate, and restart all services. On Hostim, change the image tag on both apps and redeploy; the web app runs db:prepare on every start, which applies pending migrations. Back up the database before a major version.


Alternatives​

  • GoToSocial β€” a single Go binary with SQLite, far lighter, works with Mastodon clients
  • Akkoma β€” Elixir fork of Pleroma, lighter than Mastodon with more customisation
  • Misskey β€” ActivityPub server with reactions, drive storage and a different UI

Source + Docs​


Looking for something else? Browse all templates β†’


Try it now​

Deploy Mastodon Now – in less than 60 seconds