Author: Quan Hua
Last Updated: Wed, Jan 4, 2023Mastodon 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.
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 Vultr Object Storage bucket.
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
, and docker-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
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
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.
Navigate to Databases in your Customer Portal and deploy a Vultr 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. 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" ]
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
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>
Start the PostgreSQL Database with Docker Compose.
docker compose -f /opt/mastodon/docker-compose.yml up -d db
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
with sysctl
.
sysctl -w vm.max_map_count=262144
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
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
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=
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.
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
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. 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;
}
}
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'
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.
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
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
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 server
sudo apt-get install -y fail2ban
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
and banaction_allports
settings to ufw
in the file /etc/fail2ban/jail.local
as follows:
banaction = ufw
banaction_allports = ufw
Restart the fail2ban
service.
sudo systemctl restart fail2ban
For more details about how to use Cache-Control headers with Nginx, see this Nginx Configuration file at the official Mastodon GitHub repository.