Mastodon Setup with Docker and nginx-proxy

Mastodon Setup with Docker and nginx-proxy

Learn how to setup a Mastodon server using Docker and nginx-proxy. This setup comes with automated SSL.

I have been working on a setup with Mastodon that is easy to repeat and share. A setup with very few steps. Please consider that this setup is not enough for a production environment. It requires additional security measures. Please put your recommendations in the comments! :grinning:

Our starting point is the docker-compose.yml shipped with the Mastodon code. Why is it not enough? It assumes you setup up proxy with HTTPS endpoints yourself. So let’s integrate this as well in Docker.

Consider also the compact setup with the Caddy webserver.

Setup

Few remarks to start with:

  • my testing system: Ubuntu 20.04.3 LTS (GNU/Linux 5.4.0-97-generic x86_64)
  • install first some software with apt install docker docker.io jq git
  • create an unprivileged user account, e.g. mastodon

    adduser --disabled-login mastodon
    adduser mastodon docker
    adduser mastodon sudo # optional, remove later
    su mastodon # switch to that user
    
  • my docker compose: Docker Compose version v2.2.3 (based on go)

    install docker compose in 3 lines:

    mkdir -p ~/.docker/cli-plugins
    curl -sSL https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose
    chmod +x ~/.docker/cli-plugins/docker-compose
    
  • my testing domain (for this example): social.host

  • my dot-env file .env for docker compose:

    LETS_ENCRYPT_EMAIL=admin-mail@social.host
    MASTODON_DOMAIN=social.host
    
  • I have commented out build: ., because I prefer to rely on the official images from Docker Hub.

  • With little effort, I enable as well full-text search with elasticsearch.

  • The support of VIRTUAL_PATH is brand-new in nginx-proxy. It is not yet in the main branch, so that we rely on nginxproxy/nginx-proxy:dev-alpine.

  • The Mastodon code also ships an nginx configuration. However, nginx-proxy creates much of it as well, so that I currently believe no further configuration is required here. However, nginx-proxy allows to add custom elements to the generated configuration.

  • The setup places all databases and uploaded files in the folder mastodon
# file: 'docker-compose.yml'
version: "3.7"

services:
  nginx-proxy:
    image: nginxproxy/nginx-proxy:dev-alpine
    container_name: nginx-proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf:/etc/nginx/conf.d
      - ./nginx/vhost:/etc/nginx/vhost.d
      - html:/usr/share/nginx/html
      - ./nginx/certs:/etc/nginx/certs:ro
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - ./nginx/logs:/var/log/nginx
    networks:
      - external_network
      - internal_network

  acme-companion:
    image: nginxproxy/acme-companion
    container_name: nginx-proxy-acme
    volumes_from:
      - nginx-proxy
    volumes:
      - ./nginx/certs:/etc/nginx/certs:rw
      - ./nginx/acme:/etc/acme.sh
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      DEFAULT_EMAIL: "${LETS_ENCRYPT_EMAIL}"
    networks:
      - external_network

  db:
    restart: always
    image: postgres:14-alpine
    shm_size: 256mb
    networks:
      - internal_network
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "postgres"]
    volumes:
      - .mastodon//postgres14:/var/lib/postgresql/data
    environment:
      POSTGRES_HOST_AUTH_METHOD: trust

  redis:
    restart: always
    image: redis:6-alpine
    networks:
      - internal_network
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
    volumes:
      - ./mastodon/redis:/data

  # elasticsearch
  es:
    restart: always
    image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.8.10
    environment:
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - "cluster.name=es-mastodon"
      - "discovery.type=single-node"
      - "bootstrap.memory_lock=true"
    networks:
      - internal_network
    healthcheck:
      test: ["CMD-SHELL", "curl --silent --fail localhost:9200/_cluster/health || exit 1"]
    volumes:
      - ./mastodon/elasticsearch:/usr/share/elasticsearch/data
    ulimits:
      memlock:
        soft: -1
        hard: -1

  web:
    # build: .
    image: tootsuite/mastodon:v3.4.6
    restart: always
    env_file: mastodon.env.production
    command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
    networks:
      - external_network
      - internal_network
    healthcheck:
      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:/mastodon/public/system
    environment:
      VIRTUAL_HOST: "${MASTODON_DOMAIN}"
      VIRTUAL_PATH: "/"
      VIRTUAL_PORT: 3000
      LETSENCRYPT_HOST: "${MASTODON_DOMAIN}"
      ES_HOST: mastodon-elastic
      ES_ENABLED: true

  streaming:
    # build: .
    image: tootsuite/mastodon:v3.4.6
    restart: always
    env_file: mastodon.env.production
    command: node ./streaming
    networks:
      - external_network
      - internal_network
    healthcheck:
      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
    environment:
      VIRTUAL_HOST: "${MASTODON_DOMAIN}"
      VIRTUAL_PATH: "/api/v1/streaming"
      VIRTUAL_PORT: 4000

  sidekiq:
    # build: .
    image: tootsuite/mastodon:v3.4.6
    restart: always
    env_file: mastodon.env.production
    command: bundle exec sidekiq
    depends_on:
      - db
      - redis
    networks:
      # - external_network
      - internal_network
    volumes:
      - ./mastodon/public/system:/mastodon/public/system

