This guide provides a step-by-step walkthrough for deploying a production-ready Mastodon instance. We will use the official Mastodon Docker images and configure them to run securely behind our modern Traefik v3 reverse proxy, which handles TLS, security headers, and threat protection via CrowdSec.

This setup ensures a robust, scalable, and secure social media platform that you control.

Changelog

DateChange
2025-10-30Initial Version: Guide created, adapting the official Mastodon Docker setup for a modern Traefik v3 and CrowdSec security stack.

1. Prerequisites

This guide builds upon a secure and pre-existing Docker environment. Before you begin, you must have a fully functional Traefik v3 and CrowdSec stack.

โš ๏ธ HARD REQUIREMENT

The following steps will not work correctly without the Traefik stack running as described in the prerequisite guide.

You will also need:

  • A dedicated domain for your Mastodon instance (e.g., mastodon.your-domain.com) pointed to your serverโ€™s IP address.
  • sudo or root access.
  • A functional email (SMTP) server for sending transactional emails.

2. Directory Structure

First, create a dedicated directory for your Mastodon configuration and data. This structure will hold all necessary files and persistent data.

# Create the main directory
sudo mkdir -p /opt/containers/mastodon
cd /opt/containers/mastodon

# Create directories for persistent data
sudo mkdir -p postgres redis public/system

# Mastodon runs as user 991. We need to set the correct permissions.
sudo chown -R 991:991 public/system

3. Configuration

Configuring Mastodon involves generating a configuration file using an interactive wizard and then creating our Docker Compose file to orchestrate the services.

3.1. Docker Compose (docker-compose.yml)

First, create the docker-compose.yml file. We create this file before running the setup wizard so that Docker knows which image to use.

sudo tee docker-compose.yml > /dev/null << 'EOF'
services:
  db:
    image: postgres:14-alpine
    restart: unless-stopped
    shm_size: 256mb
    networks:
      - mastodon-net
    healthcheck:
      test: ['CMD', 'pg_isready', '-U', 'postgres']
    volumes:
      - ./postgres:/var/lib/postgresql/data
    environment:
      - 'POSTGRES_HOST_AUTH_METHOD=trust'

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    networks:
      - mastodon-net
    healthcheck:
      test: ['CMD', 'redis-cli', 'ping']
    volumes:
      - ./redis:/data

  web:
    image: ghcr.io/mastodon/mastodon:v4.4.8
    container_name: mastodon-web
    restart: unless-stopped
    env_file: .env.production
    command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
    networks:
      - proxy
      - mastodon-net
    healthcheck:
      test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:3000/health || exit 1']
    depends_on:
      - db
      - redis
    user: 991:991
    volumes:
      - ./public/system:/mastodon/public/system
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.mastodon-web.rule=Host(`${MASTODON_HOST}`)"
      - "traefik.http.routers.mastodon-web.entrypoints=websecure"
      - "traefik.http.routers.mastodon-web.tls.certresolver=tls_resolver"
      - "traefik.http.routers.mastodon-web.middlewares=security-headers@file,crowdsec-bouncer@docker"
      - "traefik.http.services.mastodon-web.loadbalancer.server.port=3000"
      - "traefik.docker.network=proxy"

  streaming:
    image: ghcr.io/mastodon/mastodon:v4.4.8 # Note: Since v4.3.0, streaming is part of the main image
    container_name: mastodon-streaming
    restart: unless-stopped
    env_file: .env.production
    command: node ./streaming
    networks:
      - proxy
      - mastodon-net
    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-streaming.rule=Host(`${MASTODON_HOST}`) && PathPrefix(`/api/v1/streaming`)"
      - "traefik.http.routers.mastodon-streaming.entrypoints=websecure"
      - "traefik.http.routers.mastodon-streaming.tls.certresolver=tls_resolver"
      - "traefik.http.routers.mastodon-streaming.middlewares=security-headers@file,crowdsec-bouncer@docker"
      - "traefik.http.services.mastodon-streaming.loadbalancer.server.port=4000"
      - "traefik.docker.network=proxy"

  sidekiq:
    image: ghcr.io/mastodon/mastodon:v4.4.8
    container_name: mastodon-sidekiq
    restart: unless-stopped
    env_file: .env.production
    command: bundle exec sidekiq
    depends_on:
      - db
      - redis
    networks:
      - mastodon-net
    volumes:
      - ./public/system:/mastodon/public/system
    healthcheck:
      test: ['CMD-SHELL', "ps aux | grep '[s]idekiq' || false"]
    user: 991:991

