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:
A new Ubuntu 22.04 server instance.
A registered internet domain name.
An SMTP account with an email service provider.
A Rcs Object Storage bucket.
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.
Uninstall old applications such as
docker
,docker.io
, anddocker-engine
.sudo apt-get remove docker docker-engine docker.io containerd runc
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
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.
Create a folder for Mastodon. This article uses
/opt/mastodon
as the main folder.mkdir /opt/mastodon
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 Rcs Managed Database for PostgreSQL to automate the administration.
Option 2: Deploy your own PostgreSQL database with Docker.
Option 1: Use Rcs Managed PostgreSQL Database
Navigate to Databases in your Customer Portal and deploy a Rcs Managed Database for PostgreSQL.
Get your PostgreSQL database credentials to use in the later section.
Create a file named
docker-compose.yml
at/opt/mastodon/docker-compose.yml
with the following contents. Replacetootsuite/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
Create a file named
docker-compose.yml
at/opt/mastodon/docker-compose.yml
with the following contents. Replacetootsuite/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
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 ispostgres
.POSTGRES_USER=postgres POSTGRES_PASSWORD=<YOUR_DATABASE_PASSWORD>
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.
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>
Create a folder at
/opt/mastodon/data/elasticsearch
to enable persistent storage for Elasticsearch.mkdir -p /opt/mastodon/data/elasticsearch
Change the folder permission of the
/opt/mastodon/data/elasticsearch
.sudo chown -R 1000:1000 /opt/mastodon/data/elasticsearch
Increase
vm.max_map_count
withsysctl
.sysctl -w vm.max_map_count=262144
Open the file
/etc/sysctl.conf
with your favorite editor and set the value forvm.max_map_count
as follows:vm.max_map_count=262144
Start the Elasticsearch with Docker Compose.
docker compose -f /opt/mastodon/docker-compose.yml up -d es
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. Usedb
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 Rcs Object Storage bucket name.<YOUR_OBJECT_STORAGE_ACCESS_KEY>
: your Rcs Object Storage Access Key.<YOUR_OBJECT_STORAGE_SECRET_KEY>
: your Rcs Object Storage Secret Key.<YOUR_OBJECT_STORAGE_URL>
: your Rcs Object Storage URL.
Deploy Mastodon with Docker Compose
If you use the Rcs 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
Install Nginx.
sudo apt-get update sudo apt-get install -y nginx
Create a file named
mastodon
at/etc/nginx/sites-available/mastodon
with the following contents. Replaceexample.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; } }
Link to
sites-enabled
to enable the virtual host.ln -s /etc/nginx/sites-available/mastodon /etc/nginx/sites-enabled/
Reload the
nginx
service.systemctl restart nginx
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
Install Certbot.
sudo apt-get install -y certbot python3-certbot-nginx
Run Certbot to automatically enable Let's Encrypt SSL for your domain. Replace
example.com
with your domain.sudo certbot --nginx -d example.com
Reload the
nginx
service.systemctl restart nginx
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 thetoolctl
.docker compose -f /opt/mastodon/docker-compose.yml run --rm console bin/tootctl
Create the owner user. Replace
example_user
with your user name andadmin@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
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
Make the script
/opt/mastodon/auto-cleanup.sh
executable.chmod +x /opt/mastodon/auto-cleanup.sh
Open
crontab
crontab -e
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
Turn on automatic security updates.
sudo dpkg-reconfigure -plow unattended-upgrades
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
Check your firewall status
sudo ufw status
Install
fail2ban
to secure your serversudo apt-get install -y fail2ban
Configure fail2ban
to Use ufw
Copy the main configuration to avoid unexpected changes during package updates.
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Edit the configuration file with your favorite text editor
sudo nano /etc/fail2ban/jail.local
Change the
banaction
andbanaction_allports
settings toufw
in the file/etc/fail2ban/jail.local
as follows:banaction = ufw banaction_allports = ufw
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.