How to Install Mastodon with Docker on Ubuntu 22.04

Updated on November 21, 2023
How to Install Mastodon with Docker on Ubuntu 22.04 header image

Introduction

Mastodon is a free, open-source, decentralized social network. Mastodon allows users to set up self-hosted servers to communicate with each other through the federated network.

This article shows how to set up a Mastodon instance on Ubuntu with Docker.

At the end of this article, you will have:

  • Set up a Mastodon instance.
  • Set up Elasticsearch for your Mastodon instance.
  • Managed your Mastodon instance with tootctl.
  • Automated the Mastodon maintenance.
  • Configured Nginx and Let's Encrypt SSL
  • Secured the server with ufw and fail2ban.

Prerequisites

Before beginning this guide, you should have the following:

Install Docker and Docker Compose

Docker is an open-source platform for developing, shipping, and running applications. Docker enables you to run Mastodon in an isolated and optimized environment.

Follow the below steps to install Docker on your server.

  1. Uninstall old applications such as docker, docker.io, and docker-engine.

     sudo apt-get remove docker docker-engine docker.io containerd runc
  2. Set up the repository

     sudo apt-get update
     sudo apt-get install -y ca-certificates curl gnupg lsb-release
     curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
     echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
     sudo apt-get update
  3. Install the latest version of Docker Engine

     sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

Prepare a Directory for Mastodon

This section shows how to create a folder for Mastodon and some necessary environment files to follow this article.

  1. Create a folder for Mastodon. This article uses /opt/mastodon as the main folder.

     mkdir /opt/mastodon
  2. Create an empty file named .env.es and .env.mastodon for environment variables.

     touch /opt/mastodon/.env.es
     touch /opt/mastodon/.env.mastodon

Deploy a PostgreSQL Database

This section shows two options for using PostgreSQL Database with Mastodon:

  • Option 1: Use a Vultr Managed Database for PostgreSQL to automate the administration.
  • Option 2: Deploy your own PostgreSQL database with Docker.

Option 1: Use Vultr Managed PostgreSQL Database

  1. Navigate to Databases in your Customer Portal and deploy a Vultr Managed Database for PostgreSQL.

  2. Get your PostgreSQL database credentials to use in the later section.

  3. Create a file named docker-compose.yml at /opt/mastodon/docker-compose.yml with the following contents. Replace tootsuite/mastodon:v4.0 with another tag if you want.

     version: '3'
     networks:
       external_network:
       internal_network:
         internal: true
     services:
       redis:
         restart: always
         image: redis:7-alpine
         networks:
           - internal_network
         healthcheck:
           test: [ 'CMD', 'redis-cli', 'ping' ]
         volumes:
           - ./data/redis:/data
       es:
         restart: always
         image: docker.elastic.co/elasticsearch/elasticsearch:7.17.8
         environment:
           - "ES_JAVA_OPTS=-Xms512m -Xmx512m -Des.enforce.bootstrap.checks=true"
           - "xpack.license.self_generated.type=basic"
           - "xpack.security.enabled=false"
           - "xpack.watcher.enabled=false"
           - "xpack.graph.enabled=false"
           - "xpack.ml.enabled=false"
           - "bootstrap.memory_lock=true"
           - "cluster.name=mastodon-es"
           - "discovery.type=single-node"
           - "thread_pool.write.queue_size=1000"
         env_file:
           - .env.es
         networks:
           - external_network
           - internal_network
         healthcheck:
           test:
             [
               "CMD-SHELL",
               "curl --silent --fail localhost:9200/_cluster/health || exit 1"
             ]
         volumes:
           - /opt/mastodon/data/elasticsearch:/usr/share/elasticsearch/data
         ulimits:
           memlock:
             soft: -1
             hard: -1
           nofile:
             soft: 65536
             hard: 65536
         ports:
           - '127.0.0.1:9200:9200'
    
       console:
         image: tootsuite/mastodon:v4.0
         env_file: .env.mastodon
         command: /bin/bash
         restart: "no"
         depends_on:
           - redis
         networks:
           - internal_network
           - external_network
         volumes:
           - ./data/public/system:/mastodon/public/system
    
       web:
         image: tootsuite/mastodon:v4.0
         restart: always
         env_file: .env.mastodon
         command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
         networks:
           - internal_network
           - external_network
         healthcheck:
           # prettier-ignore
           test:
             [
               'CMD-SHELL',
               'wget -q --spider --proxy=off localhost:3000/health || exit 1'
             ]
         ports:
           - '127.0.0.1:3000:3000'
         depends_on:
           - es
           - redis
         volumes:
           - ./data/public/system:/mastodon/public/system
    
       streaming:
         image: tootsuite/mastodon:v4.0
         restart: always
         env_file: .env.mastodon
         command: node ./streaming
         networks:
           - external_network
           - internal_network
         healthcheck:
           # prettier-ignore
           test:
             [
               'CMD-SHELL',
               'wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health || exit 1'
             ]
         ports:
           - '127.0.0.1:4000:4000'
    
       sidekiq:
         image: tootsuite/mastodon:v4.0
         restart: always
         env_file: .env.mastodon
         command: bundle exec sidekiq
         networks:
           - external_network
           - internal_network
         volumes:
           - ./data/public/system:/mastodon/public/system
         healthcheck:
           test: [ 'CMD-SHELL', "ps aux | grep '[s]idekiq 6' || false" ]

