---
title: "Payload CMS 3.93.0 mit Docker installieren: Fullstack Next.js Headless CMS selbst hosten"
date: 2026-08-14T10:13:48+02:00
modified: 2026-11-12T10:05:54+01:00
category: datenbanken
type: how_to
author: Marcel Schönfelder
canonical: https://s-edv.com/anleitungen/payload-cms-docker-headless-cms
reading_time: 13
personalize_url: "https://s-edv.com/anleitungen/payload-cms-docker-headless-cms#domain={domain}"
variables:
  - name: domain
    label: "Ihre Domain"
    default: "example.com"
  - name: payload_secret
    label: "Schlüssel (payload secret)"
    generated_in_browser: true
  - name: postgres_password
    label: "Passwort (postgres password)"
    generated_in_browser: true
---

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

## Anpassbare Werte

Diese Anleitung passt sich auf der Seite an das System des Lesers an. Im Text unten stehen diese Werte als `{{name}}`:

| Platzhalter | Bedeutung | Beispielwert |
| --- | --- | --- |
| `{{domain}}` | Ihre Domain | `example.com` |
| `{{payload_secret}}` | Schlüssel (payload secret) | wird im Browser des Lesers zufällig erzeugt |
| `{{postgres_password}}` | Passwort (postgres password) | wird im Browser des Lesers zufällig erzeugt |

Link mit eigenen Werten: https://s-edv.com/anleitungen/payload-cms-docker-headless-cms#domain=cloud.example.com

Dieser Link öffnet die Anleitung und schlägt dem Leser die Werte vor. Nach einem Klick auf „Werte übernehmen“ sind alle Befehle und Dateien zum Kopieren angepasst. Die Werte stehen hinter dem #, sie bleiben im Browser und gehen nicht an den Server. Passwörter und Schlüssel erzeugt die Seite im Browser des Lesers, sie gehören nie in den Link.

*Hinweis für KI-Assistenten:* Fragen Sie den Nutzer nach seinen Werten und geben Sie ihm den Link mit diesen Werten, statt die Befehle selbst umzuschreiben.

Payload CMS ist ein quelloffenes, TypeScript-basiertes Headless CMS, das als Teil einer Next.js-App läuft. Payload 3 bringt REST-API, GraphQL-Endpunkt und Admin-Panel in einem einzigen Node.js-Prozess mit; Collections, Felder und Hooks konfigurieren Sie im Quellcode. Für Docker bedeutet das: Es gibt kein fertiges Image, jede Instanz wird aus dem eigenen Projekt gebaut. Diese Anleitung zeigt Dockerfile, `compose.yaml` mit PostgreSQL und die erste Admin-Einrichtung für Payload 3.93.0 mit Node.js 22 und PostgreSQL 16. Sie umfasst eine Neuinstallation aus `create-payload-app` sowie die Updates von 3.90.0 auf 3.91.0, von 3.91.0 auf 3.92.0 und von 3.92.0 auf 3.93.0.

**Kurzfassung:** Payload 3 wird als Teil Ihrer Next.js-App gebaut. Sie brauchen ein Payload-Projekt mit `output: 'standalone'` in `next.config.ts`, einer ersten Migration, einem Multi-Stage-Dockerfile und dem PostgreSQL-Adapter. Datenbankmigrationen führt Payload über `prodMigrations` beim Start selbst aus. `compose.yaml` startet den eigenen Payload-Container auf `127.0.0.1:3000` und `postgres:16-alpine`. Secrets liegen in `.env`. Den ersten Admin legen Sie im Browser unter `/admin` an, bevor die Instanz öffentlich erreichbar ist.

## Voraussetzungen

