Editing Mastodon Docker Deployment Guide 260402
Mastodon on Docker — Self-Hosted Deployment Guide
Behind Nginx Proxy Manager with Let's Encrypt SSL
Using linuxserver.io image | Ubuntu 24 | Docker Compose
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:
- 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.
- It assumes it owns ports 80 and 443 directly. Placing it behind NPM requires overriding its internal nginx SSL behavior, which it resists.
- 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.
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)
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
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...
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
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...
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.
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) |
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.
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
- Run the command exactly as written.
- Check the output against the expected output described.
- If the output does not match, consult the Problems table above before retrying.
- 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}}"
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