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 use openssl rand -hex 15 for that matter. This password is used twice in the file (see DB_PASS)
     
  • SECRET_KEY_BASE=change_me_d53e9903ba93af1035bc8... and OTP_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= and VAPID_PUBLIC_KEY=invalid_5af5c77a53508911dbebe1c9c...
    You should generate the VAPID 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!