Getestet auf
Debian 13 (Trixie), Docker Compose v2, Mastodon v4.6.0 – Stand 2026-06-18. Reverse-Proxy: nginx mit Let’s-Encrypt-Zertifikat. Reales Deployment hinter eigener Infrastruktur.
Voraussetzungen
- Server mit Docker + Docker Compose v2, ~2 GB freiem RAM und ausreichend Plattenplatz (Medienspeicher wächst mit der Aktivität).
- Zwei DNS-Namen, beide auf deinen Server bzw. Reverse-Proxy zeigend: die Handle-Domain und eine Subdomain für den Server.
- Reverse-Proxy mit gültigem TLS-Zertifikat (hier nginx).
- Optional, aber für Anmeldungen nötig: ein SMTP-Zugang.
Achtung, vorab entscheiden:
LOCAL_DOMAINundWEB_DOMAINlegst du vor dem ersten Start fest. Nachträglich ändern bricht die Föderation – der Rest des Fediverse kennt dann noch deine alten Adressen.
Platzhalter
Ersetze in allen Befehlen und Konfigurationen durchgehend dieselben Platzhalter durch deine Werte:
| Platzhalter | Bedeutung | Beispiel |
|---|---|---|
DEINE_DOMAIN | Domain für die Handles (@name@DEINE_DOMAIN) | example.de |
SERVER_DOMAIN | Subdomain, auf der Mastodon läuft | social.example.de |
ADMIN_USER | dein Mastodon-Benutzername | admin |
ADMIN_EMAIL | E-Mail-Adresse deines Admin-Accounts | admin@example.de |
DB_PASSWORT | Passwort für die PostgreSQL-Datenbank | (zufällig erzeugen) |
MAIL_SERVER | SMTP-Server (nur Schritt 9) | mail.example.de |
SMTP_USER | SMTP-Login (nur Schritt 9) | postfach@example.de |
SMTP_PASSWORT | SMTP-Passwort (nur Schritt 9) | (aus deinem Mailserver) |
Schritte
1. Verzeichnis und Compose-Datei
mkdir -p /opt/mastodon/{system,postgres,redis}
cd /opt/mastodondocker-compose.yml (ersetze DB_PASSWORT):
name: mastodon
services:
db:
image: postgres:16-alpine
restart: unless-stopped
healthcheck: {test: ["CMD","pg_isready","-U","mastodon"], interval: 10s, timeout: 5s, retries: 10}
environment:
POSTGRES_USER: mastodon
POSTGRES_DB: mastodon
POSTGRES_PASSWORD: "DB_PASSWORT"
volumes: ["./postgres:/var/lib/postgresql/data"]
redis:
image: redis:7-alpine
restart: unless-stopped
healthcheck: {test: ["CMD","redis-cli","ping"], interval: 10s, timeout: 5s, retries: 10}
volumes: ["./redis:/data"]
web:
image: ghcr.io/mastodon/mastodon:v4.6.0
restart: unless-stopped
env_file: .env.production
command: ["bundle","exec","puma","-C","config/puma.rb"]
healthcheck: {test: ["CMD-SHELL","wget -q --tries=1 --spider http://localhost:3000/health || exit 1"], interval: 15s, timeout: 5s, retries: 16}
depends_on: {db: {condition: service_healthy}, redis: {condition: service_healthy}}
ports: ["127.0.0.1:3000:3000"]
volumes: ["./system:/mastodon/public/system"]
streaming:
image: ghcr.io/mastodon/mastodon-streaming:v4.6.0
restart: unless-stopped
env_file: .env.production
healthcheck: {test: ["CMD-SHELL","wget -q --tries=1 --spider http://localhost:4000/api/v1/streaming/health || exit 1"], interval: 15s, timeout: 5s, retries: 16}
depends_on: {db: {condition: service_healthy}, redis: {condition: service_healthy}}
ports: ["127.0.0.1:4000:4000"]
sidekiq:
image: ghcr.io/mastodon/mastodon:v4.6.0
restart: unless-stopped
env_file: .env.production
command: ["bundle","exec","sidekiq","-c","10"]
depends_on: {db: {condition: service_healthy}, redis: {condition: service_healthy}}
volumes: ["./system:/mastodon/public/system"]Hinweis: Seit Mastodon 4.3 ist der Streaming-Server ein eigenes Image (
mastodon-streaming) – nicht dasmastodon-Image dafür verwenden.
2. Grund-Konfiguration .env.production
Ersetze DEINE_DOMAIN, SERVER_DOMAIN und DB_PASSWORT:
LOCAL_DOMAIN=DEINE_DOMAIN
WEB_DOMAIN=SERVER_DOMAIN
RAILS_ENV=production
NODE_ENV=production
REDIS_HOST=redis
REDIS_PORT=6379
DB_HOST=db
DB_PORT=5432
DB_NAME=mastodon
DB_USER=mastodon
DB_PASS=DB_PASSWORT
SINGLE_USER_MODE=false
WEB_CONCURRENCY=1
MAX_THREADS=5DB_PASS muss identisch zu POSTGRES_PASSWORD aus Schritt 1 sein.
3. Secrets erzeugen
SECRET_KEY_BASE und OTP_SECRET sind je ein 128-Zeichen-Hex-String (openssl rand -hex 64
erzeugt 64 Bytes = 128 Hex-Zeichen), die ActiveRecord-Encryption-Keys je 32 Hex-Zeichen:
{
echo "SECRET_KEY_BASE=$(openssl rand -hex 64)"
echo "OTP_SECRET=$(openssl rand -hex 64)"
echo "ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=$(openssl rand -hex 16)"
echo "ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=$(openssl rand -hex 16)"
echo "ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=$(openssl rand -hex 16)"
} >> .env.productionDie VAPID-Schlüssel (Web-Push) erzeugt Mastodon selbst:
docker compose run --rm -T web bundle exec rake mastodon:webpush:generate_vapid_key </dev/null \
| grep -E '^VAPID_(PRIVATE|PUBLIC)_KEY=' >> .env.productionBeim Skripten wichtig:
docker compose runliest stdin. In einem Heredoc-Skript „frisst" es sonst die restlichen Zeilen – gib ihm</dev/null.
4. Datenbank initialisieren
docker compose up -d db redis
docker compose run --rm -T web bundle exec rails db:setup </dev/null5. Owner-Account anlegen
Ersetze ADMIN_USER und ADMIN_EMAIL:
docker compose run --rm -T web bin/tootctl accounts create ADMIN_USER \
--email ADMIN_EMAIL --confirmed --role Owner </dev/nullDas gibt ein Initialpasswort aus – notier es sofort. Wenn du nach dem ersten Login im gelben Balken „pending review" hängst und immer wieder auf die Account-Seite umgeleitet wirst: Der Account ist bestätigt, aber noch nicht freigegeben. Freigeben:
docker compose run --rm -T web bin/tootctl accounts approve ADMIN_USER </dev/null6. Stack starten
docker compose up -dDer erste Start dauert etwas, weil Puma hochfährt und Assets kompiliert werden.
docker compose ps zeigt healthy, sobald alle Container bereit sind.
7. Reverse-Proxy (nginx)
Streaming läuft auf einem eigenen Port – /api/v1/streaming muss dorthin, der Rest auf web.
Ersetze SERVER_DOMAIN:
server {
listen 443 ssl;
server_name SERVER_DOMAIN;
# ssl_certificate / ssl_certificate_key ...
client_max_body_size 80M;
location /api/v1/streaming {
proxy_pass http://127.0.0.1:4000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 3600s;
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}8. Hübsche Handles per Split-Domain
Damit dein Handle @ADMIN_USER@DEINE_DOMAIN lautet (statt @ADMIN_USER@SERVER_DOMAIN), muss
DEINE_DOMAIN drei Pfade dauerhaft auf die Server-Subdomain umleiten – nur diese drei,
niemals /api/.... Ersetze DEINE_DOMAIN und SERVER_DOMAIN:
# Auf dem vHost von DEINE_DOMAIN:
location = /.well-known/host-meta { return 301 https://SERVER_DOMAIN$request_uri; }
location = /.well-known/webfinger { return 301 https://SERVER_DOMAIN$request_uri; }
location = /.well-known/nodeinfo { return 301 https://SERVER_DOMAIN$request_uri; }Test (ersetze DEINE_DOMAIN und ADMIN_USER):
curl -sL "https://DEINE_DOMAIN/.well-known/webfinger?resource=acct:ADMIN_USER@DEINE_DOMAIN"Die Antwort muss "subject":"acct:ADMIN_USER@DEINE_DOMAIN" enthalten.
9. Mailversand (optional, aber für Anmeldungen nötig)
Ohne SMTP funktionieren Registrierungen anderer nicht (keine Bestätigungsmail). In
.env.production ergänzen (ersetze MAIL_SERVER, SMTP_USER, SMTP_PASSWORT):
SMTP_SERVER=MAIL_SERVER
SMTP_PORT=465
SMTP_LOGIN=SMTP_USER
SMTP_PASSWORD=SMTP_PASSWORT
SMTP_FROM_ADDRESS=Mastodon <SMTP_USER>
SMTP_TLS=true
SMTP_AUTH_METHOD=plain
SMTP_OPENSSL_VERIFY_MODE=peerFür STARTTLS auf Port 587 statt SMTPS auf 465: SMTP_PORT=587, Zeile SMTP_TLS entfernen und
SMTP_ENABLE_STARTTLS=always setzen. Danach docker compose up -d web sidekiq.
10. Plattenplatz im Griff behalten
Mastodon cached fremde Medien – das wächst unkontrolliert. Per Cron wöchentlich aufräumen:
# /etc/cron.d/mastodon-media-cleanup
0 4 * * 0 root cd /opt/mastodon && docker compose run --rm -T web bin/tootctl media remove --days 14 >/dev/null 2>&1Troubleshooting
- „pending review", Endlosschleife auf die Account-Seite: Der Account ist nicht freigegeben →
tootctl accounts approve ADMIN_USER(siehe Schritt 5). Bind for 127.0.0.1:3000 failed: port is already allocated: Ein anderer Dienst belegt den Port. Host-Port im Compose ändern (z. B.127.0.0.1:3001:3000) und im Reverse-Proxy entsprechend.docker compose runbricht dein Skript ab: Es liest stdin und frisst den Rest deines Heredocs →</dev/nullanhängen.- Handle bleibt
@ADMIN_USER@SERVER_DOMAIN: Die well-known-Redirects aufDEINE_DOMAINfehlen oder zeigen falsch (Schritt 8)./api/...darf nicht umgeleitet werden. - Domains nachträglich ändern: Sauber nicht möglich, sobald föderiert wurde – vorher festlegen.
Changelog
- 2026-06-18 – Erstveröffentlichung.