Jump to content

Editing Mastodon Docker Deployment Guide 260402

From MediawikiCIT

Mastodon on Docker — Self-Hosted Deployment Guide

Behind Nginx Proxy Manager with Let's Encrypt SSL

Using linuxserver.io image | Ubuntu 24 | Docker Compose

Template:Tip

Why Mastodon Is More Complex Than Other Docker Apps

Most Docker apps are a single process that sits behind a reverse proxy. Mastodon is different in three important ways:

  1. It runs five processes simultaneously inside one container: Rails (web), Puma (app server), Sidekiq (background jobs), a Node.js streaming server, and its own internal nginx. This is managed by s6-overlay.
  2. It assumes it owns ports 80 and 443 directly. Placing it behind NPM requires overriding its internal nginx SSL behavior, which it resists.
  3. It enforces cryptographic secrets before it will even start. Unlike most apps, Mastodon refuses to boot without SECRET_KEY_BASE, OTP_SECRET, VAPID keys, and ACTIVE_RECORD_ENCRYPTION keys all pre-generated.

Template:Warning

Prerequisites

  • Ubuntu 24 server (bare metal or VM)
  • Docker and Docker Compose installed
  • Nginx Proxy Manager (NPM) already running on ports 80 and 443
  • A domain name with an A record pointing to your server's public IP
  • Gmail account with an App Password generated (for SMTP)

Template:Critical

Step 1 — Create the Directory

All Mastodon files live in one directory. Create it and navigate into it:

mkdir -p /opt/mastodon
cd /opt/mastodon

The volume mount /opt/mastodon/config:/config will be auto-created by linuxserver on first run and will populate with nginx config, keys, and logs.

Step 2 — Generate All Secrets Before Starting

Template:Warning

SECRET_KEY_BASE (run once)

docker run --rm --entrypoint="" lscr.io/linuxserver/mastodon:latest /app/www/bin/rails secret

OTP_SECRET (run again for a different value)

docker run --rm --entrypoint="" lscr.io/linuxserver/mastodon:latest /app/www/bin/rails secret

VAPID Keys

docker run --rm --entrypoint="" -w /app/www lscr.io/linuxserver/mastodon:latest /app/www/bin/rails mastodon:webpush:generate_vapid_key

Output will look like:

VAPID_PRIVATE_KEY=OqZ07QTRwoO...
VAPID_PUBLIC_KEY=BA7IhTSGcf0...

ACTIVE_RECORD_ENCRYPTION Keys (new requirement in Mastodon 4.3+)

docker run --rm --entrypoint="" -w /app/www lscr.io/linuxserver/mastodon:latest /app/www/bin/rails db:encryption:init

Output will look like:

ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=Xf3Rvl...
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=TK5DQU...
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=f8cyBe...

Template:Critical

Step 3 — Create docker-compose.yml

Create the file:

nano /opt/mastodon/docker-compose.yml

Paste the following, replacing all placeholder values with your actual secrets and domain:

services:
  mastodon:
    image: lscr.io/linuxserver/mastodon:latest
    container_name: mastodon
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Manila
      - LOCAL_DOMAIN=toot.yourdomain.com
      - NO_SSL=true
      - FORCE_SSL=false
      - TRUSTED_PROXY_IP=127.0.0.1/8,::1/128,192.168.0.0/16
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - DB_HOST=db
      - DB_USER=mastodon
      - DB_NAME=mastodon_production
      - DB_PASS=your_db_password
      - DB_PORT=5432
      - ES_ENABLED=false
      - SECRET_KEY_BASE=<from step 2>
      - OTP_SECRET=<from step 2>
      - VAPID_PRIVATE_KEY=<from step 2>
      - VAPID_PUBLIC_KEY=<from step 2>
      - ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=<from step 2>
      - ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=<from step 2>
      - ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=<from step 2>
      - SMTP_SERVER=smtp.gmail.com
      - SMTP_PORT=587
      - SMTP_LOGIN=your_gmail@gmail.com
      - SMTP_PASSWORD=your_gmail_app_password
      - SMTP_FROM_ADDRESS=notifications@yourdomain.com
      - SMTP_AUTH_METHOD=plain
      - SMTP_OPENSSL_VERIFY_MODE=peer
      - S3_ENABLED=false
    volumes:
      - /opt/mastodon/config:/config
    ports:
      - 8667:80
    networks:
      - mastodon-net
    depends_on:
      - db
      - redis
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    container_name: mastodon-redis
    volumes:
      - /opt/mastodon/redis:/data
    networks:
      - mastodon-net
    restart: unless-stopped

  db:
    image: postgres:14-alpine
    container_name: mastodon-db
    environment:
      - POSTGRES_DB=mastodon_production
      - POSTGRES_USER=mastodon
      - POSTGRES_PASSWORD=your_db_password
    volumes:
      - /opt/mastodon/postgres:/var/lib/postgresql/data
    networks:
      - mastodon-net
    restart: unless-stopped

