How to install Mastodon Social Network with Docker on Ubuntu 22.04 LTS
Mastodon is a free, decentralized, and open-source social network. It was created as an alternative to Twitter. Just like Twitter people can follow each other, and post messages, images, and videos. But unlike Twitter, there is no central store or authority for the content.
Instead, Mastodon operates across thousands of different servers each run by various members of the community. Users signed up on one server can easily connect to users on the other network and follow each other across instances.
Anyone can install their instance of a Mastodon server. This tutorial will teach you how to set up your instance of Mastodon on a server with Ubuntu 22.04 using Docker. Docker makes it easy to install Mastodon by containing all the packages and services required in containers.
Prerequisites
-
A server running Ubuntu 22.04 with a minimum of 2 CPU cores and 2GB of memory. You will need to upgrade the server as per requirements.
-
A non-root user with sudo privileges.
-
A fully qualified domain name (FQDN) pointing to your server. For our purposes, we will use
mastodon.example.comas the domain name. -
Mastodon sends email notifications to users. We recommend you use a 3rd party Transactional mail service like Mailgun, Sendgrid, Amazon SES, or Sparkpost. The instructions in the guide will be using Amazon SES.
-
Make sure everything is updated.
$ sudo apt update
-
Install basic utility packages. Some of them may already be installed.
$ sudo apt install wget curl nano software-properties-common dirmngr apt-transport-https gnupg gnupg2 ca-certificates lsb-release ubuntu-keyring unzip -y
Step 1 - Configure Firewall
The first step is to configure the firewall. Ubuntu comes with ufw (Uncomplicated Firewall) by default.
Check if the firewall is running.
$ sudo ufw status
You should get the following output.
Status: inactive
Allow SSH port so the firewall doesn't break the current connection on enabling it.
$ sudo ufw allow OpenSSH
Allow HTTP and HTTPS ports as well.
$ sudo ufw allow http $ sudo ufw allow https
Enable the Firewall
$ sudo ufw enable Command may disrupt existing ssh connections. Proceed with operation (y|n)? y Firewall is active and enabled on system startup
Check the status of the firewall again.
$ sudo ufw status
You should see a similar output.
Status: active To Action From -- ------ ---- OpenSSH ALLOW Anywhere 80/tcp ALLOW Anywhere 443 ALLOW Anywhere OpenSSH (v6) ALLOW Anywhere (v6) 80/tcp (v6) ALLOW Anywhere (v6) 443 (v6) ALLOW Anywhere (v6)
Step 2 - Install Docker and Docker Compose
Ubuntu 22.04 ships with an older version of Docker. To install the latest version, first, import the Docker GPG key.
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
Create Docker repository file.
$ echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Update the system repository list.
$ sudo apt update
Install the latest version of Docker.
$ sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin
Verify that it is running.
$ sudo systemctl status docker
? docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
Active: active (running) since Fri 2022-12-30 15:55:17 UTC; 29min ago
TriggeredBy: ? docker.socket
Docs: https://docs.docker.com
Main PID: 1966 (dockerd)
Tasks: 8
Memory: 20.8M
CPU: 740ms
CGroup: /system.slice/docker.service
??1966 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
By default, Docker requires root privileges. If you want to avoid using sudo every time you run the docker command, add your username to the docker group.
$ sudo usermod -aG docker $(whoami)
You will need to log out of the server and back in as the same user to enable this change or use the following command.
$ su - ${USER}
Confirm that your user is added to the Docker group.
$ groups navjot wheel docker
Step 3 - Preparing for Installation
The default limit of the mmap counts is very low for Elasticsearch. Run the following command to check the default value.
$ sysctl vm.max_map_count
You will get the following output.
vm.max_map_count = 65530
Increase the value using the following commands.
$ echo "vm.max_map_count=262144" | sudo tee /etc/sysctl.d/90-max_map_count.conf $ sudo sysctl --load /etc/sysctl.d/90-max_map_count.conf
Step 4 - Install Mastodon
Create Directories and Set Ownerships
Create directories for Mastodon and related services.
$ sudo mkdir -p /opt/mastodon/database/{postgresql,pgbackups,redis,elasticsearch}
$ sudo mkdir -p /opt/mastodon/web/{public,system}
$ sudo mkdir -p /opt/mastodon/branding
Set proper ownerships to the Elasticsearch, web, and backup directories.
$ sudo chown 991:991 /opt/mastodon/web/{public,system}
$ sudo chown 1000 /opt/mastodon/database/elasticsearch
$ sudo chown 70:70 /opt/mastodon/database/pgbackups
Switch to the Mastodon directory.
$ cd /opt/mastodon
Create environment and docker compose files
Create environment files for the application and the database.
$ sudo touch application.env database.env
Create and open the Docker compose file for editing.
$ sudo nano docker-compose.yml
Paste the following code in it.
version: '3'
services:
postgresql:
image: postgres:15-alpine
env_file: database.env
restart: always
shm_size: 512mb
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres']
volumes:
- postgresql:/var/lib/postgresql/data
- pgbackups:/backups
networks:
- internal_network
redis:
image: redis:7-alpine
restart: always
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
volumes:
- redis:/data
networks:
- internal_network
redis-volatile:
image: redis:7-alpine
restart: always
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
networks:
- internal_network
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.7
restart: always
env_file: database.env
environment:
- cluster.name=elasticsearch-mastodon
- discovery.type=single-node
- bootstrap.memory_lock=true
- xpack.security.enabled=true
- ingest.geoip.downloader.enabled=false
- "ES_JAVA_OPTS=-Xms512m -Xmx512m -Des.enforce.bootstrap.checks=true"
- xpack.license.self_generated.type=basic
- xpack.watcher.enabled=false
- xpack.graph.enabled=false
- xpack.ml.enabled=false
- thread_pool.write.queue_size=1000
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65536
hard: 65536
healthcheck:
test: ["CMD-SHELL", "nc -z elasticsearch 9200"]
volumes:
- elasticsearch:/usr/share/elasticsearch/data
networks:
- internal_network
ports:
- '127.0.0.1:9200:9200'
website:
image: tootsuite/mastodon:v4.0.2
env_file:
- application.env
- database.env
command: bash -c "bundle exec rails s -p 3000"
restart: always
depends_on:
- postgresql
- redis
- redis-volatile
- elasticsearch
ports:
- '127.0.0.1:3000:3000'
networks:
- internal_network
- external_network
healthcheck:
test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:3000/health || exit 1']
volumes:
- uploads:/mastodon/public/system
shell:
image: tootsuite/mastodon:v4.0.2
env_file:
- application.env
- database.env
command: /bin/bash
restart: "no"
networks:
- internal_network
- external_network
volumes:
- uploads:/mastodon/public/system
- static:/static
streaming:
image: tootsuite/mastodon:v4.0.2
env_file:
- application.env
- database.env
command: node ./streaming
restart: always
depends_on:
- postgresql
- redis
- redis-volatile
- elasticsearch
ports:
- '127.0.0.1:4000:4000'
networks:
- internal_network
- external_network
healthcheck:
test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health || exit 1']
sidekiq:
image: tootsuite/mastodon:v4.0.2
env_file:
- application.env
- database.env
command: bundle exec sidekiq
restart: always
depends_on:
- postgresql
- redis
- redis-volatile
- website
networks:
- internal_network
- external_network
healthcheck:
test: ['CMD-SHELL', "ps aux | grep '[s]idekiq\ 6' || false"]
volumes:
- uploads:/mastodon/public/system
networks:
external_network:
internal_network:
internal:true
volumes:
postgresql:
driver_opts:
type: none
device: /opt/mastodon/database/postgresql
o: bind
pgbackups:
driver_opts:
type: none
device: /opt/mastodon/database/pgbackups
o: bind
redis:
driver_opts:
type: none
device: /opt/mastodon/database/redis
o: bind
elasticsearch:
driver_opts:
type: none
device: /opt/mastodon/database/elasticsearch
o: bind
uploads:
driver_opts:
type: none
device: /opt/mastodon/web/system
o: bind
static:
driver_opts:
type: none
device: /opt/mastodon/web/public
o: bind
Save the file by pressing Ctrl + X and entering Y when prompted.
At the time of writing the tutorial, the latest available version of Mastodon is v4.0.2. Check the Mastodon GitHub Releases page and adjust the version in the Docker compose file appropriately. We are also using the latest versions of PostgreSQL and Redis. You can adjust them as per your requirements. We are using Elasticsearch 7.x at the moment. There is no major version of Elasticsearch you can follow on the Docker Hub page, so you will have to keep updating it manually for the security updates related to Java.
Create Application Secrets
The next step is to create application secret values.
Generate SECRET_KEY_BASE and OTP_SECRET values by running the following command twice. The first time will take some time as it will pull the images.
$ docker compose run --rm shell bundle exec rake secret
You can also use the openssl utility for the same.
$ openssl rand -hex 64
Generate VAPID_PRIVATE_KEY and VAPID_PUBLIC_KEY values by using the following command.
$ docker compose run --rm shell bundle exec rake mastodon:webpush:generate_vapid_key
You will get a similar output.
VAPID_PRIVATE_KEY=u2qsCs5JdmdmMLnUuU0sgmFGvZedteJz-lFB_xF4_ac= VAPID_PUBLIC_KEY=BJXjE2hIXvFpo6dnHqyf1i-2PcP-cBoL95UCmhhxwlAgtFw_vnrYp4GBneR7_cmI9LZUYjHFh-TBAPSb9WTqH9A=
Use the openssl utility to generate PostgreSQL and Elasticsearch passwords.
$ openssl rand -hex 15
Mastodon Environment Files
Open the application.env file for editing.
$ sudo nano application.env
Paste the following lines in it.
# environment RAILS_ENV=production NODE_ENV=production # domain LOCAL_DOMAIN=mastodon.example.com # redirect to the first profile SINGLE_USER_MODE=false # do not serve static files RAILS_SERVE_STATIC_FILES=false # concurrency WEB_CONCURRENCY=2 MAX_THREADS=5 # pgbouncer #PREPARED_STATEMENTS=false # locale DEFAULT_LOCALE=en # email, not used SMTP_SERVER=email-smtp.us-west-2.amazonaws.com SMTP_PORT=587 SMTP_LOGIN=AES_USER SMTP_PASSWORD=AES_PWD [email protected] # secrets SECRET_KEY_BASE=c09fa403575e0b431e54a2e228f20cd5a5fdfdbba0da80598959753b829a4e3c0266eedbac7e3cdf9f3345db36c56302c0e1bc5bfc8c5d516be59a2c41de7e37 OTP_SECRET=febb7dbb0d3308094083733fc923a430e52ccec767d48d7d2e0c577bfcb6863dbdfc920b1004b1f8c2967b9866bd7a0b4a15460f9fc7687aa4a42acf54e5a3d4 # Changing VAPID keys will break push notifications VAPID_PRIVATE_KEY=13RgrfOY2tkwuUycylDPOkoHennkJ0ZAPV_fUwDy7-g= VAPID_PUBLIC_KEY=BDAQuGwPbh1kbCV904adYXHvz9lLRaJHkiQkihRDPyBn3QmkAYbR21WHYoP8TkyG6dylG6IXpEVfLwdoW7fJVns= # 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=2592000 SESSION_RETENTION_PERIOD=2592000
Save the file by pressing Ctrl + X and entering Y when prompted.
We have enabled the Amazon SES mailing service. If you don't need it, you can delete the section. By default, Mastodon retains an IP address for 1 year, but we have changed it to 30 days (2592000 seconds). You can change it as per your requirement. Make sure to keep it for more than 2 days otherwise you will need to do a little more tinkering which is out of the scope of our tutorial.
Open the database.env file for editing.
$ sudo nano database.env
Paste the following lines in it.
# postgresql configuration POSTGRES_USER=mastodon POSTGRES_DB=mastodon POSTGRES_PASSWORD=15ff12dcb93aa60680d2aadb4032ee PGPASSWORD=15ff12dcb93aa60680d2aadb4032ee PGPORT=5432 PGHOST=postgresql PGUSER=mastodon # pgbouncer configuration #POOL_MODE=transaction #ADMIN_USERS=postgres,mastodon #DATABASE_URL="postgres://mastodon:15ff12dcb93aa60680d2aadb4032ee@postgresql:5432/mastodon" # elasticsearch ES_JAVA_OPTS=-Xms512m -Xmx512m ELASTIC_PASSWORD=13382e99f6b2d4dc7f3d66e4b9872d # mastodon database configuration #DB_HOST=pgbouncer DB_HOST=postgresql DB_USER=mastodon DB_NAME=mastodon DB_PASS=15ff12dcb93aa60680d2aadb4032ee DB_PORT=5432 REDIS_HOST=redis REDIS_PORT=6379 CACHE_REDIS_HOST=redis-volatile CACHE_REDIS_PORT=6379 ES_ENABLED=true ES_HOST=elasticsearch ES_PORT=9200 ES_USER=elastic ES_PASS=13382e99f6b2d4dc7f3d66e4b9872d
Save the file by pressing Ctrl + X and entering Y when prompted.
Prepare Mastodon
Get the static files ready to be served by Nginx. This step is going to take some time because Docker will pull all the images for the first time.
$ docker compose run --rm shell bash -c "cp -r /opt/mastodon/public/* /static/"
Bring up the data layer.
$ docker compose up -d postgresql redis redis-volatile
Check the status of the containers.
$ watch docker compose ps
Wait for running (healthy), then press Ctrl + C and initialize the database using the following command.
$ docker compose run --rm shell bundle exec rake db:setup
Step 5 - Install Nginx
Ubuntu 22.04 ships with an older version of Nginx. To install the latest version, you need to download the official Nginx repository.
Import Nginx's signing key.
$ curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \ | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null
Add the repository for Nginx's stable version.
$ echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg arch=amd64] \
http://nginx.org/packages/ubuntu `lsb_release -cs` nginx" \
| sudo tee /etc/apt/sources.list.d/nginx.list
Update the system repositories.
$ sudo apt update
Install Nginx.
$ sudo apt install nginx
Verify the installation.
$ nginx -v nginx version: nginx/1.22.1
Start the Nginx server.
$ sudo systemctl start nginx
Check the status of the server.
$ sudo systemctl status nginx
? nginx.service - nginx - high performance web server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
Active: active (running) since Sat 2022-12-31 02:23:12 UTC; 6s ago
Docs: https://nginx.org/en/docs/
Process: 22129 ExecStart=/usr/sbin/nginx -c /etc/nginx/nginx.conf (code=exited, status=0/SUCCESS)
Main PID: 22130 (nginx)
Tasks: 3 (limit: 4575)
Memory: 2.5M
CPU: 17ms
CGroup: /system.slice/nginx.service
??22130 "nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf"
??22131 "nginx: worker process" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""
??22132 "nginx: worker process" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""
Step 6 - Install SSL
We need to install Certbot to generate the SSL certificate. You can either install Certbot using Ubuntu's repository or grab the latest version using the Snapd tool. We will be using the Snapd version.
Ubuntu 22.04 comes with Snapd installed by default. Run the following commands to ensure that your version of Snapd is up to date. Ensure that your version of Snapd is up to date.
$ sudo snap install core $ sudo snap refresh core
Install Certbot.
$ sudo snap install --classic certbot
Use the following command to ensure that the Certbot command runs by creating a symbolic link to the /usr/bin directory.
$ sudo ln -s /snap/bin/certbot /usr/bin/certbot
Run the following