Option 2: Deploy PostgreSQL Database with Docker

  1. Create a file named docker-compose.yml at /opt/mastodon/docker-compose.yml with the following contents. Replace tootsuite/mastodon:v4.0 with another tag if you want.

     version: '3'
     networks:
       external_network:
       internal_network:
         internal: true
     services:
       db:
         restart: always
         image: postgres:14-alpine
         shm_size: 256mb
         networks:
           - internal_network
         healthcheck:
           test: [ 'CMD', 'pg_isready', '-U', 'postgres' ]
         volumes:
           - ./data/postgres:/var/lib/postgresql/data
         environment:
           - 'POSTGRES_HOST_AUTH_METHOD=trust'
         env_file:
           - .env.db
       redis:
         restart: always
         image: redis:7-alpine
         networks:
           - internal_network
         healthcheck:
           test: [ 'CMD', 'redis-cli', 'ping' ]
         volumes:
           - ./data/redis:/data
       es:
         restart: always
         image: docker.elastic.co/elasticsearch/elasticsearch:7.17.8
         environment:
           - "ES_JAVA_OPTS=-Xms512m -Xmx512m -Des.enforce.bootstrap.checks=true"
           - "xpack.license.self_generated.type=basic"
           - "xpack.security.enabled=false"
           - "xpack.watcher.enabled=false"
           - "xpack.graph.enabled=false"
           - "xpack.ml.enabled=false"
           - "bootstrap.memory_lock=true"
           - "cluster.name=mastodon-es"
           - "discovery.type=single-node"
           - "thread_pool.write.queue_size=1000"
         env_file:
           - .env.es
         networks:
           - external_network
           - internal_network
         healthcheck:
           test:
             [
               "CMD-SHELL",
               "curl --silent --fail localhost:9200/_cluster/health || exit 1"
             ]
         volumes:
           - /opt/mastodon/data/elasticsearch:/usr/share/elasticsearch/data
         ulimits:
           memlock:
             soft: -1
             hard: -1
           nofile:
             soft: 65536
             hard: 65536
         ports:
           - '127.0.0.1:9200:9200'
    
       console:
         image: tootsuite/mastodon:v4.0
         env_file: .env.mastodon
         command: /bin/bash
         restart: "no"
         depends_on:
           - db
           - redis
         networks:
           - internal_network
           - external_network
         volumes:
           - ./data/public/system:/mastodon/public/system
       web:
         image: tootsuite/mastodon:v4.0
         restart: always
         env_file: .env.mastodon
         command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
         networks:
           - internal_network
           - external_network
         healthcheck:
           # prettier-ignore
           test:
             [
               'CMD-SHELL',
               'wget -q --spider --proxy=off localhost:3000/health || exit 1'
             ]
         ports:
           - '127.0.0.1:3000:3000'
         depends_on:
           - db
           - redis
           - es
         volumes:
           - ./data/public/system:/mastodon/public/system
    
       streaming:
         image: tootsuite/mastodon:v4.0
         restart: always
         env_file: .env.mastodon
         command: node ./streaming
         networks:
           - external_network
           - internal_network
         healthcheck:
           # prettier-ignore
           test:
             [
               'CMD-SHELL',
               'wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health || exit 1'
             ]
         ports:
           - '127.0.0.1:4000:4000'
         depends_on:
           - db
           - redis
       sidekiq:
         image: tootsuite/mastodon:v4.0
         restart: always
         env_file: .env.mastodon
         command: bundle exec sidekiq
         networks:
           - external_network
           - internal_network
         volumes:
           - ./data/public/system:/mastodon/public/system
         healthcheck:
           test: [ 'CMD-SHELL', "ps aux | grep '[s]idekiq 6' || false" ]
         depends_on:
           - db
           - redis
  2. Create a file named .env.db at /opt/mastodon/.env.db with the following contents. Replace <YOUR_DATABASE_PASSWORD> with a secure secret for the database. Note that your PostgreSQL username is postgres.

     POSTGRES_USER=postgres
     POSTGRES_PASSWORD=<YOUR_DATABASE_PASSWORD>
  3. Start the PostgreSQL Database with Docker Compose.

     docker compose -f /opt/mastodon/docker-compose.yml up -d db

