Mastodon per Docker Compose installieren
Ein steiniger Weg
Wer Mastodon selbst hosten will, bekommt von der offiziellen Dokumentation eine Docker-Compose-Datei hingestellt und soll damit glücklich werden. In der Theorie klingt das vernünftig: Container, alles sauber isoliert, reproduzierbar. In der Praxis ist die Installation eine Abfolge von Überraschungen, undokumentierten Fallstricken und Workarounds, die man sich mühsam selbst erarbeiten muss. Dieser Bericht dokumentiert den Weg – inklusive aller Umwege.
Der Setup-Wizard und die verschwiegene Wahrheit über .env.production
Der offizielle Weg sieht vor, den interaktiven Setup-Wizard zu starten:1
docker compose run --rm web bundle exec rake mastodon:setup
Der Wizard führt durch die Konfiguration: Domain, Datenbankverbindung, Redis, SMTP, Schlüsselgenerierung. Am Ende zeigt er den vollständigen Inhalt der .env.production auf dem Bildschirm an – und tut dann nichts weiter damit.
Das ist der erste große Fallstrick: Der Wizard schreibt die Datei nicht selbst. Er zeigt sie nur an. Wer das nicht weiß – und die Dokumentation erwähnt es nicht explizit –, startet danach ein System ohne funktionierende Konfiguration. Den Bildschirminhalt muss man manuell in die .env.production kopieren. Das ist keine professionelle Deployment-Praxis, das ist Bastelarbeit aus den frühen 2000ern.2
Die Datenbank, die immer schon existiert
Nach dem manuellen Befüllen der .env.production kommt der nächste Stolperstein. Der Wizard fragt, ob die Datenbank initialisiert werden soll – man antwortet mit Y – und bekommt prompt:
ActiveRecord::ProtectedEnvironmentError: You are attempting to run a
destructive action against your 'production' database.
Die Datenbank existiert bereits. Warum? Weil der PostgreSQL-Container beim Start automatisch die in POSTGRES_DB definierte Datenbank anlegt – also mastodon_production – noch bevor der Wizard überhaupt läuft. Rails weigert sich dann korrekterweise, eine bestehende Production-Datenbank zu überschreiben.3
Das Ironische: docker compose down -v löscht die Volumes, aber beim nächsten docker compose run werden die abhängigen Container (db, redis, es) neu gestartet – und PostgreSQL legt die Datenbank sofort wieder an. Man kommt aus dieser Schleife nicht heraus, solange man den Wizard benutzt.
Die Lösung ist, den Wizard komplett zu umgehen und die Datenbank manuell zu droppen:4
docker compose up -d db
docker compose exec db psql -U mastodon -c "DROP DATABASE IF EXISTS mastodon_production;"
docker compose run --rm web bundle exec rails db:schema:load
docker compose run --rm web bundle exec rails db:seed
Port-Binding: 127.0.0.1 ist nicht immer die richtige Antwort
Die mitgelieferte docker-compose.yml bindet die Ports des Web- und Streaming-Dienstes an 127.0.0.1:
ports:
- '127.0.0.1:3000:3000'
- '127.0.0.1:4000:4000'
Das ist für einen lokalen nginx auf demselben Host korrekt und aus Sicherheitsgründen auch sinnvoll. Wer jedoch – wie in unserem Fall – nginx auf einem separaten VPS betreibt, der über WireGuard mit dem Homeserver verbunden ist, bekommt einen 502-Fehler. Der Reverse Proxy kann den Container nicht erreichen, weil der Port nur lokal lauscht.5
Die Lösung: das Binding auf die WireGuard-Interface-IP ändern:
ports:
- '10.66.66.4:3000:3000'
- '10.66.66.4:4000:4000'
Die Dokumentation erwähnt diesen Fall mit keinem Wort. Wer keine Standard-Topologie betreibt, ist auf sich gestellt.
Dateirechte: /public/system gehört root
Nach dem ersten Login versucht man, ein Profilbild hochzuladen – und bekommt eine Fehlerseite. Keine klare Fehlermeldung, kein Hinweis im Interface, was schiefgelaufen ist.
Die Ursache: das Verzeichnis /opt/mastodon/public/system im Container gehört root, der Mastodon-Prozess läuft aber als Benutzer mastodon. Schreibrechte: keine.6
Fix:
docker compose exec -u root web chown -R mastodon:mastodon /opt/mastodon/public/system
Dass dieser Schritt notwendig ist, steht nirgendwo in der offiziellen Dokumentation. Es ist ein bekanntes Problem, das in verschiedenen GitHub-Issues auftaucht – aber nicht behoben wird.
Elasticsearch: aktiviert, aber nicht konfiguriert
Elasticsearch ist in der docker-compose.yml als Service enthalten und wird gestartet. Man könnte meinen, Mastodon nutzt ihn automatisch. Falsch.
Ohne explizite Einträge in der .env.production ignoriert Mastodon Elasticsearch vollständig und fällt auf eine interne Suche zurück – oder zeigt im Admin-Bereich „Elasticsearch nicht verfügbar” an. Die notwendigen Variablen:7
ES_ENABLED=true
ES_HOST=es
ES_PORT=9200
Besonders tückisch: ohne ES_HOST=es versucht Mastodon, Elasticsearch auf localhost:9200 zu erreichen – also innerhalb des Web-Containers, wo natürlich kein ES läuft. Der Fehler ist kryptisch:
Connection refused - connect(2) for "localhost" port 9200
Dass der korrekte Hostname der Docker-Service-Name es ist, muss man wissen oder erraten.
Nachdem die Variablen gesetzt sind, muss der Index manuell angelegt werden:8
docker compose run --rm web bundle exec tootctl search deploy
Auch das läuft nicht durch einen normalen docker compose up, sondern muss explizit angestoßen werden. Out-of-the-box funktioniert Elasticsearch nicht.
Der GeoIP-Fehler: harmlos, aber laut
In den Elasticsearch-Logs taucht regelmäßig ein Java-Stacktrace auf, der auf den ersten Blick bedrohlich wirkt:
org.elasticsearch.ingest.geoip.GeoIpDownloader
Connection refused - connect(2) for "storage.googleapis.com"
ES versucht beim Start, GeoIP-Datenbanken von Google herunterzuladen – was in einem abgeschotteten Container-Netzwerk ohne Internetzugang natürlich scheitert. Der Fehler ist funktional irrelevant für Mastodon, erzeugt aber bei jedem Start unnötigen Lärm in den Logs.9
Fazit
Mastodon per Docker Compose zu installieren ist möglich – aber es ist keine produktionsreife, dokumentierte Deployment-Erfahrung. Es ist ein Zusammenstückeln von Informationen aus GitHub-Issues, veralteten Blog-Posts und Trial-and-Error. Die Hauptkritikpunkte:
- Der Setup-Wizard schreibt seine eigene Ausgabe nicht in die Konfigurationsdatei
- Die Datenbank-Initialisierung kollidiert mit dem PostgreSQL-Container-Init-Verhalten
- Dateirechte-Probleme sind bekannt und unbehoben
- Elasticsearch ist im Stack enthalten, aber nicht out-of-the-box aktiviert
- Die Dokumentation deckt Nicht-Standard-Topologien (getrennte Reverse-Proxy-Server, WireGuard) nicht ab
Am Ende läuft es. Aber der Weg dorthin ist unnötig steinig für ein Projekt, das Selbst-Hosting explizit als Anwendungsfall bewirbt.
-
Offizieller Mastodon-Installationsleitfaden für Docker: https://docs.joinmastodon.org/admin/install/ ↩
-
Das Verhalten des Wizards ist in mehreren GitHub-Issues dokumentiert, aber nie als Bug klassifiziert worden. Es gilt offenbar als „so gewollt”. ↩
-
Rails schützt Production-Datenbanken vor versehentlichem Überschreiben via
ActiveRecord::ProtectedEnvironmentError. Das ist sinnvoll – nur nicht im Kontext eines Erstaufbaus. ↩ -
DISABLE_DATABASE_ENVIRONMENT_CHECK=1wäre eine Alternative, ist aber eine schlechte Gewohnheit, die man nicht zur Routine werden lassen sollte. ↩ -
WireGuard-basierte Infrastrukturen sind im Homeserver-Bereich weit verbreitet. Die fehlende Dokumentation dieses Falls ist ein Versäumnis. ↩
-
GitHub Issue #20694 und verwandte Issues beschreiben das Problem seit Jahren. Eine saubere Lösung wäre, das Volume mit korrekten Rechten zu initialisieren. ↩
-
Die Variablennamen sind in der
.env.production.sampleenthalten, aber auskommentiert und ohne Erklärung, dass sie zwingend erforderlich sind, wenn ES im Stack läuft. ↩ -
tootctl search deploymuss nach jedem größeren Datenimport erneut ausgeführt werden, z. B. nach einer Migration von einer anderen Instanz. ↩ -
Das GeoIP-Download-Verhalten lässt sich in ES 7.x mit
ingest.geoip.downloader.enabled: falsein derelasticsearch.ymldeaktivieren. ↩