m.ac

Install Mastodon on Debian and Ubuntu

1,009 words6 min read

I contributed to the official Mastodon installation guide back in 2022. It has drifted since then, as server guides tend to do. This version walks through installing and configuring a Mastodon instance on your own server.


#1. What is Mastodon?

Mastodon is a free, open-source, decentralized social network. It feels familiar if you have used Twitter or X, but one company does not own the whole thing.

Instead of everyone joining one central site, users create or join independently operated servers called instances that communicate across the wider Fediverse.

Running your own Mastodon instance gives you full control over your community, data, and content policies.


#2. Prerequisites

Have these ready before you start:

  • A domain name with DNS A/AAAA records pointed to your server's IP address. This tutorial uses example.com as the placeholder.

  • A Linux server with Debian Stable or Ubuntu LTS installed and a user with sudo privileges.

  • An email delivery service such as Mailgun or Amazon Simple Email Service.

The commands below use sudo. If you prefer a root shell, run sudo -i first and drop sudo from the commands.

Log in to your server and update the system:

bash
sudo apt update
sudo apt upgrade -y
sudo apt full-upgrade -y

Install the required software and dependencies:

bash
sudo apt install curl vim wget gnupg dpkg apt-transport-https lsb-release ca-certificates -y

#3. Add package repositories

Nginx

bash
curl -sS https://n.wtf/public.key | sudo gpg --dearmor -o /usr/share/keyrings/n.wtf.gpg

echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/n.wtf.gpg] https://mirror-cdn.xtom.com/sb/nginx/ $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/n.wtf.list

On Debian, you can also enable it with extrepo:

bash
sudo apt install extrepo -y
sudo extrepo enable n.wtf

Node.js

bash
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/nodesource.gpg

echo "deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_24.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list

Or use extrepo:

bash
sudo apt install extrepo -y
sudo extrepo enable node_24.x

PostgreSQL

bash
curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo gpg --dearmor -o /usr/share/keyrings/postgresql.gpg

echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" | sudo tee /etc/apt/sources.list.d/postgresql.list

Or use extrepo:

bash
sudo apt install extrepo -y
sudo extrepo enable postgresql

Redis

bash
curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg

echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list

Or use extrepo:

bash
sudo apt install extrepo -y
sudo extrepo enable redis

Elasticsearch 8 (optional)

bash
curl -sS https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo gpg --dearmor -o /usr/share/keyrings/elasticsearch.gpg

echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/elasticsearch.gpg] https://artifacts.elastic.co/packages/8.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elasticsearch.list

Or use extrepo:

bash
sudo apt install extrepo -y
sudo extrepo enable elastic_8

Update the package lists:

bash
sudo apt update

#4. Install system packages

Install the system packages Mastodon needs:

bash
sudo apt install -y \
  imagemagick ffmpeg libpq-dev libxml2-dev libxslt1-dev file git \
  g++ libprotobuf-dev protobuf-compiler pkg-config nodejs gcc autoconf \
  bison build-essential libssl-dev libyaml-dev libreadline-dev \
  zlib1g-dev libncurses-dev libffi-dev libgdbm-dev \
  redis-server redis-tools postgresql postgresql-contrib \
  libidn11-dev libicu-dev libjemalloc-dev libvips nginx-extras

Enable Yarn through Corepack:

bash
sudo corepack enable

Enable the redis-server service:

bash
sudo systemctl enable --now redis-server

#5. Create the Mastodon user

Create a dedicated mastodon user:

bash
sudo adduser --disabled-password --gecos "" --home /home/mastodon mastodon

Then grant the right permissions for the /home/mastodon directory:

bash
sudo chmod 0755 /home/mastodon

#6. Set up PostgreSQL

Mastodon uses PostgreSQL as its database. Create a user named mastodon and allow it to create Mastodon's database:

bash
sudo -u postgres psql
CREATE USER mastodon CREATEDB;
\q

You can use PGTune to tune PostgreSQL for your server.

PGTune generates recommended settings from your RAM and CPU. Treat them as a starting point, not sacred text.


#7. Install Mastodon

