Installing Your Mastodon Instance With Docker
December 19, 2022

Installing Your Own Mastodon Instance With Docker Compose
Mastodon is a free, open-source social networking platform that is designed to be decentralised and run on users' own servers. In this post, we will go over how to install Mastodon on a server running Ubuntu 20.10 using Docker.
This tutorial is for people that have some knowledge of admin sys (but not that much is required), you should already have Docker + docker compose up and running.
I decided to write this down as the described process on the official doc was not up-to-date, and also I have an opinionated setup using Traefik that I use for all my docker projects on my server.
Note: this installation is only valid for a one-person server (up to your whole family) but not a publicly open instance. For that matter, you would have to tune multiple option for performance, add backup layer, storage, monitoring, etc.
This has been tested with Mastodon v4.0.2 on December 2022
Docker version 20.10.12 and docker-compose version 1.29.2
Step-by-step guide
Git clone Mastodon
Easy step, we start with cloning the mastodon project on our server
git clone https://github.com/mastodon/mastodon
Note: for some reason, the --link
option in the Dockerfile
did not work, so I simply removed it.
See my updated Dockerfile
below:
# syntax=docker/dockerfile:1.4
# This needs to be bullseye-slim because the Ruby image is built on bullseye-slim
ARG NODE_VERSION="16.17.1-bullseye-slim"
FROM ghcr.io/moritzheiber/ruby-jemalloc:3.0.4-slim as ruby
FROM node:${NODE_VERSION} as build
COPY --from=ruby /opt/ruby /opt/ruby
ENV DEBIAN_FRONTEND="noninteractive" \
PATH="${PATH}:/opt/ruby/bin"
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
WORKDIR /opt/mastodon
COPY Gemfile* package.json yarn.lock /opt/mastodon/
RUN apt update && \
apt-get install -y --no-install-recommends build-essential \
ca-certificates \
git \
libicu-dev \
libidn11-dev \
libpq-dev \
libjemalloc-dev \
zlib1g-dev \
libgdbm-dev \
libgmp-dev \
libssl-dev \
libyaml-0-2 \
ca-certificates \
libreadline8 \
python3 \
shared-mime-info && \
bundle config set --local deployment 'true' && \
bundle config set --local without 'development test' && \
bundle config set silence_root_warning true && \
bundle install -j"$(nproc)" && \
yarn install --pure-lockfile
FROM node:${NODE_VERSION}
ARG UID="991"
ARG GID="991"
COPY --from=ruby /opt/ruby /opt/ruby
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
ENV DEBIAN_FRONTEND="noninteractive" \
PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin"
RUN apt-get update && \
echo "Etc/UTC" > /etc/localtime && \
groupadd -g "${GID}" mastodon && \
useradd -u "$UID" -g "${GID}" -m -d /opt/mastodon mastodon && \
apt-get -y --no-install-recommends install whois \
wget \
procps \
libssl1.1 \
libpq5 \
imagemagick \
ffmpeg \
libjemalloc2 \
libicu67 \
libidn11 \
libyaml-0-2 \
file \
ca-certificates \
tzdata \
libreadline8 \
tini && \
ln -s /opt/mastodon /mastodon
COPY --chown=mastodon:mastodon . /opt/mastodon
COPY --chown=mastodon:mastodon --from=build /opt/mastodon /opt/mastodon
ENV RAILS_ENV="production" \
NODE_ENV="production" \
RAILS_SERVE_STATIC_FILES="true" \
BIND="0.0.0.0"
# Set the run user
USER mastodon
WORKDIR /opt/mastodon
# Precompile assets
RUN OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder rails assets:precompile && \
yarn cache clean
# Set the work dir and the container entry point
ENTRYPOINT ["/usr/bin/tini", "--"]
EXPOSE 3000 4000
The docker-compose.yml
This file is quite different from the official one, it is opinionated, that's why I told you that this specific setup would only work for small instances.
Let's see which decision I made:
- I have a shell container for the solo purpose of executing commands
- I use labels on the streaming and web container to configure Traefik
- I use a volume for the public user data
- I share the
.env.production
file across all containers
docker-compose.yml
version: '3.7'
networks:
traefik_default:
external: true
name: "traefik_default"
volumes:
mastodon-postgres-data:
mastodon-redis-data:
mastodon-web-data:
services:
db:
restart: always
image: postgres:14-alpine
shm_size: 256mb
env_file: .env.production
networks:
- traefik_default
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres']
volumes:
- mastodon-postgres-data:/var/lib/postgresql/data
redis:
restart: always
image: redis:7-alpine
env_file: .env.production
networks:
- traefik_default
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
volumes:
- mastodon-redis-data:/data
web:
build: .
image: tootsuite/mastodon
restart: always
env_file: .env.production
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
networks:
- traefik_default
healthcheck:
test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:3000/health || exit 1']
depends_on:
- db
- redis
volumes:
- mastodon-web-data:/mastodon/public/system
labels:
- "traefik.enable=true"
- "traefik.http.routers.mastodon.rule=Host(`mastodon.test`)"
- "traefik.http.routers.mastodon.tls=true"
- "traefik.http.routers.mastodon-unsecure.rule=Host(`mastodon.test`)"
- "traefik.http.services.mastodon.loadbalancer.server.port=3000"
# use like that: `docker-compose -f docker-compose.yml run --rm shell /bin/bash`
shell:
image: tootsuite/mastodon
env_file: .env.production
command: /bin/bash
restart: "no"
networks:
- traefik_default
depends_on:
- db
- redis
volumes:
- mastodon-web-data:/mastodon/public/system
streaming:
build: .
image: tootsuite/mastodon
restart: always
env_file: .env.production
command: node ./streaming
networks:
- traefik_default
healthcheck:
test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health || exit 1']
depends_on:
- db
- redis
labels:
- "traefik.enable=true"
- "traefik.http.routers.mastodon-api.rule=Host(`mastodon.test`) && PathPrefix(`/api/v1/streaming`)"
- "traefik.http.routers.mastodon-api.tls=true"
- "traefik.http.routers.mastodon-api-unsecure.rule=Host(`mastodon.test`) && PathPrefix(`/api/v1/streaming`)"
- "traefik.http.services.mastodon-api.loadbalancer.server.port=4000"
sidekiq:
build: .
image: tootsuite/mastodon
restart: always
env_file: .env.production
command: bundle exec sidekiq
depends_on:
- db
- redis
networks:
- traefik_default
volumes:
- mastodon-web-data:/mastodon/public/system
healthcheck:
test: ['CMD-SHELL', "ps aux | grep '[s]idekiq\ 6' || false"]
Customise our .env.production file
Copy/past the .env.production
file below and adapt fields for your needs.
# General configuration
LOCAL_DOMAIN=mastodon.test
RAILS_ENV=production
NODE_ENV=production
DEFAULT_LOCALE=en
# Redirect to the first profile
SINGLE_USER_MODE=true
# Concurrency
WEB_CONCURRENCY=2
MAX_THREADS=5
# Redis
REDIS_HOST=redis
REDIS_PORT=6379
# Database
# Postgres side
POSTGRES_USER=mastodon
POSTGRES_DB=mastodon_production
POSTGRES_PASSWORD=change_me_fca92730f229c508e07d5d752076c0
# Application side
DB_HOST=db
DB_USER=mastodon
DB_NAME=mastodon_production
DB_PASS=change_me_fca92730f229c508e07d5d752076c0
DB_PORT=5432
# Secrets
SECRET_KEY_BASE=change_me_d53e9903ba93af1035bc8...
OTP_SECRET=change_me_ec7dc9e021ef40445d6fcc8c80...
# Web Push
VAPID_PRIVATE_KEY=invalid_61da0819c462fe4a5a130b41ba911f=
VAPID_PUBLIC_KEY=invalid_5af5c77a53508911dbebe1c9c...
# Sending mail
SMTP_SERVER=smtp.service.org
SMTP_PORT=587
[email protected]
SMTP_PASSWORD=invalid_c4d9c04ee31a28aaa3a20939
[email protected]
# File storage (optional)
S3_ENABLED=false
S3_BUCKET=files.example.com
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
S3_ALIAS_HOST=files.example.com
# 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
LOCAL_DOMAIN=mastodon.test
You want to specify your domain here.
POSTGRES_PASSWORD=change_me_fca92730f229c508e07d5d752076c0
You have to generate an uniq password, you can useopenssl rand -hex 15
for that matter. This password is used twice in the file (seeDB_PASS
)
SECRET_KEY_BASE=change_me_d53e9903ba93af1035bc8...
andOTP_SECRET=change_me_ec7dc9e021ef40445d6fcc8c80...
You have to specify two unique secrets for those keys. You can do so easily with this command:docker-compose -f docker-compose.yml run --rm shell bundle exec rake secret
VAPID_PRIVATE_KEY=invalid_61da0819c462fe4a5a130b41ba911f=
andVAPID_PUBLIC_KEY=invalid_5af5c77a53508911dbebe1c9c...
You should generate theVAPID
private and public keys with:docker-compose -f docker-compose.yml run --rm shell bundle exec rake mastodon:webpush:generate_vapid_key
Initialise the database
From now, you can start and initialise the database.
First, run docker-compose -f docker-compose.yml run --rm shell bundle exec rake db:setup
when it's done you can continue with: docker-compose -f docker-compose.yml run --rm shell bundle exec rake db:migrate
Create your user
Basically, the setup is done.
Now you can boot the whole project with: docker-compose -f docker-compose.yml up -d
.
Wait a bit, so everything is ready (you can check the status with docker ps
)
Then create your first (and only) user: docker-compose -f docker-compose.yml run --rm shell bin/tootctl accounts create NICKAME --email [email protected] --confirmed --role Owner
Thereafter, you may want to disable registrations: docker-compose -f docker-compose.yml run --rm shell bin/tootctl settings registrations close
Admin tasks for later use
As you already saw, you have access to the tootctl
utility for basic maintenance tasks: docker-compose -f docker-compose.yml run --rm shell tootctl
.
Check the official doc if you need more details on it.
Web server
Most tutorials on the web will help you install and configure a web server, probably nginx, but I, personally, use Traefik for that matter.
You can check out sleeplessbeastie.eu/.../how-to-take-advantage-of-docker-to-install-mastodon where you will find how to configure nginx for your instance or gist.github.com/TrillCyborg where you have some other options.
Still, I recommend you also have a look at traefik, it handles multiples docker projects running, SSL certificates, and more.
You may have noted some weird looking labels
in the docker-compose.yml
file: that's how I configured Traefik for Mastodon, and it works like a charm along my other docker-based project on that same server.
You made it!
That's it! You have successfully installed Mastodon on your server using Docker. You can now use your Mastodon instance to connect with other users and share your thoughts and ideas.
Enjoy!