What You'll End Up With
After following this guide you will have a production-ready self-hosted Mastodon instance setup running on Ubuntu 24.04, backed by PostgreSQL 16, Redis 7, and served over HTTPS via Nginx with a Let's Encrypt certificate. Object storage is handled by a local directory (swap in S3-compatible storage later). Estimated VPS cost: a Hetzner CX22 (2 vCPU, 4 GB RAM) at roughly €4/month handles a small community comfortably.
Prerequisites
- Ubuntu 24.04 LTS server with a public IP
- A domain name with an A record pointing to that IP (e.g.,
social.example.com) - Root or sudo access
- Port 80 and 443 open in your firewall
- SMTP credentials (Postmark, Mailgun, or any SMTP relay)
1. Update the System and Install Base Dependencies
1. Refresh the package index and upgrade existing packages.
apt update && apt upgrade -y
2. Install required system packages.
apt install -y curl wget gnupg2 ca-certificates lsb-release \
build-essential git imagemagick ffmpeg libpq-dev libxml2-dev \
libxslt1-dev libssl-dev libreadline-dev libyaml-dev \
libcurl4-openssl-dev libffi-dev zlib1g-dev libidn11-dev \
libicu-dev libjemalloc-dev pkg-config
This pulls every native library Mastodon's Ruby gems and Node.js modules compile against.
2. Install PostgreSQL 16, Redis, and Node.js
3. Add the PostgreSQL APT repository and install version 16.
curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \
| gpg --dearmor -o /usr/share/keyrings/postgresql.gpg
echo "deb [signed-by=/usr/share/keyrings/postgresql.gpg] \
https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \
> /etc/apt/sources.list.d/pgdg.list
apt update && apt install -y postgresql-16
4. Install Redis 7 from the default Ubuntu repos (Ubuntu 24.04 ships Redis 7).
apt install -y redis-server
systemctl enable --now redis-server
5. Add the NodeSource repository and install Node.js 20 LTS.
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt install -y nodejs
6. Install the yarn package manager via npm.
npm install -g yarn
3. Create the Mastodon System User and Install Ruby
Mastodon runs as a dedicated unprivileged user. Ruby is managed per-user via rbenv to avoid system-wide version conflicts.
7. Create the mastodon system user with a home directory.
adduser --disabled-login mastodon
8. Switch to the mastodon user and install rbenv.
su - mastodon
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"
9. Install ruby-build as an rbenv plugin.
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
10. Install Ruby 3.3.4 (the version Mastodon 4.3.x targets).
rbenv install 3.3.4
rbenv global 3.3.4
Expect this to take 5–10 minutes on a 2-core VPS.
11. Install Bundler.
gem install bundler --no-document
4. Clone Mastodon and Configure the Database
12. Still as the mastodon user, clone the latest stable release (4.3.2 at time of writing).
git clone https://github.com/mastodon/mastodon.git ~/live
cd ~/live
git checkout v4.3.2
13. Install Ruby gems (production only, with jemalloc).
bundle config deployment 'true'
bundle config without 'development test'
bundle install -j$(nproc)
14. Install JavaScript dependencies.
yarn install --pure-lockfile
15. Exit back to root and create the PostgreSQL role.
exit
sudo -u postgres psql -c "CREATE USER mastodon CREATEDB;"
No password is needed because Mastodon connects via the local Unix socket with peer authentication.
5. Run the Mastodon Setup Wizard
The interactive wizard generates your .env.production file, runs database migrations, and precompiles assets.
16. Switch back to the mastodon user and run the wizard.
su - mastodon
cd ~/live
RAILS_ENV=production bundle exec rake mastodon:setup
Answer each prompt:
| Prompt | Example answer |
|---|---|
| Domain name | social.example.com |
| Single-user mode | no |
| Store media on cloud | no (local for now) |
| PostgreSQL host | /var/run/postgresql |
| PostgreSQL port | 5432 |
| PostgreSQL DB name | mastodon_production |
| PostgreSQL user | mastodon |
| Redis host | 127.0.0.1 |
| Redis port | 6379 |
| SMTP server | your relay hostname |
| SMTP port | 587 |
| SMTP login/password | your credentials |
| Send e-mail from | notifications@social.example.com |
The wizard will write ~/live/.env.production, migrate the database, and precompile assets (asset compilation takes 3–8 minutes).
17. When the wizard asks to create an admin account, enter a username and e-mail. Save the generated password.
6. Configure Nginx and Let's Encrypt
18. Exit to root and install Nginx and Certbot.
exit
apt install -y nginx certbot python3-certbot-nginx
19. Copy Mastodon's bundled Nginx config.
cp /home/mastodon/live/dist/nginx.conf /etc/nginx/sites-available/mastodon
ln -s /etc/nginx/sites-available/mastodon /etc/nginx/sites-enabled/mastodon
rm /etc/nginx/sites-enabled/default
20. Replace the placeholder domain in the config.
sed -i 's/example.com/social.example.com/g' /etc/nginx/sites-available/mastodon
21. Obtain a Let's Encrypt certificate. Certbot rewrites the Nginx config automatically.
certbot --nginx -d social.example.com --non-interactive \
--agree-tos -m admin@example.com
Expected output (last two lines):
Successfully received certificate.
Deploying certificate to VirtualHost /etc/nginx/sites-available/mastodon
22. Reload Nginx.
nginx -t && systemctl reload nginx
7. Install and Start systemd Services
Mastodon runs three processes: web (Puma), sidekiq (background jobs), and streaming (WebSocket API).
23. Copy the bundled systemd unit files.
cp /home/mastodon/live/dist/mastodon-*.service /etc/systemd/system/
24. Reload systemd and enable all three services.
systemctl daemon-reload
systemctl enable --now mastodon-web mastodon-sidekiq mastodon-streaming
25. Check that all three units are active.
systemctl status mastodon-web mastodon-sidekiq mastodon-streaming
Expected output for each unit:
Active: active (running) since ...
Verify It Works
26. Confirm the web process is listening on port 3000.
ss -tlnp | grep 3000
LISTEN 0 1024 127.0.0.1:3000 0.0.0.0:*
27. Confirm the streaming API is listening on port 4000.
ss -tlnp | grep 4000
28. Hit the instance API from the server itself.
curl -s https://social.example.com/api/v1/instance | python3 -m json.tool | head -20
You should see a JSON object with "uri": "social.example.com" and your instance title.
29. Open https://social.example.com in a browser and log in with the admin credentials saved in step 17.
Troubleshooting
Services fail to start after reboot
Run journalctl -u mastodon-web -n 50 to read the last 50 log lines. The most common cause is a missing or malformed .env.production. Confirm the file exists at /home/mastodon/live/.env.production and contains SECRET_KEY_BASE and OTP_SECRET.
Asset precompilation fails with a memory error On a 1 GB RAM VPS, add a 2 GB swap file before running the wizard:
fallocate -l 2G /swapfile && chmod 600 /swapfile
mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
Nginx returns 502 Bad Gateway
Verify Puma is running (systemctl status mastodon-web) and that the upstream block in /etc/nginx/sites-available/mastodon points to unix:/home/mastodon/live/tmp/sockets/puma.sock.
Emails are not delivered Test SMTP from the Rails console:
su - mastodon
cd ~/live
RAILS_ENV=production bundle exec rails c
ActionMailer::Base.mail(to: 'you@example.com', subject: 'test', body: 'ok').deliver_now
Check the Sidekiq log (journalctl -u mastodon-sidekiq -n 100) for SMTP authentication errors.
Federation is not working
Confirm your domain resolves correctly (dig A social.example.com) and that the /.well-known/webfinger endpoint is reachable from an external host:
curl https://social.example.com/.well-known/webfinger
Next Steps
With your self-hosted Mastodon instance setup complete, consider this guide on optimizing web performance for these hardening and cost-optimisation steps:
- Object storage: Move media to Hetzner Object Storage (S3-compatible, €0.023/GB) by setting
S3_ENABLED=truein.env.productionand restarting the services. - Automatic backups: Schedule a daily
pg_dump mastodon_production | gzip > /backups/mastodon-$(date +%F).sql.gzvia cron. - Elasticsearch: Install OpenSearch 2.x and set
ES_ENABLED=trueto enable full-text search across posts. - Scaling Sidekiq: Edit
/etc/systemd/system/mastodon-sidekiq.serviceto add multiple queues with-cconcurrency flags as your user count grows. - Renewal automation: Certbot installs a systemd timer automatically; verify it with
systemctl status certbot.timer.