networks:
  proxy:
    external: true
  mastodon-net:
    driver: bridge
EOF
โ„น๏ธ A NOTE ON DATABASE SECURITY

The POSTGRES_HOST_AUTH_METHOD=trust setting is convenient and generally safe within an isolated Docker network where no ports are exposed to the outside world. It allows other containers within the same docker-compose.yml to connect without a password. For production environments with stricter security requirements, you should configure proper password-based authentication.

๐Ÿ’ก TRAEFIK LABELS EXPLAINED

The labels section is crucial. It tells our Traefik proxy how to handle requests for Mastodon.

  • tls.certresolver=tls_resolver: This uses the Letโ€™s Encrypt resolver we defined in our main Traefik stack.
  • middlewares=security-headers@file,crowdsec-bouncer@docker: This is the key integration. It applies our predefined security headers and protects the instance with the CrowdSec bouncer, blocking malicious IPs.

3.2. Environment File (.env)

Create a simple .env file to store your domain name. The docker-compose.yml will use this.

sudo tee .env > /dev/null << 'EOF'
MASTODON_HOST=mastodon.your-domain.com
EOF
โš ๏ธ IMPORTANT

Replace mastodon.your-domain.com with your actual domain. This file is separate from the main Mastodon configuration (.env.production) that we will generate next.

3.3. Mastodon Setup Wizard

Mastodon provides an interactive setup wizard to generate its main configuration file (.env.production). Letโ€™s run it.

# This command runs the setup task in a temporary container
sudo docker compose run --rm web bundle exec rake mastodon:setup

The wizard will ask you a series of questions. Here are some recommendations:

  • Domain name: Enter the same domain you used in your .env file (e.g., mastodon.your-domain.com).
  • Single user mode?: n (unless you want a private, single-person instance).
  • Are you using Docker?: y. This will correctly set the database and Redis hostnames.
  • Database host: It should default to db. Press Enter.
  • Redis host: It should default to redis. Press Enter.
  • SMTP settings: Provide the details for your email server. Mastodon needs this to send confirmation emails.
  • Save configuration?: y. This will create the .env.production file.

4. Launch the Stack

Now that all configuration is in place, you can start your Mastodon instance.

4.1. Initial Database Migration

Before the first full launch, we need to prepare the database.

# Run the database migration in a temporary container
sudo docker compose run --rm web rails db:migrate

4.2. Start All Services

With the database ready, bring the entire stack online.

sudo docker compose up -d

The first launch will take a few minutes as Docker downloads the images. You can monitor the progress with sudo docker compose logs -f.

5. Post-Installation Steps

Your instance is running, but you need to create an admin account.

# Use this command to create an admin user interactively
sudo docker compose run --rm web tootctl accounts create YOUR_USERNAME \
  --email YOUR_EMAIL@example.com --confirmed --role Admin

Replace YOUR_USERNAME and YOUR_EMAIL@example.com with your desired credentials. You will be prompted to set a password.

You can now navigate to https://mastodon.your-domain.com and log in with your new admin account.

6. Maintenance

Updating Mastodon

Updating is a straightforward process. First, check the official Mastodon release notes for any special instructions.

cd /opt/containers/mastodon

# Update the image tags in your docker-compose.yml to the new version

# Pull the new images
sudo docker compose pull

# Stop the services before migrating the database
sudo docker compose down

# Run database migrations required by the new version
sudo docker compose run --rm web rails db:migrate

# Start the stack again
sudo docker compose up -d --remove-orphans

Backing Up

A complete backup consists of the database, user-uploaded files, and Redis data.

cd /opt/containers/mastodon

# 1. Back up the database
sudo docker compose exec -T db pg_dump -U postgres -d mastodon_production | gzip > mastodon_db_backup_$(date +%F).sql.gz

# 2. Back up user-uploaded files
sudo tar -czvf mastodon_files_backup_$(date +%F).tar.gz public/system

# 3. Back up Redis data
sudo tar -czvf mastodon_redis_backup_$(date +%F).tar.gz redis

Conclusion

You have successfully deployed a production-ready Mastodon instance. By leveraging Docker for containerization and integrating with a modern Traefik and CrowdSec stack, your instance is not only scalable and easy to manage but also benefits from robust, centralized security and TLS management.

๐Ÿ“šOFFICIAL MASTODON DOCS ๐Ÿ“ฆOFFICIAL GITHUB REPO