Zum Hauptinhalt springen
S-EDV news
← Alle Anleitungen
📘 Anleitung Datenbanken 14.08.2026 · 12 min Lesezeit

Payload CMS mit Docker installieren: Fullstack Next.js Headless CMS selbst hosten

Payload CMS v3 ist das modernste TypeScript-native Headless CMS für Next.js-Teams – 43.000 GitHub-Stars, MIT-lizenziert und ohne Vendor-Lock-in. Diese Anleitung zeigt, wie du dein eigenes Payload-Projekt via Multi-Stage Dockerfile und Docker Compose mit PostgreSQL produktionsreif betreibst.

Payload CMS mit Docker installieren: Fullstack Next.js Headless CMS selbst hosten mit flexiblen Datenmodellen, REST und GraphQL API, MongoDB, Docker Container und eigener Server Infrastruktur. KI-generiert

Payload CMS ist ein TypeScript-natives, Open-Source Headless CMS, das vollständig in Next.js integriert läuft. Anders als klassische CMS-Lösungen bringt Payload v3 REST-API, GraphQL-Endpunkt und Admin-Panel als Teil der eigenen Next.js-App mit – alles in einem einzigen Node.js-Prozess. Mit 43.000 GitHub-Stars, über 15.000 Commits und der aktuellen Version 3.85.1 (Stand Juni 2026) ist es die leistungsfähigste Option für Entwicklerteams, die ein programmatisch konfigurierbares, typsicheres CMS ohne externen SaaS-Anbieter benötigen. Wer Payload CMS per Docker betreiben will, muss dabei eines wissen: Es gibt kein fertiges Image auf Docker Hub – jede Instanz wird aus dem eigenen Next.js-Projektcode gebaut. Diese Anleitung führt dich Schritt für Schritt durch den Aufbau des Multi-Stage Dockerfiles, der compose.yaml mit PostgreSQL und der ersten Admin-Einrichtung im Browser.

Voraussetzungen

  1. Docker Engine 24+ und Docker Compose v2 (docker compose, nicht docker-compose) auf einem Linux-Host, einer VM oder einem NAS – wie du Docker auf Ubuntu/Debian installierst, erklärt die Docker-Grundlagen-Anleitung.
  2. Eigenes Payload CMS / Next.js Projekt-Repository – erstellt via npx create-payload-app oder manuell. Die Konfiguration (Collections, Felder, Hooks) liegt im Quellcode.
  3. Node.js 20.9+ lokal (nur für Entwicklung und Code-Editierung, nicht zwingend auf dem Server).
  4. pnpm 9+ oder npm als Paketmanager im Projekt (Dockerfile und Build nutzen ihn).
  5. openssl zum Generieren des PAYLOAD_SECRET: openssl rand -base64 32.
  6. Mindestens 1 GB RAM für den laufenden Container; der Build-Prozess benötigt ca. 2 GB.
  7. Reverse Proxy für HTTPS (z. B. Nginx, Traefik oder Caddy) empfohlen für den Produktivbetrieb – die Anleitung Traefik als Docker-Reverse-Proxy erklärt das Setup.

Eckdaten auf einen Blick

ParameterWertHinweis
Image (Payload)Selbst gebaut (kein Hub-Image)Multi-Stage, node:22-alpine Basis
Image (Datenbank)postgres:16-alpineOffizielles Image, Docker Hub; Tag pinnen
Port3000:3000App + Admin-Panel (/admin) + API (/api)
Volume Medienpayload-media:/app/public/mediaPflicht – sonst gehen Uploads verloren
Volume Datenbankpayload-postgres-data:/var/lib/postgresql/dataPflicht für Datenpersistenz
Pflicht-EnvsPAYLOAD_SECRET, DATABASE_URI, POSTGRES_PASSWORDAlle in .env speichern, nie committen
LizenzMIT (Open Source)Kein SaaS-Pflichtabo, kein Vendor-Lock-in

Schritt 1: Projektordner und Dateien vorbereiten

Lege auf deinem Server einen Arbeitsordner an und wechsle hinein. Falls du dein Payload-Projekt schon lokal hast, überträgst du es per Git oder SCP. Der Ordner enthält am Ende folgende Kerndateien:

mkdir -p /opt/payload-cms
cd /opt/payload-cms
# Projekt per Git klonen (oder Dateien übertragen):
git clone https://github.com/dein-user/dein-payload-projekt.git .