volumes:
  html:

networks:
  external_network:
  internal_network:
    internal: true

With this file in place, create a few more folders and launch the setup of the instance. If the instance has been setup before, a database setup may be enough.

# mastodon
touch mastodon.env.production
sudo chown 991:991 mastodon.env.production
mkdir -p mastodon/public
sudo chown -R 991:991 mastodon/public
mkdir -p mastodon/elasticsearch
sudo chmod g+rwx mastodon/elasticsearch
sudo chgrp 0 mastodon/elasticsearch

# first time: setup mastodon
# https://github.com/mastodon/mastodon/issues/16353 (on RUBYOPT)
docker compose run --rm -v $(pwd)/mastodon.env.production:/opt/mastodon/.env.production -e RUBYOPT=-W0 web bundle exec rake mastodon:setup

# subsequent times: skip generation of config and only setup database
docker compose run --rm -v $(pwd)/mastodon.env.production:/opt/mastodon/.env.production web bundle exec rake db:setup

# launch mastodon
docker compose run -d

# look into the logs, -f for live logs
docker compose logs -f

Mastodon Twitter Crossposter

To setup the Mastodon Twitter Poster for crossposting, add the following services to the docker-compose.yml

crossposter-db:
  restart: always
  image: postgres:14-alpine
  container_name: "crossposter-db"
  healthcheck:
    test: pg_isready -U postgres
  environment:    
    POSTGRES_HOST_AUTH_METHOD: trust
  volumes:
    - ./crossposter/postgres:/var/lib/postgresql/data
  networks:
    - internal_network

crossposter-redis:
  restart: always
  image: redis:6.0-alpine
  container_name: "crossposter-redis"
  healthcheck:
    test: redis-cli ping
  volumes:
    - ./crossposter/redis:/data
  networks:
    - internal_network

crossposter-web:
  restart: always
  build: https://github.com/renatolond/mastodon-twitter-poster.git#main
  image: mastodon-twitter-poster
  container_name: "crossposter-web"
  env_file: crossposter.env.production
  environment:
    ALLOWED_DOMAIN: "${MASTODON_DOMAIN}"
    DB_HOST: crossposter-db
    REDIS_URL: "redis://crossposter-redis"
  networks:
    - internal_network
    - external_network
  expose:
    - "3000"
  depends_on:
    - crossposter-db

crossposter-sidekiq:
  restart: always
  build: https://github.com/renatolond/mastodon-twitter-poster.git#main
  image: mastodon-twitter-poster
  container_name: "crossposter-sidekiq"
  env_file: crossposter.env.production
  environment:
    ALLOWED_DOMAIN: "${MASTODON_DOMAIN}"
    REDIS_URL: "redis://crossposter-redis"
    DB_HOST: crossposter-db
  command: bundle exec sidekiq -c 5 -q default
  healthcheck:
    test: ps aux | grep '[s]idekiq\ 6' || false
  networks:
    # - external_network
    - internal_network
  depends_on:
    - crossposter-db
    - crossposter-redis

The crossposter requires a database setup before the containers can be launched:

docker-compose run --rm crossposter-web bundle exec rake db:setup

References