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 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
- Docker Engine 24+ und Docker Compose v2 (
docker compose, nichtdocker-compose) auf einem Linux-Host, einer VM oder einem NAS – wie du Docker auf Ubuntu/Debian installierst, erklärt die Docker-Grundlagen-Anleitung. - Eigenes Payload CMS / Next.js Projekt-Repository – erstellt via
npx create-payload-appoder manuell. Die Konfiguration (Collections, Felder, Hooks) liegt im Quellcode. - Node.js 20.9+ lokal (nur für Entwicklung und Code-Editierung, nicht zwingend auf dem Server).
- pnpm 9+ oder npm als Paketmanager im Projekt (Dockerfile und Build nutzen ihn).
- openssl zum Generieren des
PAYLOAD_SECRET:openssl rand -base64 32. - Mindestens 1 GB RAM für den laufenden Container; der Build-Prozess benötigt ca. 2 GB.
- 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
| Parameter | Wert | Hinweis |
|---|---|---|
| Image (Payload) | Selbst gebaut (kein Hub-Image) | Multi-Stage, node:22-alpine Basis |
| Image (Datenbank) | postgres:16-alpine | Offizielles Image, Docker Hub; Tag pinnen |
| Port | 3000:3000 | App + Admin-Panel (/admin) + API (/api) |
| Volume Medien | payload-media:/app/public/media | Pflicht – sonst gehen Uploads verloren |
| Volume Datenbank | payload-postgres-data:/var/lib/postgresql/data | Pflicht für Datenpersistenz |
| Pflicht-Envs | PAYLOAD_SECRET, DATABASE_URI, POSTGRES_PASSWORD | Alle in .env speichern, nie committen |
| Lizenz | MIT (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:
Dockerfile– Multi-Stage Build (folgt in Schritt 2).dockerignore– schließtnode_modules,.nextund.envausnext.config.mjs– muss zwingendoutput: '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
distVerifizieren: 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.jsVerifizieren: 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.deDer 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 --buildVerfolge den Build-Fortschritt und den anschließenden Start:
docker compose logs -fVerifizieren: Nach dem Start prüfst du den Status beider Container:
docker compose psErwartete 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 FoundSchritt 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/adminDu 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=50Keine 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-ObjektSchritt 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 302Prü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 --buildFü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).sqlWie 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 seinTroubleshooting / Typische Fehler
- „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_URIals Build-Argument übergeben, wenn die DB zur Build-Zeit erreichbar ist. - Container startet, aber
/administ nicht erreichbar:HOSTNAME=0.0.0.0fehlt oder ist falsch gesetzt. Ohne diese Variable lauscht der Next.js-Server nur auflocalhost– im Container nicht von außen erreichbar. Prüfen:docker compose logs payload | grep -i hostname. - Uploads verschwinden nach Container-Neustart: Kein Named Volume für
/app/public/mediadefiniert. Prüfen:docker inspect payload | grep -A5 Mounts. Volumepayload-mediamuss in der compose.yaml eingetragen sein. - „output: standalone not set" / server.js fehlt:
next.config.mjsenthält nichtoutput: 'standalone'. Ergänzen, danndocker compose up -d --build. - „Invalid or missing PAYLOAD_SECRET": Secret ist leer, zu kurz oder nicht in der
.env. Muss mindestens 32 Zeichen sein. Generieren mitopenssl rand -base64 32. - „Database connection failed" beim Start:
depends_onohnecondition: service_healthyoder fehlendespg_isready-Healthcheck. PostgreSQL akzeptiert erst nach vollständigem Hochfahren Verbindungen. Healthcheck in compose.yaml (wie oben) ist Pflicht. - Broken Images / Medien zeigen falsche URLs:
NEXT_PUBLIC_SERVER_URLfehlt oder zeigt auf falsche Domain. Payload baut absolute Medien-URLs aus dieser Variable. Nach Änderung: neu bauen mit--build. - Build sehr langsam oder
pnpm installschlägt fehl:.dockerignorefehlt. Ohne sie werdennode_modules(oft >500 MB) in den Build-Context kopiert..dockerignoreanlegen (siehe Schritt 1). - 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
- Docker Compose: Multi-Container-Stacks aufbauen – Compose-Syntax und Stack-Konzepte vertieft erklärt
- Traefik als Docker-Reverse-Proxy mit automatischem HTTPS – Payload hinter TLS betreiben
- MySQL & PostgreSQL Backup automatisieren mit cron – Payload-Datenbank regelmäßig sichern
- Docker Compose absichern: Secrets, Healthchecks, Non-Root – Produktionshärtung für den Stack
- 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