Some years ago, I would like to setup Mastodon on my own server to try it but I gave up because I thought it was too complicated.

I re-tried and used a docker stack to do it. Let’s see how.

Prerequisites

I assume you have docker and docker-compose installed, with a reverse-proxy like traefik, haproxy, nginx, or anything else in front to handle your SSL connection.

docker-compose file

You will find a docker-compose.yaml file sample on Mastodon’s github repository: https://github.com/mastodon/mastodon/blob/main/docker-compose.yml

I updated it to fit my needs:

  • Use official mastodon image with a tag instead of building my own image
  • Use docker volumes instead of mount local directories
  • Define labels for my local traefik instance (for web and for streaming setvices)
  • Create a dedicated network called mastodon instead of their external_network

Here is my docker-compose.yaml:

version: '3'
services:
  db:
    restart: always
    image: postgres:14-alpine
    shm_size: 256mb
    networks:
      - internal_network
    healthcheck:
      test: ['CMD', 'pg_isready', '-U', 'postgres']
    volumes:
      - mastodon_pg:/var/lib/postgresql/data
    environment:
      - 'POSTGRES_HOST_AUTH_METHOD=trust'
      - 'POSTGRES_USER=mastodon'
      - 'POSTGRES_PASSWORD=a-strong-password'

  redis:
    restart: always
    image: redis:7-alpine
    networks:
      - internal_network
    healthcheck:
      test: ['CMD', 'redis-cli', 'ping']
    volumes:
      - mastodon_redis:/data

  es:
    restart: always
    image: docker.elastic.co/elasticsearch/elasticsearch:7.17.4
    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=es-mastodon"
      - "discovery.type=single-node"
      - "thread_pool.write.queue_size=1000"
    networks:
       - mastodon
       - internal_network
    healthcheck:
       test: ["CMD-SHELL", "curl --silent --fail localhost:9200/_cluster/health || exit 1"]
    volumes:
       - mastodon_es:/usr/share/elasticsearch/data
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    #ports:
    #  - '127.0.0.1:9200:9200'

  web:
    image: tootsuite/mastodon:v4.0.2
    restart: always
    env_file: .env.production
    command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
    networks:
      - mastodon
      - internal_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:
      - mastodon_public_system:/opt/mastodon/public/system
    labels:
      traefik.enable: "true"
      traefik.http.routers.mastodon-http.entrypoints: "web"
      traefik.http.routers.mastodon-http.rule: "Host(`mastodon.open-web.fr`)"
      traefik.http.routers.mastodon-http.middlewares: "SslHeader@file"
      traefik.http.routers.mastodon-https.middlewares: "SslHeader@file"
      traefik.http.routers.mastodon-https.entrypoints: "websecure"
      traefik.http.routers.mastodon-https.rule: "Host(`mastodon.open-web.fr`)"
      traefik.http.routers.mastodon-https.tls: "true"
      traefik.http.routers.mastodon-https.tls.certresolver: "letsencrypt"
      traefik.http.services.mastodon-https.loadbalancer.server.port: 3000

  streaming:
    image: tootsuite/mastodon:v4.0.2
    restart: always
    env_file: .env.production
    command: node ./streaming
    networks:
      - mastodon
      - 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
    labels:
      traefik.enable: "true"
      traefik.http.routers.mastodonstream-http.entrypoints: "web"
      traefik.http.routers.mastodonstream-http.rule: "(Host(`mastodon.open-web.fr`) && PathPrefix(`/api/v1/streaming`))"
      traefik.http.routers.mastodonstream-http.middlewares: "SslHeader@file"
      traefik.http.routers.mastodonstream-https.middlewares: "SslHeader@file"
      traefik.http.routers.mastodonstream-https.entrypoints: "websecure"
      traefik.http.routers.mastodonstream-https.rule: "(Host(`mastodon.open-web.fr`) && PathPrefix(`/api/v1/streaming`))"
      traefik.http.routers.mastodonstream-https.tls: "true"
      traefik.http.routers.mastodonstream-https.tls.certresolver: "letsencrypt"
      traefik.http.services.mastodonstream-https.loadbalancer.server.port: 4000

  sidekiq:
    image: tootsuite/mastodon:v4.0.2
    restart: always
    env_file: .env.production
    command: bundle exec sidekiq
    depends_on:
      - db
      - redis
    networks:
      - mastodon
      - internal_network
    volumes:
      - mastodon_public_system:/opt/mastodon/public/system
    healthcheck:
      test: ['CMD-SHELL', "ps aux | grep '[s]idekiq\ 6' || false"]

  ## Uncomment to enable federation with tor instances along with adding the following ENV variables
  ## http_proxy=http://privoxy:8118
  ## ALLOW_ACCESS_TO_HIDDEN_SERVICE=true
  # tor:
  #   image: sirboops/tor
  #   networks:
  #      - mastodon
  #      - internal_network
  #
  # privoxy:
  #   image: sirboops/privoxy
  #   volumes:
  #     - ./priv-config:/opt/config
  #   networks:
  #     - mastodon
  #     - internal_network

volumes:
  mastodon_es:
  mastodon_public_system:
  mastodon_pg:
  mastodon_redis:

networks:
  mastodon:
    external:
      name: mastodon
  internal_network:
    internal: tru

sysctl configuration for elasticsearch

To let elasticsearch works, you need to define vm.max_map_count sysctl setting. Create a /etc/sysctl.d/99-mastodon-es.conf file:

cat << EOF > /etc/sysctl.d/99-mastodon-es.conf
vm.max_map_count = 262144
EOF

Then apply the new configuration:

sysctl --system

First setup

Before launch docker-compose up -d, you need to initialize the database and generate a .env.production file.

Create an empty .env.production file, we will fill it later

touch .env.production

Next, in the command below:

  • You will reply to some questions such as your instance url, name, etc.
  • You will populate your database
  • Your .env.production will be generated and displayed

Let’s go:

docker-compose run --rm web bash -c "bundle exec rake mastodon:setup && cat .env.production"

The above command will display your .env.production content. Fill your .env.production file with this :-)

Launch your instance

You can now launch your mastodon instance with docker-compose up -d

Backup your instance

Having a backup strategy is very important! Read the fucking documentation for this: https://docs.joinmastodon.org/admin/backups/

Upgrade your instance

It is also important to let your instance up-to-date. Enable notification from the Mastodon Github Repository and read the releases notes for upgrade instructions.

Most of the time, upgrade process will be to increase your mastodon docker image version then run these commands:

docker-compose pull
docker-compose run --rm -e SKIP_POST_DEPLOYMENT_MIGRATIONS=true web rails db:migrate
docker-compose run --rm web rails db:migrate
docker-compose up -d