Stelle sicher, dass dein Projekt die folgenden drei Dateien enthält bzw. lege sie an – sie sind die Grundlage des Docker-Builds:

  1. Dockerfile – Multi-Stage Build (folgt in Schritt 2)
  2. .dockerignore – schließt node_modules, .next und .env aus
  3. next.config.mjs – muss zwingend output: 'standalone' enthalten

Prüfe bzw. ergänze next.config.mjs:

# next.config.mjs (Auszug – output: standalone ist Pflicht)
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
  // ... weitere Konfiguration
};
export default nextConfig;

Lege außerdem eine .dockerignore an, damit der Build-Context schlank bleibt:

node_modules
.next
.env
.env.local
dist

Verifizieren: Führe ls -la /opt/payload-cms aus. Du solltest package.json, next.config.mjs, payload.config.ts (oder .js), .dockerignore und dein src/-Verzeichnis sehen. Fehlt output: 'standalone' in next.config.mjs, schlägt der spätere Build fehl.

Schritt 2: Multi-Stage Dockerfile erstellen

Da Payload kein fertiges Docker-Image anbietet, baust du das Image selbst. Ein Multi-Stage Dockerfile hält das finale Image klein: Der builder-Stage kompiliert die App, der schlanke runner-Stage enthält nur das Nötigste für den Betrieb.

Wichtig: Payload benötigt beim Build eine laufende Datenbank für Migrationen und Schema-Generierung. Die empfohlene Lösung ist, Migrationen vom Build zu trennen und als Entrypoint beim Container-Start auszuführen (siehe unten). Alternativ kannst du DATABASE_URI als Build-Argument übergeben – das erfordert aber eine erreichbare DB zur Build-Zeit.

# Dockerfile (Multi-Stage, pnpm-Variante – npm-Äquivalent in Kommentaren)
FROM node:22-alpine AS base
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app

# --- Dependencies ---
FROM base AS deps
COPY package.json pnpm-lock.yaml* ./
RUN pnpm install --frozen-lockfile

# --- Builder ---
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# Migrationen werden NICHT hier ausgeführt – separat im Entrypoint
RUN pnpm build

# --- Runner (schlankes Production-Image) ---
FROM node:22-alpine AS runner
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

# Nur Standalone-Output und statische Assets kopieren
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
COPY --from=builder /app/src/migrations ./src/migrations

# Entrypoint: Migrationen ausführen, dann App starten
COPY docker-entrypoint.sh ./
RUN chmod +x docker-entrypoint.sh

EXPOSE 3000
ENTRYPOINT ["./docker-entrypoint.sh"]

Erstelle außerdem das Entrypoint-Script docker-entrypoint.sh im Projektordner:

#!/bin/sh
set -e
echo "Führe Payload-Migrationen aus..."
node_modules/.bin/payload migrate
echo "Starte Payload CMS..."
exec node server.js

Verifizieren: Prüfe beide Dateien lokal mit ls -la Dockerfile docker-entrypoint.sh. Der Build selbst wird erst in Schritt 4 ausgeführt – dort siehst du, ob alle Stages fehlerfrei durchlaufen.

Schritt 3: .env-Datei mit Secrets anlegen

Alle sensitiven Werte kommen in eine .env-Datei, die niemals ins Git-Repository committet wird. Docker Compose liest sie automatisch aus dem Projektordner.

Generiere zunächst einen sicheren PAYLOAD_SECRET (mindestens 32 Zeichen):

openssl rand -base64 32
# Beispiel-Ausgabe: k8Xz2mN9qRvTpLwYbJdA4eFcHsUiOgKn...

Lege dann die .env-Datei an:

# /opt/payload-cms/.env – NICHT ins Git committen!

# Kryptografischer Schlüssel (mind. 32 Zeichen, zufällig)
PAYLOAD_SECRET=dein-langer-zufaelliger-schluessel-hier

# PostgreSQL-Passwort (frei wählbar, sicher)
POSTGRES_PASSWORD=sicheres-db-passwort-2026

# Öffentliche URL der Instanz (wichtig für Media-Links)
NEXT_PUBLIC_SERVER_URL=https://cms.deine-domain.de

Der DATABASE_URI wird direkt in der compose.yaml aus diesen Variablen zusammengesetzt und muss nicht separat in die .env. Das hält die Konfiguration übersichtlich.

Verifizieren: Führe grep -c PAYLOAD_SECRET /opt/payload-cms/.env aus – Ausgabe muss 1 sein. Prüfe außerdem, dass .env in der .gitignore steht: grep .env /opt/payload-cms/.gitignore.

Schritt 4: compose.yaml anlegen und Stack starten