Switch to the mastodon user and move into /home/mastodon. From here until you exit this shell, all commands run as the mastodon user without sudo:

bash
sudo -i -u mastodon
cd ~

Install rbenv, a Ruby version manager:

bash
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
cd ~/.rbenv && src/configure && make -C src
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
source ~/.bashrc
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build

Clone the Mastodon Git repository and switch to the latest released version:

bash
cd ~
git clone https://github.com/mastodon/mastodon.git live && cd live
git checkout $(git tag -l | grep '^v[0-9.]*$' | sort -V | tail -n 1)

After checkout, install the Ruby version required by Mastodon:

bash
RUBY_CONFIGURE_OPTS=--with-jemalloc rbenv install

Install the Ruby and JavaScript dependencies:

bash
bundle config deployment 'true'
bundle config without 'development test'
bundle install -j$(getconf _NPROCESSORS_ONLN)
yarn install --immutable

Wait for the install to finish before running the setup wizard.


#8. Generate the Mastodon configuration file

Run the interactive setup wizard:

text
RAILS_ENV=production bin/rails mastodon:setup

This process will:

  • Create a configuration file
  • Run asset precompilation
  • Create the database schema

The configuration file is saved as .env.production. You can review and edit it as needed. For detailed options, see the official documentation.

Leave the mastodon shell for now:

bash
exit

#9. Configure Nginx and SSL certificates

Use acme.sh to obtain free TLS certificates from Let's Encrypt. acme.sh is installed per user and its commands run without sudo; running this section as root (sudo -i) is recommended for reloading Nginx after certificate issuance:

text
cd ~
git clone https://github.com/acmesh-official/acme.sh.git
cd ./acme.sh
./acme.sh --install -m user@example.com
source ~/.bashrc
acme.sh --upgrade --auto-upgrade
acme.sh --set-default-ca --server letsencrypt

Replace user@example.com with your actual email address. If you leave the example address, Let's Encrypt will reject it with an error like this:

bash
[Mon Apr 28 06:28:13 PM UTC 2025] Registering account: https://acme-v02.api.letsencrypt.org/directory
[Mon Apr 28 06:28:14 PM UTC 2025] Account registration error: {
  "type": "urn:ietf:params:acme:error:invalidContact",
  "detail": "Error creating new account :: contact email has forbidden domain \"example.com\"",
  "status": 400
}

Create a directory for Let's Encrypt HTTP-01 verification and another directory for the SSL certificates:

bash
sudo mkdir -p /var/www/letsencrypt/.well-known/{acme-challenge,pki-validation}
sudo chown www-data:www-data /var/www/letsencrypt -R
sudo mkdir -p /etc/nginx/ssl

Modify the default Nginx configuration file.

bash
sudo bash -c 'cat > /etc/nginx/sites-available/default << "EOF"
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;

    location ~ ^/.well-known/(acme-challenge|pki-validation)/ {
      root /var/www/letsencrypt;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}
EOF'

This configuration redirects all requests on HTTP port 80 to HTTPS port 443, except for requests to /.well-known/acme-challenge/ and /.well-known/pki-validation/, which are served from the /var/www/letsencrypt directory.

The acme-challenge path is what Let's Encrypt uses for HTTP-01 validation. The pki-validation path is kept for other certificate workflows.

Reload Nginx:

bash
sudo nginx -t
sudo nginx -s reload

Issue SSL certificates for your domain name:

bash
acme.sh --issue -d example.com -w /var/www/letsencrypt

Replace example.com with your own domain name.

Install the certificates to /etc/nginx/ssl:

bash
acme.sh --install-cert -d example.com \
--key-file       /etc/nginx/ssl/example.com.key  \
--fullchain-file /etc/nginx/ssl/example.com.crt \
--ca-file        /etc/nginx/ssl/example.com.ca.crt \
--reloadcmd     "sudo systemctl restart nginx"

You may need to add your current user to the sudoers file so acme.sh can restart Nginx without a password prompt. To do this, run:

bash
sudo visudo

Add the following line at the end of the file:

bash
your_username ALL=(ALL) NOPASSWD: /bin/systemctl restart nginx

