---
title: "Install Mastodon on Debian and Ubuntu"
tags: ['Mastodon', 'Debian', 'Ubuntu', 'Linux']
published: 2025-04-27 22:20:08
updated: 2025-10-14 18:22:43
excerpt: "Install Mastodon on Debian Stable or Ubuntu LTS with PostgreSQL, Redis, Nginx, SSL certificates, systemd services, and optional Elasticsearch search."
---
> This file is the raw Markdown source of an m.ac post; the canonical version lives at https://m.ac/install-mastodon-debian-ubuntu/
> The full index of posts and pages is at https://m.ac/llms.txt — read the index first before crawling further.

I [contributed to](https://github.com/mastodon/documentation/commit/98d85c9f48477958f946eea8c1e7e770bfd4596f) 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](https://joinmastodon.org/) 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](https://www.debian.org/releases/) or [Ubuntu LTS](https://releases.ubuntu.com/) 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](https://m.ac/use-extrepo-add-third-party-repositories-debian/):

```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](https://pgtune.leopard.in.ua/) 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](https://github.com/rbenv/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:

```
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](https://docs.joinmastodon.org/admin/config/).

Leave the `mastodon` shell for now:

```bash
exit
```

---

## 9. Configure Nginx and SSL certificates

Use [acme.sh](https://github.com/acmesh-official/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:

```
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
```