Deploy Elasticsearch with Docker

Elasticsearch enables full-text search in Mastodon. This section shows how to prepare the system and deploy Elasticsearch with Docker.

  1. Create a file named .env.es at /opt/mastodon/.env.es with the following contents. Replace <YOUR_ELASTIC_SEARCH_PASSWORD> with a secure secret for Elasticsearch.

     ELASTIC_PASSWORD=<YOUR_ELASTIC_SEARCH_PASSWORD>
  2. Create a folder at /opt/mastodon/data/elasticsearch to enable persistent storage for Elasticsearch.

     mkdir -p /opt/mastodon/data/elasticsearch
  3. Change the folder permission of the /opt/mastodon/data/elasticsearch.

     sudo chown -R 1000:1000 /opt/mastodon/data/elasticsearch
  4. Increase vm.max_map_count with sysctl.

     sysctl -w vm.max_map_count=262144
  5. Open the file /etc/sysctl.conf with your favorite editor and set the value for vm.max_map_count as follows:

     vm.max_map_count=262144
  6. Start the Elasticsearch with Docker Compose.

     docker compose -f /opt/mastodon/docker-compose.yml up -d es
  7. Create search indices for Elasticsearch. Ignore the error ProgressBar::InvalidProgressError if it occurs.

     docker compose -f /opt/mastodon/docker-compose.yml run --rm console bin/tootctl search deploy

Prepare Mastodon Secret Keys

Run the following command twice to generate two random secrets. In the next section, note these secrets to replace with the text <YOUR_RANDOM_SECRET>.

docker compose -f /opt/mastodon/docker-compose.yml run --rm console bundle exec rake secret

Here are two examples of random secrets.

009aa164cea560916c4e9cc9232a163783f8164cd6c2751c4cdb85689deca44f578108c0c7e0fecefff34b14da6cae661d10090e38f128e1ec286faf19a4b97c
c962fbb4c4692fbfa8333f50c0358ca38cbe5b88a11e7238752c53897b5ae55fc21e7d893c1673493db195bc39123263198a2bc4a0873c4f0e9038c1dd98fdd4

Run the following command to generate the Voluntary Application Server Identity (VAPID) keys to send and receive website push notifications. In the next section, note these secrets to replace with the text <YOUR_VAPID_PRIVATE_KEY> and <YOUR_VAPID_PUBLIC_KEY>.

docker compose -f /opt/mastodon/docker-compose.yml run --rm console bundle exec rake mastodon:webpush:generate_vapid_key

Here are an example of VAPID keys

VAPID_PRIVATE_KEY=_zy6kJtBrakQy18PWu1zj4VpNecMIEUHK0xKI_8-8KA=
VAPID_PUBLIC_KEY=BCHtrfabm8Q7BAkEEQu2IChJzUeOiB-tBFTIxMuQqxFaXqfsfkYeZfsmwGTGliwPICcw7uFRaaFO754NXUzsSQE=

Prepare Mastodon Environment Variables

Edit the file named .env.mastodon at /opt/mastodon/.env.mastodon with the following contents:

# This is a sample configuration file. You can generate your configuration
# with the `rake mastodon:setup` interactive setup wizard, but to customize
# your setup even further, you'll need to edit it manually. This sample does
# not demonstrate all available configuration options. Please look at
# https://docs.joinmastodon.org/admin/config/ for the full documentation.

# Note that this file accepts slightly different syntax depending on whether
# you are using `docker-compose` or not. In particular, if you use
# `docker-compose`, the value of each declared variable will be taken verbatim,
# including surrounding quotes.
# See: https://github.com/mastodon/mastodon/issues/16895

# Federation
# ----------
# This identifies your server and cannot be changed safely later
# ----------
LOCAL_DOMAIN=<YOUR_DOMAIN>

# Redis
# -----
REDIS_HOST=redis
REDIS_PORT=6379