networks:
  mastodon-net:
    driver: bridge

Template:Note

Step 4 — Start the Stack

cd /opt/mastodon
docker compose up -d

Watch the logs and wait for the boot sequence to complete (30-60 seconds):

docker compose logs -f mastodon

A successful boot ends with lines like:

Connection to localhost (127.0.0.1) 3000 port [tcp/*] succeeded!
[ls.io-init] done.
Sidekiq 8.x connecting to Redis with options...

Template:Warning

Step 5 — Fix Internal nginx SSL Conflict

The linuxserver image generates an nginx config that listens on both port 80 and port 443, and passes X-Forwarded-Proto: http to Rails. This causes Rails to issue a 301 redirect to HTTPS even when NO_SSL=true and FORCE_SSL=false are set. The fix is to edit the nginx config directly and hardcode X-Forwarded-Proto to https.

Template:Warning

Remove 443 listeners and hardcode X-Forwarded-Proto

# Fix active config
docker exec mastodon sed -i 's/listen 443 ssl default_server;//g' /config/nginx/site-confs/default.conf
docker exec mastodon sed -i 's/listen \[\:\:\]:443 ssl default_server;//g' /config/nginx/site-confs/default.conf
docker exec mastodon sed -i 's/include \/config\/nginx\/ssl.conf;//g' /config/nginx/site-confs/default.conf
docker exec mastodon sed -i 's/proxy_set_header X-Forwarded-Proto $scheme;/proxy_set_header X-Forwarded-Proto https;/g' /config/nginx/site-confs/default.conf

# Fix sample so it survives restarts
docker exec mastodon sed -i 's/listen 443 ssl default_server;//g' /config/nginx/site-confs/default.conf.sample
docker exec mastodon sed -i 's/listen \[\:\:\]:443 ssl default_server;//g' /config/nginx/site-confs/default.conf.sample
docker exec mastodon sed -i 's/include \/config\/nginx\/ssl.conf;//g' /config/nginx/site-confs/default.conf.sample
docker exec mastodon sed -i 's/proxy_set_header X-Forwarded-Proto $scheme;/proxy_set_header X-Forwarded-Proto https;/g' /config/nginx/site-confs/default.conf.sample

# Reload nginx without restarting the container
docker exec mastodon s6-svc -r /run/service/svc-nginx

Verify the fix

curl -v http://localhost:8667 -H "Host: toot.yourdomain.com"

You should see HTTP/1.1 200 OK. If you still see 301 Moved Permanently, check that the sed commands ran correctly:

docker exec mastodon cat /config/nginx/site-confs/default.conf | grep -n "301\|https\|ssl\|443"

Step 6 — Configure Nginx Proxy Manager

In NPM, create a new Proxy Host with these settings:

Setting Value
Domain Name toot.yourdomain.com
Scheme http
Forward Hostname / IP Your server's LAN IP (e.g. 192.168.1.100)
Forward Port 8667 (or whichever host port you chose)
Cache Assets Off
Block Common Exploits On
Websockets Support On (critical for live feed)
SSL Certificate Request via Let's Encrypt
Force SSL On
HTTP/2 Support On
HSTS Enabled Off (optional, enable after confirming working)

Template:Warning

Template:Note

Step 7 — Create Admin Account

Mastodon starts with zero accounts. Create your owner account from the command line:

docker exec -it mastodon /app/www/bin/tootctl accounts create YOUR_USERNAME --email YOUR@EMAIL --confirmed --role Owner

The command outputs a randomly generated password. Copy it immediately. Log in at https://toot.yourdomain.com with your email and that password.

Template:Warning

docker exec -it mastodon /app/www/bin/tootctl accounts approve YOUR_USERNAME

Then change your password immediately in Settings > Account > Account Settings.

Problems Encountered and Countermeasures

Problem Cause Fix
Crash loop: ACTIVE_RECORD_ENCRYPTION keys missing New requirement in Mastodon 4.3+. Container exits immediately without these three keys set. Generate with: docker run --rm --entrypoint="" -w /app/www lscr.io/... /app/www/bin/rails db:encryption:init
Redis: connect ECONNREFUSED 127.0.0.1:6379 Redis service not in same compose file, or containers not on same Docker network. Add redis service to same docker-compose.yml with a shared named network (mastodon-net).
bin/rails: no such file or directory linuxserver entrypoint intercepts the command before rails runs. Always use --entrypoint="" and full path: /app/www/bin/rails
No Rakefile found error for rake tasks Working directory not set; rake cannot find the app root. Add -w /app/www to the docker run command.
301 redirect loop at toot.yourdomain.com Internal nginx passes X-Forwarded-Proto: http to Rails. Rails sees HTTP and redirects to HTTPS infinitely. Sed-edit both default.conf and default.conf.sample to hardcode X-Forwarded-Proto https. Reload nginx with s6-svc.
NO_SSL=true and FORCE_SSL=false not working FORCE_SSL was removed in newer Mastodon. NO_SSL prevents regenerating the config but cannot fix an already-generated config. Edit the nginx config files directly. The sample must also be edited or changes are overwritten on restart.
403 Forbidden on curl test Host header mismatch. Mastodon's nginx rejects requests where the Host header does not match LOCAL_DOMAIN. Always test with: curl -v http://localhost:8667 -H "Host: toot.yourdomain.com"
Pending review banner after login Mastodon requires admin approval for all accounts including ones created via tootctl. Run: docker exec -it mastodon /app/www/bin/tootctl accounts approve YOUR_USERNAME
Port 80/443 already in use NPM is already running on those ports on the host. Map Mastodon to an unused host port (e.g. 8667:80). NPM proxies to it. Drop the 443 mapping entirely.
Container still starting when testing Mastodon takes 30-60 seconds to fully boot. curl or browser tests fail during this window. Watch logs: docker compose logs -f mastodon. Only test after seeing [ls.io-init] done.

Instructions for AI Agent Execution

If you are an AI agent following this guide, use this decision tree at each stage:

At each step, verify before proceeding

  1. Run the command exactly as written.
  2. Check the output against the expected output described.
  3. If the output does not match, consult the Problems table above before retrying.
  4. Never proceed to the next step if the current step has not produced the expected output.

Key verification commands

# Is the container running?
docker compose ps

# Is it fully booted?
docker compose logs mastodon | tail -20
# Look for: [ls.io-init] done.

# Is nginx listening on port 80 inside the container?
docker exec mastodon netstat -tlnp
# Look for: 0.0.0.0:80 LISTEN nginx

# Is Mastodon responding correctly?
curl -v http://localhost:8667 -H "Host: toot.yourdomain.com"
# Look for: HTTP/1.1 200 OK
# A 301 means the nginx X-Forwarded-Proto fix has not been applied yet.

# Are all env vars present?
docker exec mastodon env | grep -E 'ACTIVE_RECORD|SECRET|VAPID|NO_SSL|FORCE'
# All seven secrets must appear with non-empty values.

# What ports are in use on the host?
docker ps --format "table {{.Names}}\t{{.Ports}}"

Template:Tip

Post-Setup Checklist

  • [ ] Change your admin password in Settings > Account > Account Settings
  • [ ] Enable Two-Factor Authentication in Settings > Account > Two-Factor Auth
  • [ ] Set up server rules and description in Admin > Server Settings
  • [ ] Close registrations or set to invite-only in Admin > Server Settings > Registrations
  • [ ] Revoke any SMTP credentials exposed during setup and generate new ones
  • [ ] Back up /opt/mastodon/config and /opt/mastodon/postgres regularly
  • [ ] Back up your docker-compose.yml and store it in Forgejo or another version-controlled location

Template:Critical