Zum Hauptinhalt springen
S-EDV news
← Alle Anleitungen
📘 Anleitung Docker 27.10.2026 · 11 min Lesezeit

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

Titelbild: Postiz selbst hosten mit Docker Compose, Redaktionsplan sowie Backup und Restore

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.de und 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.

EckdatumWert
Imageghcr.io/gitroomhq/postiz-app:v2.25.3 (GHCR, Tags mit v-Präfix)
Port5000 im Container (interner Nginx vor Frontend 4200 und Backend 3000), im Beispiel 4007 auf dem Host
Volumespostgres-volume (Postiz-Daten), postiz-uploads (Medien), postiz-config, postiz-redis-data, zwei Temporal-Volumes
Pflicht-EnvMAIN_URL, FRONTEND_URL, NEXT_PUBLIC_BACKEND_URL, BACKEND_INTERNAL_URL, JWT_SECRET, DATABASE_URL, REDIS_URL, TEMPORAL_ADDRESS
Wichtige OptionenDISABLE_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.

Postiz-Anmeldeseite mit der Meldung Registration is disabled und dem Link Login instead
Nach DISABLE_REGISTRATION=true zeigt die Registrierungsseite nur noch den Hinweis und den Login-Link.

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.

Postiz-Dialog Add Channel mit Kacheln für X, LinkedIn, Reddit, Instagram, YouTube, Mastodon, Bluesky, Nostr, WordPress und weitere Plattformen
Die Kanalauswahl in v2.25.3: jede Kachel braucht eigene Zugangsdaten der jeweiligen Plattform.

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.

Postiz-Kalender in der Wochenansicht mit einem Entwurf am Donnerstag um 11 Uhr und der Versionsangabe v2.25.3
Der Entwurf erscheint in der Wochenansicht des Kalenders, links stehen die verbundenen Kanäle.
Postiz-Editor mit Beitragstext, angehängtem Bild, Vorschau und den Schaltflächen Save as draft und Schedule
Im Editor sehen Sie Text, Medien und eine Vorschau pro Kanal, gespeichert wird als Entwurf oder geplanter Beitrag.

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

FehlerbildUrsacheLösung
Login meldet secretOrPrivateKey must have a value (HTTP 400), der Container ist trotzdem healthyJWT_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 401JWT_SECRET wurde geändert, alte Sitzungen sind ungültigAltes 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 LoginFRONTEND_URL passt nicht zur Adresse im Browser oder es fehlt HTTPS. Mit http://localhost:4007 setzte Postiz das Cookie für Domain=localhostAlle drei URL-Variablen auf die öffentliche HTTPS-Adresse setzen und den Container neu erstellen
Registration is disabled beim ersten KontoDISABLE_REGISTRATION=true gilt nur, solange schon eine Organisation existiert, etwa aus einem zurückgespielten BackupMit 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

PostizSocial MediaDocker ComposeSelfhostingTemporalBackupPostgreSQL