# PostgreSQL
# ----------
DB_HOST=<YOUR_DATABASE_HOST>
DB_USER=<YOUR_DATABASE_USERNAME>
DB_NAME=<YOUR_DATABASE_DBNAME>
DB_PASS=<YOUR_DATABASE_PASSWORD>
DB_PORT=<YOUR_DATABASE_PORT>

# Elasticsearch (optional)
# ------------------------
ES_ENABLED=true
ES_HOST=es
ES_PORT=9200
# Authentication for ES (optional)
ES_USER=elastic
ES_PASS=<YOUR_ELASTIC_SEARCH_PASSWORD>

# Secrets
# -------
# Make sure to use `rake secret` to generate secrets
# -------
SECRET_KEY_BASE=<YOUR_RANDOM_SECRET>
OTP_SECRET=<YOUR_RANDOM_SECRET>

# Web Push
# --------
# Generate with `rake mastodon:webpush:generate_vapid_key`
# --------
VAPID_PRIVATE_KEY=<YOUR_VAPID_PRIVATE_KEY>
VAPID_PUBLIC_KEY=<YOUR_VAPID_PUBLIC_KEY>

# Sending mail
# ------------
SMTP_SERVER=<YOUR_SMTP_SERVER>
SMTP_PORT=587
SMTP_LOGIN=<YOUR_SMTP_LOGIN>
SMTP_PASSWORD=<YOUR_SMTP_PASSWORD>
SMTP_FROM_ADDRESS=<YOUR_SMTP_EMAIL>

# File storage (optional)
# -----------------------
S3_ENABLED=true
S3_BUCKET=<YOUR_OBJECT_STORAGE_BUCKET>
AWS_ACCESS_KEY_ID=<YOUR_OBJECT_STORAGE_ACCESS_KEY>
AWS_SECRET_ACCESS_KEY=<YOUR_OBJECT_STORAGE_SECRET_KEY>
S3_ALIAS_HOST=<YOUR_OBJECT_STORAGE_URL>

# IP and session retention
# -----------------------
# Make sure to modify the scheduling of ip_cleanup_scheduler in config/sidekiq.yml
# to be less than daily if you lower IP_RETENTION_PERIOD below two days (172800).
# -----------------------
IP_RETENTION_PERIOD=31556952
SESSION_RETENTION_PERIOD=31556952

Replace the text placeholder in /opt/mastodon/.env.mastodon as follows:

  • <YOUR_DOMAIN>: your domain name.
  • <YOUR_DATABASE_HOST>: your PostgreSQL database host. Use db if you deploy a PostgreSQL with Docker.
  • <YOUR_DATABASE_USERNAME>: your PostgreSQL username from the previous section.
  • <YOUR_DATABASE_DBNAME>: your PostgreSQL database name from the previous section.
  • <YOUR_DATABASE_PASSWORD>: your PostgreSQL database password from the previous section.
  • <YOUR_DATABASE_PORT>: your PostgreSQL database port from the previous section.
  • <YOUR_ELASTIC_SEARCH_PASSWORD>: your Elasticsearch password from the previous section.
  • <YOUR_RANDOM_SECRET>: Mastodon secret keys from the previous section.
  • <YOUR_VAPID_PRIVATE_KEY>: VAPID private Key from the previous section.
  • <YOUR_VAPID_PUBLIC_KEY>: VAPID public Key from the previous section.
  • <YOUR_SMTP_LOGIN>: your SMPL account credentials.
  • <YOUR_SMTP_PASSWORD>: your SMPL account credentials.
  • <YOUR_SMTP_EMAIL>: your SMPL email address.
  • <YOUR_SMTP_SERVER>: your SMPL server information.
  • <YOUR_OBJECT_STORAGE_BUCKET>: your Vultr Object Storage bucket name.
  • <YOUR_OBJECT_STORAGE_ACCESS_KEY>: your Vultr Object Storage Access Key.
  • <YOUR_OBJECT_STORAGE_SECRET_KEY>: your Vultr Object Storage Secret Key.
  • <YOUR_OBJECT_STORAGE_URL>: your Vultr Object Storage URL.

Deploy Mastodon with Docker Compose

If you use the Vultr Managed PostgreSQL database, run the following command to set up the database.

docker compose -f /opt/mastodon/docker-compose.yml run --rm console bundle exec rake db:migrate

If you deploy PostgreSQL with Docker, run the following command to set up the database.

docker compose -f /opt/mastodon/docker-compose.yml run --rm console bundle exec rake db:setup

Deploy Mastodon services with Docker Compose.

docker compose -f /opt/mastodon/docker-compose.yml up -d

