Deploying Mastodon with Docker and a Modern Traefik Stack
Table of Contents ๐
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
| Date | Change |
|---|---|
| 2025-10-30 | Initial 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. sudoor 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 |
| ๐ก TRAEFIK LABELS EXPLAINED |
The
|
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 |
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
.envfile (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.productionfile.
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.


