Postiz mit Docker Compose: Social-Media-Planung selbst hosten
Postiz plant Beiträge für über 30 Netzwerke aus einem Kalender. Diese Anleitung zeigt den getesteten Betrieb von v2.25.3 mit Docker Compose, HTTPS, gesperrter Registrierung, Medien-Uploads, Backup per pg_dump, Restore nach Totalverlust und das Update von v2.25.2.
Geprüft am 27.10.2026 · für Postiz 2.25.3
Mit KI erstellt – redaktionelle Prüfung ausstehend
WerbelinksMit * markierte Links sind Werbelinks: Bei einem Kauf erhalten wir eine Provision, der Preis bleibt gleich. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Mehr dazu

Postiz plant Beiträge für über 30 soziale Netzwerke und Publikationsplattformen aus einem gemeinsamen Kalender: LinkedIn, Mastodon, Bluesky, Instagram, YouTube, Discord, WordPress, Listmonk und viele mehr. Das Projekt gitroomhq/postiz-app steht unter AGPL-3.0, hat rund 38.500 Sterne auf GitHub und war am 27.10.2026 mit 188 neuen Sternen an einem Tag in den GitHub-Trends. Für Agenturen, Vereine und kleine Unternehmen bleiben Redaktionsplan und Medien so auf dem eigenen Server.
Diese Anleitung zeigt den offiziellen Stack mit Docker Compose, im Labor getestet mit Postiz v2.25.2 und dem Update auf v2.25.3: HTTPS, erstes Konto, Registrierung schließen, Entwurf mit Bild, Backup und Restore nach Totalverlust.
Voraussetzungen
- Linux-Server oder VM mit 2 CPU-Kernen, 4 GB RAM und 20 GB freiem Speicher, x86_64 oder ARM64 (das Image gibt es für beide Architekturen). Im Test belegte der Stack nach dem Start etwa 3,1 GB, davon 2,0 GB der App-Container.
- Docker Engine mit Docker Compose Plugin, etwa auf Debian 12 oder Ubuntu 24.04. Die Grundeinrichtung beschreibt die Anleitung Docker Compose unter Linux als Selfhosting-Grundlage.
- Eine Subdomain wie
postiz.example.deund ein Reverse Proxy mit TLS-Zertifikat, zum Beispiel Zoraxy, Caddy oder Nginx Proxy Manager. - Für jedes Netzwerk, das Postiz bedienen soll, ein Entwicklerkonto bei der Plattform mit eigener App (Client-ID und Secret). Das betrifft etwa X, LinkedIn, Meta, YouTube und TikTok.
Schritt 1: Projektordner und .env anlegen
Das offizielle Repository postiz-docker-compose enthält neben der Compose-Datei den Ordner dynamicconfig, den Temporal beim Start einliest. Klonen Sie das Repository und legen Sie die Geheimnisse in einer .env ab, statt sie wie im Original direkt in die Compose-Datei zu schreiben:
git clone https://github.com/gitroomhq/postiz-docker-compose postiz
cd postiz
cat > .env <<EOF
POSTIZ_URL=https://postiz.example.de
JWT_SECRET=$(openssl rand -hex 32)
POSTGRES_PASSWORD=$(openssl rand -hex 16)
TEMPORAL_DB_PASSWORD=$(openssl rand -hex 16)
DISABLE_REGISTRATION=false
EOF
chmod 600 .env
Der JWT_SECRET signiert alle Sitzungen; ändert er sich, sind alle Benutzer abgemeldet. Die .env gehört daher ins Backup.
Verifizieren: ls dynamicconfig zeigt development-sql.yaml, und grep -c '^[A-Z]' .env liefert 5.
Schritt 2: compose.yaml schreiben
Ersetzen Sie die mitgelieferte docker-compose.yaml durch eine schlanke compose.yaml. Gegenüber dem Original ist die Version gepinnt, die Passwörter stehen in der .env, feste container_name-Einträge und Netzwerknamen sind entfernt, und die optionalen Dienste Temporal UI, Admin-Tools und Spotlight fehlen. Plattform-Schlüssel ergänzen Sie später.
services:
postiz:
image: ghcr.io/gitroomhq/postiz-app:v2.25.3
restart: always
environment:
MAIN_URL: ${POSTIZ_URL}
FRONTEND_URL: ${POSTIZ_URL}
NEXT_PUBLIC_BACKEND_URL: ${POSTIZ_URL}/api
BACKEND_INTERNAL_URL: http://localhost:3000
JWT_SECRET: ${JWT_SECRET}
DATABASE_URL: postgresql://postiz:${POSTGRES_PASSWORD}@postiz-postgres:5432/postiz
REDIS_URL: redis://postiz-redis:6379
TEMPORAL_ADDRESS: temporal:7233
IS_GENERAL: "true"
DISABLE_REGISTRATION: ${DISABLE_REGISTRATION:-false}
RUN_CRON: "true"
STORAGE_PROVIDER: local
UPLOAD_DIRECTORY: /uploads
NEXT_PUBLIC_UPLOAD_DIRECTORY: /uploads
API_LIMIT: 30
volumes:
- postiz-config:/config/
- postiz-uploads:/uploads/
ports:
- "127.0.0.1:4007:5000"
healthcheck:
test: ["CMD", "node", "-e", "const r=require('http').get('http://localhost:5000/',res=>process.exit(res.statusCode<500?0:1));r.on('error',()=>process.exit(1));r.setTimeout(4000,()=>{r.destroy();process.exit(1)})"]
interval: 30s
timeout: 10s
retries: 5
start_period: 120s
depends_on:
postiz-postgres:
condition: service_healthy
postiz-redis:
condition: service_healthy
temporal:
condition: service_healthy
postiz-postgres:
image: postgres:17-alpine
restart: always
environment:
POSTGRES_USER: postiz
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: postiz
volumes:
- postgres-volume:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postiz -d postiz"]
interval: 10s
timeout: 5s
retries: 5
postiz-redis:
image: redis:7.2
restart: always
volumes:
- postiz-redis-data:/data
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep -q PONG"]
interval: 10s
timeout: 5s
retries: 5
temporal-elasticsearch:
image: elasticsearch:7.17.27
restart: always
environment:
- discovery.type=single-node
- ES_JAVA_OPTS=-Xms256m -Xmx256m
- xpack.security.enabled=false
- cluster.routing.allocation.disk.threshold_enabled=true
- cluster.routing.allocation.disk.watermark.low=512mb
- cluster.routing.allocation.disk.watermark.high=256mb
- cluster.routing.allocation.disk.watermark.flood_stage=128mb
volumes:
- temporal-elasticsearch-data:/usr/share/elasticsearch/data
healthcheck:
test: ["CMD-SHELL", "curl -fsS 'http://localhost:9200/_cluster/health?wait_for_status=yellow&timeout=5s' || exit 1"]
interval: 10s
timeout: 10s
retries: 10
start_period: 60s
temporal-postgresql:
image: postgres:16
restart: always
environment:
POSTGRES_USER: temporal
POSTGRES_PASSWORD: ${TEMPORAL_DB_PASSWORD}
volumes:
- temporal-postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U temporal"]
interval: 10s
timeout: 5s
retries: 5
temporal:
image: temporalio/auto-setup:1.28.1
restart: always
environment:
- DB=postgres12
- DB_PORT=5432
- POSTGRES_USER=temporal
- POSTGRES_PWD=${TEMPORAL_DB_PASSWORD}
- POSTGRES_SEEDS=temporal-postgresql
- DYNAMIC_CONFIG_FILE_PATH=config/dynamicconfig/development-sql.yaml
- ENABLE_ES=true
- ES_SEEDS=temporal-elasticsearch
- ES_VERSION=v7
- TEMPORAL_NAMESPACE=default
volumes:
- ./dynamicconfig:/etc/temporal/config/dynamicconfig
healthcheck:
test: ["CMD", "temporal", "operator", "cluster", "health", "--address", "temporal:7233"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
depends_on:
temporal-postgresql:
condition: service_healthy
temporal-elasticsearch:
condition: service_healthy
volumes:
postgres-volume:
postiz-redis-data:
postiz-config:
postiz-uploads:
temporal-postgres-data:
temporal-elasticsearch-data:
Der Port ist an 127.0.0.1 gebunden, damit nur der Reverse Proxy auf demselben Host Postiz erreicht. Läuft der Proxy auf einer anderen Maschine, entfernen Sie das Präfix und beschränken den Zugriff mit der Firewall, siehe Linux-Server mit ufw und fail2ban absichern. Weitere Härtung beschreibt Docker Compose absichern.
| Eckdatum | Wert |
|---|---|
| Image | ghcr.io/gitroomhq/postiz-app:v2.25.3 (GHCR, Tags mit v-Präfix) |
| Port | 5000 im Container (interner Nginx vor Frontend 4200 und Backend 3000), im Beispiel 4007 auf dem Host |
| Volumes | postgres-volume (Postiz-Daten), postiz-uploads (Medien), postiz-config, postiz-redis-data, zwei Temporal-Volumes |
| Pflicht-Env | MAIN_URL, FRONTEND_URL, NEXT_PUBLIC_BACKEND_URL, BACKEND_INTERNAL_URL, JWT_SECRET, DATABASE_URL, REDIS_URL, TEMPORAL_ADDRESS |
| Wichtige Optionen | DISABLE_REGISTRATION, STORAGE_PROVIDER, API_LIMIT, Plattform-Schlüssel wie LINKEDIN_CLIENT_ID |
Verifizieren: docker compose config --quiet läuft ohne Ausgabe durch. Eine Warnung wie The "JWT_SECRET" variable is not set bedeutet, dass die .env nicht im selben Ordner liegt.
Schritt 3: Stack starten und HTTPS davorschalten
docker compose up -d --pull always
docker compose ps --format '{{.Service}} {{.Status}}'
docker compose exec postiz printenv NEXT_PUBLIC_VERSION
Erst starten Datenbanken und Elasticsearch, dann Temporal, zuletzt Postiz:
postiz Up 14 seconds (healthy)
postiz-postgres Up 44 seconds (healthy)
postiz-redis Up 43 seconds (healthy)
temporal Up 23 seconds (healthy)
temporal-elasticsearch Up 45 seconds (healthy)
temporal-postgresql Up 44 seconds (healthy)
v2.25.3
Richten Sie im Reverse Proxy die Subdomain auf http://127.0.0.1:4007 ein. Postiz braucht HTTPS zwingend: Das Login-Cookie wird mit Secure; SameSite=None gesetzt, und Browser verwerfen es über unverschlüsseltes HTTP. Die Cookie-Domain leitet Postiz aus FRONTEND_URL ab; im Test galt sie für die übergeordnete Domain (Domain=.example.de). Nutzen Sie daher keine Domain, deren Subdomains Fremde kontrollieren.
Verifizieren: Alle sechs Dienste stehen auf healthy, und curl -sI https://postiz.example.de/ antwortet mit 307 und einer Weiterleitung auf die Anmeldeseite.
Schritt 4: Erstes Konto anlegen und Registrierung schließen
Öffnen Sie https://postiz.example.de/auth und registrieren Sie sich mit E-Mail, Passwort und Firmenname. Ohne Mailversand aktiviert Postiz neue Konten sofort (Log: Email service provider: no provider).
Wichtig: Nach dem ersten Konto bleibt die Registrierung offen. Im Test legte direkt danach ein zweites Konto ohne Einladung eine eigene Organisation auf dem Server an (HTTP 200). Schließen Sie die Registrierung deshalb sofort:
sed -i 's/^DISABLE_REGISTRATION=.*/DISABLE_REGISTRATION=true/' .env
docker compose up -d postiz
curl -s https://postiz.example.de/api/auth/can-register
{"register":false}
Ein weiterer Registrierungsversuch endet danach mit Registration is disabled und HTTP 400. Laut Dokumentation sperrt die Variable auch die Anmeldung über OIDC. Teammitglieder laden Sie in den Einstellungen ein.

Verifizieren: docker compose exec postiz printenv DISABLE_REGISTRATION zeigt true, und die Seite /auth bietet kein Formular mehr an.
Schritt 5: Kanäle, Entwürfe und Medien
Über Add Channel wählen Sie die Plattform. Die meisten Kanäle verbinden sich per OAuth und funktionieren erst, wenn Sie bei der Plattform eine Developer-App angelegt und deren Schlüssel als Variablen wie LINKEDIN_CLIENT_ID oder X_API_KEY eingetragen haben. Die Weiterleitungsadressen nennt die Provider-Dokumentation von Postiz. Manche Plattformen prüfen neue Apps vor der Freigabe. Kanäle mit selbst gehosteten Zielen wie Listmonk oder WordPress fragen stattdessen nach Adresse, Benutzername und Passwort.

Im Test haben wir einen Nostr-Kanal mit einem Wegwerf-Schlüssel verbunden, weil Nostr keine Developer-App verlangt, und damit einen Entwurf mit Bild im Kalender eingeplant. Bilder landen im Volume postiz-uploads unter /uploads/JAHR/MONAT/TAG/ und werden über https://postiz.example.de/uploads/... ausgeliefert.


Holt eine Plattform Medien per URL ab (laut Dokumentation etwa TikTok), muss /uploads öffentlich per HTTPS erreichbar sein; für interne Installationen empfiehlt das Projekt Cloudflare R2.
Verifizieren: docker compose exec postiz find /uploads -type f listet das hochgeladene Bild, und der Beitrag steht in der Datenbank als Entwurf:
docker compose exec -T postiz-postgres psql -U postiz -d postiz -Atc \
"select state, left(content,40) from \"Post\" where \"deletedAt\" is null;"
DRAFT|<p>RESTORE-MARKER-4711 Wochenplan Social
Schritt 6: Backup und Restore
Beiträge, Konten und Kanal-Tokens liegen in der Postiz-Datenbank, Medien im Upload-Volume. Sichern Sie beides plus die .env:
mkdir -p backup
docker compose exec -T postiz-postgres pg_dump -U postiz -Fc postiz > backup/postiz.dump
docker run --rm -v postiz_postiz-uploads:/data:ro -v "$PWD/backup":/backup alpine \
tar czf /backup/uploads.tgz -C /data .
cp .env backup/env.bak
Den Volume-Namen (Projektordner plus Volume) zeigt docker volume ls. Im Test löschte docker compose down -v alle Volumes, die neue Datenbank hatte 0 Tabellen. Die Rücksicherung:
docker compose up -d postiz-postgres
docker compose exec -T postiz-postgres pg_restore -U postiz -d postiz --exit-on-error < backup/postiz.dump
docker compose create postiz
docker run --rm -v postiz_postiz-uploads:/data -v "$PWD/backup":/backup:ro alpine \
tar xzf /backup/uploads.tgz -C /data
docker compose up -d
docker compose create legt das Upload-Volume an, ohne die App zu starten. Danach war der Entwurf wieder im Kalender, das Bild lieferte HTTP 200, und eine alte Sitzung blieb gültig, weil der JWT_SECRET gleich blieb.
Verifizieren: pg_restore endet ohne Fehlermeldung, die Abfrage aus Schritt 5 zeigt Ihre Beiträge, und curl -o /dev/null -w '%{http_code}' https://postiz.example.de/uploads/PFAD liefert 200.
Schritt 7: Postiz aktualisieren
Der Test begann mit v2.25.2 und wurde auf v2.25.3 gehoben. Sichern Sie vorher wie in Schritt 6, ändern Sie dann nur den Tag:
sed -i 's/postiz-app:v2.25.2/postiz-app:v2.25.3/' compose.yaml
docker compose pull postiz
docker compose up -d
docker compose logs --no-log-prefix postiz | grep -i "in sync"
The database is already in sync with the Prisma schema.
Postiz gleicht beim Start das Datenbankschema per prisma db push automatisch an, eine eigene Migration ist nicht nötig. Nach dem Update meldete der Container v2.25.3, Entwurf und Sitzung blieben erhalten. Vor größeren Sprüngen lesen Sie die Release Notes; für den Wechsel von v2.11 auf v2.12 (Temporal kam hinzu) gibt es eine eigene Migrationsanleitung. Über neue Versionen informiert etwa Diun, siehe Container-Updates mit Diun und WUD.
Verifizieren: docker compose exec postiz printenv NEXT_PUBLIC_VERSION zeigt die neue Version, die Versionsangabe unten links in der Kanalleiste ebenso.
Troubleshooting
| Fehlerbild | Ursache | Lösung |
|---|---|---|
Login meldet secretOrPrivateKey must have a value (HTTP 400), der Container ist trotzdem healthy | JWT_SECRET leer, im Log steht WARN [Configuration issue] JWT_SECRET not set. | Secret in der .env setzen, docker compose up -d postiz |
| Alle Benutzer abgemeldet, API antwortet 401 | JWT_SECRET wurde geändert, alte Sitzungen sind ungültig | Altes Secret aus dem Backup zurückspielen, im Test war die alte Sitzung danach wieder gültig |
| Anmeldung scheint zu klappen, die Seite springt aber zurück zum Login | FRONTEND_URL passt nicht zur Adresse im Browser oder es fehlt HTTPS. Mit http://localhost:4007 setzte Postiz das Cookie für Domain=localhost | Alle drei URL-Variablen auf die öffentliche HTTPS-Adresse setzen und den Container neu erstellen |
Registration is disabled beim ersten Konto | DISABLE_REGISTRATION=true gilt nur, solange schon eine Organisation existiert, etwa aus einem zurückgespielten Backup | Mit vorhandenem Konto anmelden oder für die Ersteinrichtung kurz auf false setzen |
Häufige Fragen
Kann ich Postiz ohne Developer-Apps der Plattformen nutzen?
Für die großen Netzwerke nein. Postiz bringt keine eigenen Schlüssel mit, jede selbst gehostete Instanz braucht pro Plattform eine eigene App. Ohne Developer-App kommen Sie bei Kanälen wie Nostr, Listmonk oder WordPress aus, weil dort ein Schlüssel oder Adresse und Zugangsdaten genügen. Mastodon braucht eine Anwendung auf der Instanz, deren Schlüssel in MASTODON_CLIENT_ID und MASTODON_CLIENT_SECRET stehen.
Gibt es in der selbst gehosteten Version Tarifgrenzen?
Laut Konfigurationsreferenz schaltet erst ein gesetzter STRIPE_PUBLISHABLE_KEY Tarife und Limits ein. Ohne Stripe-Schlüssel verhält sich die Instanz wie der höchste Tarif. Grenzen setzt nur API_LIMIT für die öffentliche API (im offiziellen Compose 30 Beitragsaufrufe pro Stunde).
Übersteht der Stack einen Neustart des Servers?
Ja. Während des Tests startete der Testhost unerwartet neu, alle sechs Dienste kamen dank restart: always von selbst wieder hoch, und der Entwurf war unverändert vorhanden.
Fazit
Postiz läuft mit dem offiziellen Stack zuverlässig, ist mit Temporal und Elasticsearch aber kein kleiner Dienst; rechnen Sie mit 4 GB RAM. Kalender, Entwürfe, Medien, Backup und Update funktionierten im Test ohne Nacharbeit. Zwei Punkte verdienen Aufmerksamkeit: Die Registrierung bleibt nach dem ersten Konto offen, bis Sie sie abschalten, und der eigentliche Aufwand steckt in den Developer-Apps der einzelnen Plattformen. Für LinkedIn, Meta oder TikTok beantragen Sie die App-Freigaben am besten früh.
Weiterführende Anleitungen und Quellen
- Mastodon mit Docker selbst betreiben: eigene Instanz als Postiz-Kanal
- Listmonk als Newsletter-Server mit Docker: Newsletter als Postiz-Ziel
- Zoraxy als Reverse Proxy mit Weboberfläche
- gitroomhq/postiz-app auf GitHub
- gitroomhq/postiz-docker-compose
- Postiz-Dokumentation: Docker Compose
- Postiz-Dokumentation: Configuration Reference
- Postiz-Dokumentation: Uploads und Storage