Deploy Nginx Reverse Proxy

  1. Install Nginx.

     sudo apt-get update
     sudo apt-get install -y nginx
  2. Create a file named mastodon at /etc/nginx/sites-available/mastodon with the following contents. Replace example.com with your domain.

     server {
         server_name example.com;
    
         proxy_set_header Host $host;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header X-Forwarded-Proto $scheme;
         proxy_set_header Proxy "";
         proxy_http_version 1.1;
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Connection "upgrade";
    
             location / {
                 proxy_pass http://localhost:3000;
                 proxy_pass_header Server;
    
                 proxy_buffering on;
                 proxy_redirect off;
             }
    
             location ^~ /api/v1/streaming {
    
                 proxy_pass http://localhost:4000;
                 proxy_buffering off;
                 proxy_redirect off;
             }
     }
  3. Link to sites-enabled to enable the virtual host.

     ln -s /etc/nginx/sites-available/mastodon /etc/nginx/sites-enabled/
  4. Reload the nginx service.

     systemctl restart nginx
  5. Config ufw firewall to allow Nginx ports. Skip if your server doesn't have ufw.

     sudo ufw allow 'Nginx Full'

Secure Mastodon with Let's Encrypt SSL

  1. Install Certbot.

     sudo apt-get install -y certbot python3-certbot-nginx
  2. Run Certbot to automatically enable Let's Encrypt SSL for your domain. Replace example.com with your domain.

     sudo certbot --nginx -d example.com
  3. Reload the nginx service.

     systemctl restart nginx
  4. Navigate to https://<YOUR_DOMAIN> to access your Mastodon instance.

Manage Your Mastodon Instance

Here are some useful commands to manage your Mastodon instance.

  • Use Docker Compose run command to access the toolctl.

      docker compose -f /opt/mastodon/docker-compose.yml run --rm console bin/tootctl
  • Create the owner user. Replace example_user with your user name and admin@gmail.com with your email address.

      docker compose -f /opt/mastodon/docker-compose.yml run --rm console bin/tootctl accounts create example_user --email admin@gmail.com --confirmed --role Owner
  • Create search indices for Elasticsearch.

      docker compose -f /opt/mastodon/docker-compose.yml run --rm console bin/tootctl search deploy
  • Disable registrations.

      docker compose -f /opt/mastodon/docker-compose.yml run --rm console bin/tootctl settings registrations close
  • Remove cached media files.

      docker compose -f /opt/mastodon/docker-compose.yml run --rm console bin/tootctl media remove
  • Remove local thumbnails for preview cards.

      docker compose -f /opt/mastodon/docker-compose.yml run --rm console bin/tootctl preview_cards remove

Setup Maintenance Automation

  1. Make a script file named auto-cleanup.sh at /opt/mastodon/auto-cleanup.sh with the following contents:

     #!/bin/sh
     docker compose -f /opt/mastodon/docker-compose.yml run --rm console bin/tootctl media remove
     docker compose -f /opt/mastodon/docker-compose.yml run --rm console bin/tootctl preview_cards remove
  2. Make the script /opt/mastodon/auto-cleanup.sh executable.

     chmod +x /opt/mastodon/auto-cleanup.sh
  3. Open crontab

     crontab -e
  4. Add a new crontab job to run auto-cleanup.sh every day at 00:00.

     0 0 * * * /opt/mastodon/auto-cleanup.sh

Configure Your Server Firewall

  1. Turn on automatic security updates.

     sudo dpkg-reconfigure -plow unattended-upgrades
  2. Set up a firewall with ufw.

     sudo apt-get install ufw
     sudo ufw default allow outgoing
     sudo ufw default deny incoming
     sudo ufw allow 22 comment 'SSH'
     sudo ufw allow http comment 'HTTP'
     sudo ufw allow https comment 'HTTPS'
     sudo ufw enable
  3. Check your firewall status

     sudo ufw status
  4. Install fail2ban to secure your server

     sudo apt-get install -y fail2ban

Configure fail2ban to Use ufw

  1. Copy the main configuration to avoid unexpected changes during package updates.

     sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
  2. Edit the configuration file with your favorite text editor

     sudo nano /etc/fail2ban/jail.local
  3. Change the banaction and banaction_allports settings to ufw in the file /etc/fail2ban/jail.local as follows:

     banaction = ufw
     banaction_allports = ufw
  4. Restart the fail2ban service.

     sudo systemctl restart fail2ban

Further reading

For more details about how to use Cache-Control headers with Nginx, see this Nginx Configuration file at the official Mastodon GitHub repository.