How to Self-Host Payload CMS with Docker Compose
What Is Payload CMS?
Payload is a TypeScript-first, open-source headless CMS that installs directly into a Next.js project. Unlike traditional CMSs that run as standalone applications, Payload is a framework — you define content models in TypeScript, and it generates both a REST API and GraphQL API automatically. It includes a polished React-based admin panel, built-in authentication, access control, and content versioning. The project has 40,000+ GitHub stars and ships weekly releases.
Prerequisites
- A Linux server (Ubuntu 22.04+ recommended)
- Docker and Docker Compose installed (guide)
- 2 GB of RAM minimum (4 GB recommended)
- 10 GB of free disk space
- Node.js 20.9+ (for local development and initial project scaffolding)
- A domain name (optional, for remote access)
Project Setup
Payload requires a Next.js project with a configuration file. Start by creating the project locally:
npx create-payload-app@latest my-project
Choose PostgreSQL as the database adapter when prompted. This generates a project with payload.config.ts, page collections, and a working admin panel.
Docker Compose Configuration
Create a Dockerfile in your project root:
FROM node:20-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable pnpm && pnpm install --frozen-lockfile
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
RUN corepack enable pnpm && pnpm build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
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
CMD ["node", "server.js"]
Create a docker-compose.yml file:
services:
payload:
build: .
container_name: payload
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
ports:
- "3000:3000"
environment:
DATABASE_URI: postgres://payload:changeme-strong-password@postgres:5432/payload # CHANGE password
PAYLOAD_SECRET: changeme-generate-with-openssl-rand-hex-32 # CHANGE THIS
NEXT_PUBLIC_SERVER_URL: http://localhost:3000 # CHANGE to your domain
NODE_ENV: production
volumes:
- payload-media:/app/media
networks:
- payload-net
postgres:
image: postgres:16-alpine
container_name: payload-db
restart: unless-stopped
environment:
POSTGRES_USER: payload
POSTGRES_PASSWORD: changeme-strong-password # Must match DATABASE_URI above
POSTGRES_DB: payload
volumes:
- payload-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U payload"]
interval: 10s
timeout: 5s
retries: 5
networks:
- payload-net
volumes:
payload-db:
payload-media:
networks:
payload-net:
Create a .env file alongside:
# Required — generate with: openssl rand -hex 32
PAYLOAD_SECRET=your-secret-here
# Database connection
DATABASE_URI=postgres://payload:changeme@postgres:5432/payload
# Public URL (used for admin panel links and API responses)
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
Build and start:
docker compose up -d --build
The first build takes 2–4 minutes (compiling TypeScript and Next.js). Subsequent rebuilds are faster with Docker layer caching.
Initial Setup
- Wait for the build to complete:
docker compose logs -f payload - Open
http://your-server-ip:3000/adminin your browser - Create the first admin account (the registration form appears on first visit)
- Start defining collections in
payload.config.tsand rebuild
Configuration
Database Adapters
Payload supports three databases. Configure in payload.config.ts:
| Database | Adapter Package | Best For |
|---|---|---|
| PostgreSQL | @payloadcms/db-postgres | Production self-hosting (recommended) |
| MongoDB | @payloadcms/db-mongodb | Dynamic schemas, nested data |
| SQLite | @payloadcms/db-sqlite | Single-server, file-based storage |
Key Environment Variables
| Variable | Purpose | Required |
|---|---|---|
PAYLOAD_SECRET | Encryption key for auth tokens and sessions | Yes |
DATABASE_URI | Database connection string | Yes |
NEXT_PUBLIC_SERVER_URL | Public URL for API and admin links | Yes |
PORT | HTTP port (default: 3000) | No |
NODE_ENV | Set to production for optimized builds | Recommended |
Content Models
Define collections in payload.config.ts:
import { buildConfig } from 'payload'
import { postgresAdapter } from '@payloadcms/db-postgres'
export default buildConfig({
db: postgresAdapter({
pool: { connectionString: process.env.DATABASE_URI },
}),
collections: [
{
slug: 'posts',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'content', type: 'richText' },
{ name: 'status', type: 'select', options: ['draft', 'published'] },
],
},
],
})
After changing the config, rebuild the Docker image to apply changes.
Reverse Proxy
Place Payload behind a reverse proxy with SSL:
- Forward Port:
3000 - WebSocket Support: Enable (Next.js uses WebSockets for hot module replacement in development)
See our Reverse Proxy Setup guide for Nginx Proxy Manager, Traefik, or Caddy configurations.
Backup
Back up these volumes:
| Volume | Contents |
|---|---|
payload-db | PostgreSQL database (all content, users, settings) |
payload-media | Uploaded files and images |
# Database backup
docker compose exec postgres pg_dump -U payload payload > payload-backup.sql
# Media backup
docker run --rm -v payload-media:/data -v $(pwd):/backup alpine tar czf /backup/payload-media.tar.gz -C /data .
For a comprehensive backup strategy, see our Backup Strategy guide.
Troubleshooting
Build fails with memory errors
Symptom: FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
Fix: The Next.js build process is memory-intensive. Set NODE_OPTIONS=--max-old-space-size=4096 in your Dockerfile’s build stage. Ensure the build host has at least 4 GB of available RAM.
Admin panel shows blank page
Symptom: Navigating to /admin shows a white screen or “Application error.”
Fix: Check that NEXT_PUBLIC_SERVER_URL matches the URL you’re accessing the site from. If behind a reverse proxy, ensure it passes the correct Host header and supports WebSocket connections.
Database connection refused
Symptom: Error: connect ECONNREFUSED at startup.
Fix: Ensure PostgreSQL is healthy before Payload starts. The depends_on with health check in the Compose file should handle this. Verify DATABASE_URI matches the PostgreSQL credentials.
Changes to collections not reflected
Symptom: New fields or collections don’t appear in the admin panel.
Fix: Payload generates database migrations at build time. After changing payload.config.ts, you must rebuild the Docker image: docker compose up -d --build.
Resource Requirements
- RAM: 512 MB idle, 1–2 GB under load (plus PostgreSQL)
- CPU: Low for content management. Medium during builds.
- Disk: 500 MB for application, plus media storage
Verdict
Payload CMS is the best headless CMS option for TypeScript developers who want full control over their content model. Its type-safe collections, dual API (REST + GraphQL), and built-in auth system are genuinely excellent. The admin panel is polished and customizable.
The trade-off is complexity. Payload doesn’t have an official Docker image — you build your own. Every content model change requires a rebuild. If you want a headless CMS you can pull and run, Directus or Strapi are simpler to self-host. If you want a traditional CMS, WordPress or Ghost deploy in minutes.
Choose Payload if your team writes TypeScript and wants a CMS that’s a first-class part of the codebase rather than a separate service.
Related
- Payload vs Directus: Which Headless CMS to Self-Host?
- How to Self-Host WordPress with Docker Compose
- How to Self-Host Ghost with Docker
- How to Self-Host Directus with Docker Compose
- How to Self-Host Strapi with Docker Compose
- Directus vs Strapi: Best Headless CMS
- Wagtail vs WordPress: Which CMS to Self-Host?
- Best Self-Hosted CMS Platforms
- Self-Hosted Alternatives to Squarespace
- Docker Compose Basics
Get self-hosting tips in your inbox
Get the Docker Compose configs, hardware picks, and setup shortcuts we don't put in articles. Weekly. No spam.
Comments