Self-Hosted Mastodon Instance Setup on Ubuntu 24.04

by David Park
Self-Hosted Mastodon Instance Setup on Ubuntu 24.04

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=true in .env.production and restarting the services.
  • Automatic backups: Schedule a daily pg_dump mastodon_production | gzip > /backups/mastodon-$(date +%F).sql.gz via cron.
  • Elasticsearch: Install OpenSearch 2.x and set ES_ENABLED=true to enable full-text search across posts.
  • Scaling Sidekiq: Edit /etc/systemd/system/mastodon-sidekiq.service to add multiple queues with -c concurrency flags as your user count grows.
  • Renewal automation: Certbot installs a systemd timer automatically; verify it with systemctl status certbot.timer.