Copy the Nginx configuration template from the Mastodon directory:

bash
sudo cp /home/mastodon/live/dist/nginx.conf /etc/nginx/sites-available/mastodon
sudo ln -s /etc/nginx/sites-available/mastodon /etc/nginx/sites-enabled/mastodon

The template needs three edits: your domain, your certificate paths, and one deleted block.

First, set your domain in a shell variable and let sed handle the renaming. The match side stays literal (that is exactly what the template says), and the replacement side carries your domain, so this works for any real domain:

bash
DOMAIN=example.com
sudo sed -i \
  -e "s/server_name example.com;/server_name $DOMAIN;/g" \
  -e "s|# ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;|ssl_certificate     /etc/nginx/ssl/$DOMAIN.crt;|" \
  -e "s|# ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;|ssl_certificate_key /etc/nginx/ssl/$DOMAIN.key;|" \
  /etc/nginx/sites-available/mastodon

This sets server_name in both server blocks, and uncomments the two ssl_certificate lines while pointing them at the certificates acme.sh installed to /etc/nginx/ssl earlier.

Then open the file and delete the server block that starts with listen 80. Our default site from section 9 already redirects HTTP to HTTPS and serves the ACME challenges, so this block would only get in the way of renewals:

nginx
server {
  listen 80;
  listen [::]:80;
  server_name example.com;
  root /home/mastodon/live/public;
  location /.well-known/acme-challenge/ { allow all; }
  location / { return 301 https://$host$request_uri; }
}

After the edits, the top of the remaining server block should look like this:

nginx
server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name example.com;

  ssl_protocols TLSv1.2 TLSv1.3;

  ssl_prefer_server_ciphers on;
  ssl_session_cache shared:SSL:10m;
  ssl_session_tickets off;

  ssl_certificate     /etc/nginx/ssl/example.com.crt;
  ssl_certificate_key /etc/nginx/ssl/example.com.key;

Create a cache folder for Nginx and reload it:

bash
sudo mkdir -p /var/cache/nginx
sudo chown www-data:www-data /var/cache/nginx -R
sudo nginx -t
sudo nginx -s reload

#10. Set up systemd services

Copy the systemd service templates from the Mastodon directory.

bash
sudo cp /home/mastodon/live/dist/mastodon-*.service /etc/systemd/system/

Start and enable the new systemd services:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now mastodon-web mastodon-sidekiq mastodon-streaming

Visit https://example.com/ in your browser to open your Mastodon instance.


#11. Enable Elasticsearch (optional)

Mastodon supports full-text search when Elasticsearch is available. Enable it only if your server has enough RAM and CPU; otherwise, keep the instance lighter and skip this section.

If you have already added the official Elasticsearch 8.x repository, install it with apt:

bash
sudo apt install elasticsearch

Create a jvm.options file to configure Elasticsearch memory usage:

bash
sudo bash -c 'echo "-Xms2g
-Xmx2g" > /etc/elasticsearch/jvm.options.d/jvm.options'

Elasticsearch 8 ships with security enabled but serves HTTPS with a self-signed certificate by default. For a localhost-only node it is simpler to keep authentication and turn off TLS on the HTTP layer:

bash
echo "xpack.security.http.ssl.enabled: false" | sudo tee -a /etc/elasticsearch/elasticsearch.yml

Enable and start the systemd service:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now elasticsearch

Set a password for the built-in elastic user and note it down:

bash
sudo /usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic

Edit the /home/mastodon/live/.env.production file and add the following lines, replacing password with the one you just generated:

bash
# Elasticsearch (optional)
# ------------------------
ES_ENABLED=true
ES_HOST=localhost
ES_PORT=9200
# Authentication for ES (optional)
ES_USER=elastic
ES_PASS=password

Restart the Mastodon services so they read the new settings:

bash
sudo systemctl restart mastodon-{web,sidekiq}

Run the following command to create the Elasticsearch indices and populate them with searchable data:

bash
sudo -i -u mastodon
RAILS_ENV=production /home/mastodon/live/bin/tootctl search deploy
Share