Die compose.yaml definiert beide Services: den selbst gebauten Payload-Container und PostgreSQL mit Healthcheck. Der Healthcheck stellt sicher, dass der Payload-Container erst startet, wenn PostgreSQL tatsächlich Verbindungen akzeptiert – depends_on allein reicht dafür nicht aus.

# /opt/payload-cms/compose.yaml
services:
  payload:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: payload
    restart: unless-stopped
    ports:
      - "3000:3000"
    volumes:
      - payload-media:/app/public/media
    environment:
      NODE_ENV: production
      DATABASE_URI: postgres://payload:${POSTGRES_PASSWORD}@payload-postgres:5432/payload
      PAYLOAD_SECRET: ${PAYLOAD_SECRET}
      NEXT_PUBLIC_SERVER_URL: ${NEXT_PUBLIC_SERVER_URL:-http://localhost:3000}
      NEXT_TELEMETRY_DISABLED: "1"
      HOSTNAME: "0.0.0.0"
      PORT: "3000"
    depends_on:
      payload-postgres:
        condition: service_healthy

  payload-postgres:
    image: postgres:16-alpine
    container_name: payload-postgres
    restart: unless-stopped
    volumes:
      - payload-postgres-data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: payload
      POSTGRES_USER: payload
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U payload"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  payload-media:
  payload-postgres-data:

Starte nun den Stack. Beim ersten Start wird das Image gebaut – das dauert je nach Hardware und Internetverbindung 3–10 Minuten:

cd /opt/payload-cms
docker compose up -d --build

Verfolge den Build-Fortschritt und den anschließenden Start:

docker compose logs -f

Verifizieren: Nach dem Start prüfst du den Status beider Container:

docker compose ps

Erwartete Ausgabe (beide Container running, PostgreSQL healthy):

NAME               IMAGE                STATUS                   PORTS
payload            payload-cms-payload  Up 2 minutes             0.0.0.0:3000->3000/tcp
payload-postgres   postgres:16-alpine   Up 2 minutes (healthy)

Prüfe anschließend die HTTP-Erreichbarkeit:

curl -I http://localhost:3000
# Erwartete Antwort: HTTP/1.1 200 OK  oder  HTTP/1.1 302 Found

Schritt 5: Admin-Benutzer anlegen und Admin-Panel prüfen

Payload CMS verwendet beim ersten Aufruf von /admin einen einmaligen Registrierungs-Wizard. Es gibt keinen separaten CLI-Befehl – alles läuft im Browser.

Öffne im Browser:

http://localhost:3000/admin
# Bei Remote-Server: http://server-ip:3000/admin

Du siehst ein Formular zur Anlage des ersten Admin-Benutzers (E-Mail-Adresse + Passwort). Fülle es aus und bestätige. Nach dem Absenden ist die Registrierung für weitere Nutzer gesperrt – neue Benutzer können nur noch über das Admin-Panel oder die API angelegt werden.

Navigiere nach dem Login durch das Admin-Panel: Unter Collections siehst du alle konfigurierten Inhaltstypen deiner payload.config.ts. Unter Globals findest du globale Einstellungen.

Verifizieren: Prüfe die Logs auf Fehler nach dem ersten Login:

docker compose logs payload --tail=50

Keine Fehlermeldungen zu Datenbankverbindung oder fehlenden Umgebungsvariablen bedeutet: Payload CMS läuft korrekt. Außerdem sollte die REST-API antworten:

curl http://localhost:3000/api/users/me
# Erwartete Antwort: JSON mit {"message":"You must be logged in..."} oder Benutzer-Objekt

Schritt 6: Reverse Proxy und HTTPS einrichten (Produktion)

Für den Produktivbetrieb solltest du Payload CMS hinter einem Reverse Proxy mit TLS betreiben. Port 3000 bleibt dabei intern und wird nicht direkt ans Internet exponiert. Wie du Traefik als automatisch-HTTPS-Reverse-Proxy vor deinen Docker-Stack schaltest, beschreibt die Anleitung Traefik als Docker-Reverse-Proxy mit automatischem HTTPS.

Alternativ funktioniert auch Caddy oder Nginx Proxy Manager. Wichtig nach dem Umstellen auf HTTPS: Passe NEXT_PUBLIC_SERVER_URL in der .env auf die HTTPS-URL an und führe docker compose up -d --build erneut aus – der Wert wird beim Build eingebettet.

Verifizieren: Nach dem Einrichten des Reverse Proxys:

curl -I https://cms.deine-domain.de/admin
# Erwartete Antwort: HTTP/2 200  oder  HTTP/2 302

Prüfe außerdem, ob Medien-Uploads korrekte absolute URLs haben – ein Bild hochladen und die angezeigte URL im Admin-Panel kontrollieren. Sie muss mit https://cms.deine-domain.de/ beginnen.

Schritt 7: Updates und Backups

Da das Image selbst gebaut wird, bedeutet ein Update: Payload-Version in package.json erhöhen, dann neu bauen.

# Payload auf neue Version aktualisieren (Beispiel mit pnpm):
pnpm update @payloadcms/next @payloadcms/db-postgres @payloadcms/richtext-lexical
# Danach Image neu bauen und Stack neustarten:
docker compose up -d --build

Für Datenbankbackups nutzt du pg_dump direkt aus dem laufenden PostgreSQL-Container:

docker exec payload-postgres pg_dump -U payload payload \
  > /opt/backups/payload-$(date +%Y%m%d).sql

Wie du PostgreSQL-Backups automatisierst und per rclone in die Cloud synchronisierst, erklärt die Anleitung MySQL & PostgreSQL Backup automatisieren mit cron. Für Mediendateien sicherst du das Volume payload-media – am einfachsten mit einem tar-Archiv oder rsync.

Verifizieren: Nach dem Update:

docker compose ps
# Beide Container wieder Up + healthy

curl -I http://localhost:3000/admin
# HTTP 200 oder 302

docker compose logs payload --tail=20
# Keine Fehler, Migrationsmeldungen sollten erfolgreich abgeschlossen sein

Troubleshooting / Typische Fehler

  1. „connect ECONNREFUSED 127.0.0.1:5432" beim Build: Der Payload-Build benötigt eine laufende Datenbank. Stelle sicher, dass Migrationen nicht im Build-Stage, sondern im Entrypoint-Script ausgeführt werden (wie im Dockerfile oben). Alternativ: DATABASE_URI als Build-Argument übergeben, wenn die DB zur Build-Zeit erreichbar ist.
  2. Container startet, aber /admin ist nicht erreichbar: HOSTNAME=0.0.0.0 fehlt oder ist falsch gesetzt. Ohne diese Variable lauscht der Next.js-Server nur auf localhost – im Container nicht von außen erreichbar. Prüfen: docker compose logs payload | grep -i hostname.
  3. Uploads verschwinden nach Container-Neustart: Kein Named Volume für /app/public/media definiert. Prüfen: docker inspect payload | grep -A5 Mounts. Volume payload-media muss in der compose.yaml eingetragen sein.
  4. „output: standalone not set" / server.js fehlt: next.config.mjs enthält nicht output: 'standalone'. Ergänzen, dann docker compose up -d --build.
  5. „Invalid or missing PAYLOAD_SECRET": Secret ist leer, zu kurz oder nicht in der .env. Muss mindestens 32 Zeichen sein. Generieren mit openssl rand -base64 32.
  6. „Database connection failed" beim Start: depends_on ohne condition: service_healthy oder fehlendes pg_isready-Healthcheck. PostgreSQL akzeptiert erst nach vollständigem Hochfahren Verbindungen. Healthcheck in compose.yaml (wie oben) ist Pflicht.
  7. Broken Images / Medien zeigen falsche URLs: NEXT_PUBLIC_SERVER_URL fehlt oder zeigt auf falsche Domain. Payload baut absolute Medien-URLs aus dieser Variable. Nach Änderung: neu bauen mit --build.
  8. Build sehr langsam oder pnpm install schlägt fehl: .dockerignore fehlt. Ohne sie werden node_modules (oft >500 MB) in den Build-Context kopiert. .dockerignore anlegen (siehe Schritt 1).
  9. Kein Image via docker pull payloadcms/payload: Es gibt kein offizielles pre-built Image auf Docker Hub oder ghcr.io. Das ist konzeptionell so gewollt – Payload-Konfiguration liegt im eigenen Code.

Häufige Fragen

Kann ich Payload CMS ohne eigenen Quellcode als fertiges Docker-Image betreiben?

Nein. Es gibt kein offizielles pre-built Image auf Docker Hub oder der GitHub Container Registry. Payload CMS wird immer zusammen mit der eigenen Next.js-App gebaut – das Docker-Image enthält deinen Code. Das ist konzeptionell gewollt: Collections, Felder und Hooks werden im Quellcode (TypeScript) konfiguriert und sind beim Build-Zeitpunkt bekannt.

Welche Datenbank soll ich in Production verwenden?

Für Produktionsbetrieb empfiehlt die Community PostgreSQL (postgres:16-alpine), auch wenn die offizielle Dokumentation im Docker-Beispiel noch MongoDB zeigt. PostgreSQL bietet bessere ACID-Compliance und Performance für größere Datenmengen. MongoDB ist ebenfalls vollständig unterstützt. SQLite eignet sich nur für lokale Entwicklung – nicht für Multi-Instance-Deployments oder Produktionsumgebungen.

Wie lege ich den ersten Admin-Benutzer an?

Beim ersten Aufruf von http://localhost:3000/admin im Browser erscheint automatisch ein Registrierungsformular. Du gibst E-Mail-Adresse und Passwort ein – fertig. Es gibt keinen CLI-Befehl. Nach der ersten Registrierung ist das Formular gesperrt; weitere Benutzer werden nur noch über das Admin-Panel oder die API angelegt.

Wie führe ich Datenbankmigrationen durch?

In dieser Anleitung laufen Migrationen beim Container-Start via docker-entrypoint.sh (payload migrate vor dem Starten von server.js). Die Migrations-Dateien liegen im src/migrations/-Verzeichnis und werden von Payload automatisch generiert. Nach einem Payload-Update kannst du neue Migrationen lokal generieren (pnpm payload migrate:create) und mit dem Code committen.

Kann ich Payload CMS auf einem ARM64-Server betreiben?

Ja. Das Basis-Image node:22-alpine ist multi-arch und unterstützt sowohl amd64 als auch arm64 (Apple Silicon, Raspberry Pi 4/5 64-bit, Oracle Ampere). Einige native Module wie sharp für Bildoptimierung benötigen eventuell plattformspezifische Build-Flags im Dockerfile.

Wie aktualisiere ich Payload CMS auf eine neue Version?

Erhöhe die @payloadcms/*-Pakete in der package.json auf die neue Version, committe die Änderungen, überträge sie auf den Server und führe docker compose up -d --build aus. Neue Migrationen werden beim nächsten Container-Start automatisch über das Entrypoint-Script ausgeführt.

Was passiert mit Mediendateien bei einem Cloud-Deployment ohne persistenten Speicher?

Auf Cloud-PaaS-Plattformen mit ephemerem Dateisystem (z. B. Fly.io, Render ohne persistente Disks) gehen Uploads verloren. In diesem Fall musst du einen Cloud-Storage-Adapter integrieren: Payload bietet offizielle Adapter für AWS S3, Azure Blob Storage und Google Cloud Storage. Das Named Volume payload-media in der compose.yaml ist nur für Self-Hosted-Szenarien mit persistentem Speicher ausreichend.

Fazit

Payload CMS v3 ist eine außergewöhnlich leistungsfähige Wahl für Next.js-Teams, die ein versioniertes, typsicheres CMS ohne Kompromisse bei der Kontrolle benötigen. Der Docker-Betrieb erfordert einen eigenen Build-Schritt – aber genau das ist die Stärke: Das Image enthält immer deinen Code, deine Collections und deine Konfiguration. Das Ergebnis ist ein vollständig selbstgehosteter, DSGVO-konformer Content-Server ohne monatliche SaaS-Gebühren und ohne Vendor-Lock-in. Der einmalige Setup-Aufwand (Dockerfile, Entrypoint, compose.yaml) zahlt sich spätestens beim zweiten Projekt aus, wenn die Vorlage wiederverwendet werden kann.

Für Teams, die ein ähnliches Konzept mit weniger Build-Aufwand suchen, ist Directus mit Docker eine interessante Alternative – es bringt ein fertiges Image mit und verwandelt bestehende SQL-Datenbanken direkt in eine API. Für reine Datenbankaufgaben ohne CMS-Schicht lohnt ein Blick auf die PostgreSQL-Backup- und Migrations-Anleitung.

Weiterführende Anleitungen und Quellen

  1. Docker Compose: Multi-Container-Stacks aufbauen – Compose-Syntax und Stack-Konzepte vertieft erklärt
  2. Traefik als Docker-Reverse-Proxy mit automatischem HTTPS – Payload hinter TLS betreiben
  3. MySQL & PostgreSQL Backup automatisieren mit cron – Payload-Datenbank regelmäßig sichern
  4. Docker Compose absichern: Secrets, Healthchecks, Non-Root – Produktionshärtung für den Stack
  5. Directus mit Docker – Alternative Headless-CMS-Lösung mit fertigem Image

Offizielle Quellen: Payload CMS – Production Deployment | Payload CMS auf GitHub | Sliplane: How to Run Payload CMS in Docker