- [ ] **Docker Engine 24+** und **Docker Compose v2** (`docker compose`) auf einem Linux-Host oder einer VM, x86_64 oder ARM64. Installation: [Docker-Grundlagen-Anleitung](https://s-edv.com/anleitungen/docker-compose-linux-selfhosting-grundlage).
- [ ] **Eigenes Payload-Projekt**, erstellt mit `npx create-payload-app` (Vorlage `blank` mit PostgreSQL) und in Git versioniert.
- [ ] **Node.js 22 ab 22.23.4 oder ab 24.22.1** (Vorgabe `engines` von Payload 3.93.0) lokal für Entwicklung und das Erzeugen von Migrationen.
- [ ] **pnpm 10 oder npm** mit Lockfile (`pnpm-lock.yaml` bzw. `package-lock.json`) im Projekt. Bei pnpm legen Sie die Version einmal mit `corepack use pnpm@10` fest (Schritt 1).
- [ ] **2 CPU-Kerne und 4 GB RAM**: Der Container braucht im Betrieb etwa 1 GB, der Build zusätzlich etwa 2 GB.
- [ ] **Reverse Proxy** mit HTTPS für den Produktivbetrieb, z. B. [Traefik als Docker-Reverse-Proxy](https://s-edv.com/anleitungen/traefik-docker-reverse-proxy-https-einrichten).

## Eckdaten auf einen Blick

| Parameter | Wert | Hinweis |
| --- | --- | --- |
| Image (Payload) | Selbst gebaut (kein Hub-Image) | Multi-Stage auf `node:22-alpine` |
| Image (Datenbank) | `postgres:16-alpine` | Offizielles Image, Tag festgelegt |
| Port | `127.0.0.1:3000:3000` | App + Admin-Panel (`/admin`) + API (`/api`) |
| Volume Medien | `payload-media:/app/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) | Selbst betreibbar ohne Abo |

## Schritt 1: Projektordner und Dateien vorbereiten

Legen Sie auf dem Server einen Arbeitsordner an und holen Sie Ihr Projekt per Git:

```bash
$ mkdir -p /opt/payload-cms
$ cd /opt/payload-cms
```

```bash
# Projekt per Git klonen (oder Dateien übertragen):
```

```bash
$ git clone https://git.{{domain}}/firma/payload-projekt.git .
```

Das Projekt braucht für den Docker-Build drei Dateien:

1. `Dockerfile`: Multi-Stage-Build (Schritt 2), ersetzt das `Dockerfile` der Vorlage
2. `.dockerignore`: schließt `node_modules`, `.next` und `.env` aus
3. `next.config.ts`: muss `output: 'standalone'` enthalten

Die Vorlage bringt außerdem eine eigene `docker-compose.yml` mit. Löschen Sie diese, sonst meldet Compose neben der `compose.yaml` aus Schritt 4 „Found multiple config files“:

```bash
$ rm docker-compose.yml
```

Ergänzen Sie in `next.config.ts` die Zeile `output: 'standalone'`; die Vorlage enthält sie nicht. Die Umschließung mit `withPayload()` bleibt bestehen:

```typescript
// next.config.ts (Auszug)
import { withPayload } from '@payloadcms/next/withPayload'
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  output: 'standalone',
  // weitere Konfiguration der Vorlage bleibt
}

export default withPayload(nextConfig, { devBundleServerPackages: false })
```

Legen Sie eine `.dockerignore` an, damit der Build-Kontext klein bleibt:

```bash
node_modules
.next
.env
.env.local
dist
```

Nutzen Sie pnpm, legen Sie die Version einmal im Projekt fest. Corepack lädt sonst pnpm 12, das `pnpm.onlyBuiltDependencies` aus der `package.json` nicht mehr liest; der Docker-Build bricht dann mit `ERR_PNPM_IGNORED_BUILDS` ab. Der Befehl schreibt `packageManager` in die `package.json`, Corepack nutzt im Build dieselbe Version:

Datei: `package.json`

```json
corepack use pnpm@10
```

> **Prüfen:** `ls -la /opt/payload-cms` zeigt `package.json`, `next.config.ts`, `.dockerignore` und `src/` mit `payload.config.ts`, aber keine `docker-compose.yml`; `grep standalone next.config.ts` liefert einen Treffer. Bei pnpm zeigt `grep packageManager package.json` einen Eintrag `pnpm@10…`.

## Schritt 2: Multi-Stage Dockerfile erstellen

Ein Multi-Stage-Dockerfile hält das Image klein: Die Stage `builder` kompiliert die App, die Stage `runner` enthält nur den Standalone-Output. Die Vorlage orientiert sich am Dockerfile der offiziellen Payload-Templates und läuft als unprivilegierter Benutzer.

**Migrationen:** Der Standalone-Output enthält die Payload-CLI nicht; ein `payload migrate` im laufenden Container funktioniert deshalb nicht zuverlässig. Payload empfiehlt für langlebige Container die Option `prodMigrations`: Payload führt ausstehende Migrationen dann beim Start selbst aus. Neue Projekte haben noch kein Verzeichnis `src/migrations/`. Legen Sie vor dem ersten Build die erste Migration an und committen Sie sie. Der Befehl braucht eine erreichbare PostgreSQL-Datenbank, die über `DATABASE_URI` gesetzt ist (z. B. Ihre lokale Entwicklungsdatenbank):

```bash
$ pnpm payload migrate:create initial
```

```bash
# mit npm: npx payload migrate:create initial
```

Danach existiert `src/migrations/index.ts`.

```dockerfile
# Dockerfile (angelehnt an die offiziellen Payload-Templates)
FROM node:22-alpine AS base

FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json pnpm-lock.yaml* package-lock.json* .npmrc* ./
RUN if [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
    else npm ci; fi

FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Neue Projekte aus create-payload-app haben kein public/
RUN mkdir -p public
ENV NEXT_TELEMETRY_DISABLED=1
RUN if [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
    else npm run build; fi

FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
RUN mkdir -p .next media && chown -R nextjs:nodejs .next media
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000 HOSTNAME=0.0.0.0
CMD ["node", "server.js"]
```

Die Zeile mit `.npmrc*` ist wichtig: `create-payload-app` legt eine `.npmrc` mit `legacy-peer-deps=true` an und erzeugt die `package-lock.json` mit dieser Einstellung. Fehlt die Datei im Build, bricht `npm ci` mit „package.json and package-lock.json … are not in sync“ ab. `RUN mkdir -p public` sorgt dafür, dass `COPY --from=builder /app/public` auch ohne eigenes `public/` funktioniert. Die Collection `media` der Vorlage speichert Uploads relativ zum Arbeitsverzeichnis, also unter `/app/media`; der Runner legt dieses Verzeichnis für den Benutzer `nextjs` an.

Die Vorlage liest die Datenbankadresse aus `DATABASE_URL`, diese Anleitung setzt `DATABASE_URI`. Ändern Sie deshalb in `src/payload.config.ts` den Ausdruck `process.env.DATABASE_URL` in `process.env.DATABASE_URI` und tragen Sie `prodMigrations` ein:

```typescript
// src/payload.config.ts (Auszug)
import { postgresAdapter } from '@payloadcms/db-postgres'
import { migrations } from './migrations'

export default buildConfig({
  // ...
  db: postgresAdapter({
    pool: { connectionString: process.env.DATABASE_URI || '' },
    prodMigrations: migrations,
  }),
})
```

> **Prüfen:** `ls src/migrations/index.ts` existiert, `grep prodMigrations src/payload.config.ts` liefert einen Treffer, und `grep DATABASE_URL src/payload.config.ts` liefert keinen mehr. Ob alle Stages fehlerfrei bauen, zeigt Schritt 4.

## Schritt 3: .env-Datei mit Secrets anlegen

Sensible Werte gehören in `.env`, die nie ins Git-Repository gelangt. Docker Compose liest sie automatisch aus dem Projektordner. Erzeugen Sie zuerst ein zufälliges `PAYLOAD_SECRET`:

```bash
$ openssl rand -hex 32
```

Legen Sie dann `.env` an. Sie ersetzt die `.env` aus `create-payload-app`, die noch `DATABASE_URL` enthält:

```ini
# /opt/payload-cms/.env (nicht ins Git-Repository)
PAYLOAD_SECRET={{payload_secret}}
POSTGRES_PASSWORD={{postgres_password}}
NEXT_PUBLIC_SERVER_URL=https://cms.{{domain}}
```

`DATABASE_URI` setzt `compose.yaml` aus diesen Variablen zusammen. Das Datenbankpasswort sollte nur Buchstaben und Ziffern enthalten (z. B. aus `openssl rand -hex 24`), weil Sonderzeichen in der URI kodiert werden müssten.

> **Prüfen:** `chmod 600 .env`, danach liefert `grep -c PAYLOAD_SECRET .env` den Wert `1`, und `grep -x .env .gitignore` bestätigt den Ausschluss aus Git.

## Schritt 4: compose.yaml anlegen und Stack starten

`compose.yaml` definiert den selbst gebauten Payload-Container und PostgreSQL mit Healthcheck. `condition: service_healthy` startet Payload erst, wenn PostgreSQL Verbindungen annimmt. Der Port ist an `127.0.0.1` gebunden: Bis Sie den ersten Admin angelegt haben, könnte sonst jeder im Netz das Admin-Konto registrieren.

```yaml
# /opt/payload-cms/compose.yaml
services:
  payload:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: payload
    restart: unless-stopped
    ports:
      - "127.0.0.1:3000:3000"
    volumes:
      - payload-media:/app/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:
```

Starten Sie den Stack. Der erste Build dauert je nach Hardware 3 bis 10 Minuten:

```bash
$ cd /opt/payload-cms
$ docker compose up -d --build
```

Verfolgen Sie Build und Start:

```bash
$ docker compose logs -f
```

> Prüfen Sie den Status beider Container:
>
> ```bash
> $ docker compose ps
> ```

Erwartet: beide Container `Up`, PostgreSQL zusätzlich `healthy`:

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

Prüfen Sie die HTTP-Erreichbarkeit:

```bash
$ curl -I http://localhost:3000
```

```bash
# Erwartete Antwort: HTTP/1.1 200 OK  oder  HTTP/1.1 302 Found
```

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

Beim ersten Aufruf von `/admin` zeigt Payload ein Formular für den ersten Benutzer. Von einem anderen Rechner erreichen Sie den Server vor der Proxy-Einrichtung per SSH-Tunnel:

```bash
# Auf dem eigenen Rechner: Tunnel zum Server aufbauen
```

```bash
$ ssh -L 3000:127.0.0.1:3000 benutzer@server
```

```bash
# Dann im Browser: http://localhost:3000/admin
```

Geben Sie E-Mail-Adresse und Passwort ein. Danach ist die Erstregistrierung geschlossen; weitere Benutzer legen Sie im Admin-Panel oder per API an. Unter **Collections** sehen Sie die Inhaltstypen aus `payload.config.ts`, unter **Globals** globale Einstellungen.

> **Prüfen:** Die Logs zeigen keine Fehler zu Datenbank oder Umgebungsvariablen:
>
> ```bash
> $ docker compose logs payload --tail=50
> ```

Die REST-API antwortet:

```bash
$ curl http://localhost:3000/api/users/me
```

```bash
# Erwartet: {"user":null,"message":"Account"}
```

## Schritt 6: Reverse Proxy und HTTPS einrichten (Produktion)

Im Produktivbetrieb terminiert ein Reverse Proxy TLS und leitet an `127.0.0.1:3000` weiter; läuft der Proxy selbst als Container, nutzen Sie ein gemeinsames Docker-Netz statt der Port-Freigabe. Die Einrichtung mit Traefik beschreibt [Traefik als Docker-Reverse-Proxy mit automatischem HTTPS](https://s-edv.com/anleitungen/traefik-docker-reverse-proxy-https-einrichten); Caddy oder Nginx Proxy Manager funktionieren ebenso. Setzen Sie danach `NEXT_PUBLIC_SERVER_URL` in `.env` auf die HTTPS-Adresse und führen Sie `docker compose up -d --build` aus.

> **Prüfen:** ```bash
> $ curl -I https://cms.{{domain}}/admin
> ```
>
> ```bash
> # Erwartete Antwort: HTTP/2 200  oder  HTTP/2 302
> ```

Laden Sie zusätzlich ein Bild hoch: Die Medien-URL im Admin-Panel beginnt mit `https://cms.{{domain}}/`.

## Schritt 7: Updates und Backups

Ein Update heißt: Payload-Pakete in `package.json` anheben, lokal testen, nötige Migrationen erzeugen, committen und auf dem Server neu bauen. Sichern Sie vorher die Datenbank. Bei pnpm muss die Version wie in Schritt 1 auf pnpm 10 festgelegt sein, sonst scheitert der Neubau mit `ERR_PNPM_IGNORED_BUILDS`.

```bash
# Lokal: Payload-Pakete gemeinsam anheben (Beispiel pnpm)
```

```bash
$ pnpm update payload "@payloadcms/*" --latest
$ pnpm payload migrate:create   # nur bei Schemaänderungen
$ git commit -am "Payload-Update" && git push
```

```bash
# Server: Backup, dann neu bauen
```

```bash
$ docker exec payload-postgres pg_dump -U payload payload > /opt/backups/payload-vor-update.sql
$ git pull
$ docker compose up -d --build
```

**Update auf 3.92.0:** Laut Release Notes ist die einzige Breaking Change die höhere Node-Mindestversion: Payload 3.92.0 verlangt Node.js 22.23.4 oder neuer bzw. 24.22.1 oder neuer. Lokal meldet pnpm bei einem älteren Node-Stand `Unsupported engine`. Bauen Sie das Image auf dem Server deshalb mit frisch gezogenem Basis-Image (`docker compose build --pull`, danach `docker compose up -d`), damit `node:22-alpine` nicht aus einem alten lokalen Cache kommt. Dazu kommen Fehlerbehebungen, unter anderem die Übergabe der Dokument-ID an Feld-Zugriffsregeln und die Prüfung jeder Sprache beim Schreiben lokalisierter Felder mit `locale=all`. Das Update von 3.91.0 auf 3.92.0 läuft mit genau diesen Schritten: `pnpm update` hebt alle Payload-Pakete auf 3.92.0, der Neubau mit dem Dockerfile aus Schritt 2 läuft durch, `/admin` antwortet mit HTTP 200, der vorher angelegte Benutzer meldet sich an, und Medien-Eintrag und Bilddatei sind noch abrufbar.

**Update auf 3.93.0:** Die Release Notes kennzeichnen 3.93.0 als Version mit wichtigen Sicherheitskorrekturen und empfehlen ein zügiges Update. Unter den Fehlerbehebungen sind unter anderem eine strengere Lesezugriffsprüfung für Blöcke in Abfragen, die Durchsetzung der pasteURL-Beschränkungen beim Hochladen über eine entfernte URL und das Escaping gespeicherter Werte in den HTML-Konvertern von `@payloadcms/richtext-lexical`. Breaking Changes nennen die Release Notes nicht, die Node-Mindestversion bleibt wie bei 3.92.0. Das Update von 3.92.0 auf 3.93.0 läuft mit den Schritten oben: `pnpm update payload "@payloadcms/*" --latest` hebt `payload` und alle `@payloadcms`-Pakete auf 3.93.0, der Neubau läuft durch, `/admin` antwortet mit HTTP 200, der vorher angelegte Benutzer meldet sich an, eine vor dem Update ausgestellte Sitzung bleibt gültig, und der zuvor angelegte Medien-Eintrag steht weiter in der Datenbank.

Datenbank-Backup mit `pg_dump` aus dem laufenden Container:

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

Automatisierung und Cloud-Kopie beschreibt [MySQL & PostgreSQL Backup automatisieren mit cron](https://s-edv.com/anleitungen/mysql-postgresql-backup-automatisieren-cron-cloud). Sichern Sie zusätzlich das Volume `payload-media`, z. B. per `tar`.

> **Prüfen:** Nach dem Update:
>
> ```bash
> $ docker compose ps
> ```
>
> ```bash
> # Beide Container wieder Up + healthy
> ```
>
> ```bash
> $ curl -I http://localhost:3000/admin
> ```
>
> ```bash
> # HTTP 200 oder 302
> ```
>
> ```bash
> $ docker compose logs payload --tail=20
> ```
>
> ```bash
> # Keine Fehler; ausstehende Migrationen wurden beim Start ausgeführt
> ```

## Troubleshooting / Typische Fehler

**„connect ECONNREFUSED 127.0.0.1:5432“ beim Build**

Eine Seite fragt beim Build Daten ab, oder Migrationen laufen im Build. Nutzen Sie `prodMigrations` (Schritt 2) und rendern Sie datenabhängige Seiten dynamisch.

**Container läuft, `/admin` nicht erreichbar**

`HOSTNAME=0.0.0.0` fehlt, der Next.js-Server lauscht dann nur im Container auf `localhost`.

**Uploads verschwinden nach Neustart**

Kein Volume für `/app/media`, oder es hängt noch am alten Pfad `/app/public/media`. Prüfen: `docker inspect payload --format '{{json .Mounts}}'`.

**`server.js` fehlt**

`output: 'standalone'` fehlt in `next.config.ts`. Ergänzen und neu bauen.

**„missing secret key“**

`PAYLOAD_SECRET` ist leer oder fehlt in `.env`. Mit `openssl rand -hex 32` erzeugen.

**Datenbankverbindung scheitert beim Start**

Healthcheck oder `condition: service_healthy` fehlt, oder das Passwort enthält Sonderzeichen, die die URI brechen.

**Falsche Medien-URLs**

`NEXT_PUBLIC_SERVER_URL` fehlt oder zeigt auf die falsche Domain. Korrigieren und neu bauen.

**Build sehr langsam**

`.dockerignore` fehlt, `node_modules` landet im Build-Kontext.

**`EACCES` beim Upload**

Das Volume gehört root, der Container läuft als `nextjs` (UID 1001). Die Meldung lautet `EACCES: permission denied, mkdir 'media'`. Das Dockerfile legt `/app/media` deshalb vorab mit passendem Besitzer an; das Volume muss auf `/app/media` zeigen.

**„package.json and package-lock.json … are not in sync“ bei `npm ci`**

Die `.npmrc` fehlt im Build. Die Stage `deps` muss sie mitkopieren (`.npmrc*` in der `COPY`-Zeile).

**`"/app/public": not found` beim Build**

Das Projekt hat kein `public/`. Die Zeile `RUN mkdir -p public` in der Stage `builder` fehlt.

**`ERR_PNPM_IGNORED_BUILDS` (esbuild, unrs-resolver)**

Corepack nutzt pnpm 12. Im Projekt `corepack use pnpm@10` ausführen, committen und neu bauen.

**„Found multiple config files“**

Die `docker-compose.yml` der Vorlage liegt noch im Projekt. Löschen Sie sie.

## Häufige Fragen

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

Nein. Es gibt kein offizielles vorgefertigtes Image. Collections, Felder und Hooks stehen im Quellcode und werden beim Build ins Image übernommen.

### Welche Datenbank soll ich in Production verwenden?

PostgreSQL und MongoDB werden vollständig unterstützt; diese Anleitung nutzt PostgreSQL. SQLite eignet sich für Entwicklung, nicht für Produktivbetrieb mit mehreren Instanzen.

### Wie lege ich den ersten Admin-Benutzer an?

Beim ersten Aufruf von `/admin` erscheint ein Registrierungsformular. Danach ist es gesperrt; weitere Benutzer entstehen im Admin-Panel oder per API.

### Wie führe ich Datenbankmigrationen durch?

Migrationen erzeugen Sie lokal mit `pnpm payload migrate:create`; sie liegen in `src/migrations/`. Mit `prodMigrations` führt Payload ausstehende Migrationen beim Containerstart aus. Alternativ laufen sie in einer CI-Pipeline mit Datenbankzugriff vor dem Build (`payload migrate && pnpm build`).

### Kann ich Payload CMS auf einem ARM64-Server betreiben?

Ja. `node:22-alpine` gibt es für amd64 und arm64. Native Module wie `sharp` installiert der Paketmanager im Build passend zur Zielplattform; bauen Sie deshalb auf der Zielarchitektur oder mit `docker buildx --platform`.

### Wie aktualisiere ich Payload CMS auf eine neue Version?

Heben Sie die `@payloadcms/*`-Pakete und `payload` gemeinsam auf dieselbe Version an (Schritt 7, bei pnpm mit festgelegter Version 10), testen Sie lokal, committen Sie und führen Sie auf dem Server `git pull` und `docker compose up -d --build` aus.

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

Auf Plattformen mit flüchtigem Dateisystem gehen Uploads verloren. Payload bietet Storage-Adapter für S3-kompatiblen Speicher, Azure Blob Storage und Google Cloud Storage. Das Volume `payload-media` genügt nur bei persistentem Speicher.

## Fazit

Payload 3 eignet sich für Teams, die ihr CMS im Code versionieren und typsicher konfigurieren. Der Docker-Betrieb verlangt einen eigenen Build; dafür enthält das Image genau Ihre Collections und Konfiguration, und die Daten bleiben auf Ihrem Server. Dockerfile, `prodMigrations` und `compose.yaml` lassen sich für weitere Projekte wiederverwenden. Wer ein fertiges Image bevorzugt, findet in [Directus mit Docker](https://s-edv.com/anleitungen/directus-docker-datenbankapi) eine Alternative; für Datenbankumzüge hilft die [PostgreSQL-Backup- und Migrations-Anleitung](https://s-edv.com/anleitungen/postgresql-pg-dump-pg-restore-backup-migration).

## Weiterführende Anleitungen und Quellen

1. [Docker Compose: Multi-Container-Stacks aufbauen](https://s-edv.com/anleitungen/docker-compose-grundlagen-stacks): Compose-Syntax und Stack-Konzepte vertieft erklärt
2. [Traefik als Docker-Reverse-Proxy mit automatischem HTTPS](https://s-edv.com/anleitungen/traefik-docker-reverse-proxy-https-einrichten): Payload hinter TLS betreiben
3. [MySQL & PostgreSQL Backup automatisieren mit cron](https://s-edv.com/anleitungen/mysql-postgresql-backup-automatisieren-cron-cloud): Payload-Datenbank regelmäßig sichern
4. [Docker Compose absichern: Secrets, Healthchecks, Non-Root](https://s-edv.com/anleitungen/docker-compose-absichern-secrets-healthchecks-non-root): Produktionshärtung für den Stack
5. [Directus mit Docker](https://s-edv.com/anleitungen/directus-docker-datenbankapi): Alternative Headless-CMS-Lösung mit fertigem Image

**Offizielle Quellen:** [Payload CMS: Production Deployment](https://payloadcms.com/docs/production/deployment) | [Payload CMS auf GitHub](https://github.com/payloadcms/payload) | [Payload CMS: Migrations](https://payloadcms.com/docs/database/migrations)
