Analytics cookies only (Google Analytics, PostHog). No ad networks, no cross-site tracking. See our privacy policy.
A code-first headless CMS and application framework that installs into a Next.js app. Collections, access control, an admin panel, and REST, GraphQL and Local APIs are all defined in TypeScript config.
Runs on a VPS from $12/mo (Kamatera 4GB, 4 GB RAM) · prices checked Sep 2026
Pick Payload when your team writes TypeScript and wants the CMS to live inside the application: collections, fields, access control and hooks are defined in code, and the admin panel, REST, GraphQL and Local APIs are generated from that config. It installs into a Next.js app rather than running as a separate product, so you scaffold a project with `create-payload-app`, own its repository, and deploy it like any Next.js app. The whole repository is MIT-licensed, and the supported databases are MongoDB, Postgres and SQLite. If you want a ready-made admin over an existing SQL database without writing a project, Directus fits better; Strapi is the other code-configured Node CMS in this catalog.
Paid link — we earn a commission if you shop through it.
Run these commands on your server:
# Payload has no prebuilt image: scaffold the blank template (pinned to 3.90.2) with Postgres, then build it with the template's own Dockerfile
PGPASS=$(openssl rand -hex 16)
docker run --rm -v "$PWD":/work -w /work -u "$(id -u):$(id -g)" -e HOME=/tmp node:22.17.0-alpine npx --yes create-payload-app@3.90.2 payload-app --template blank --db postgres --db-connection-string "postgresql://payload:$PGPASS@postgres:5432/payload" --version 3.90.2 --use-npm --no-git --no-agent
cd payload-app
# the template's Dockerfile needs standalone output, a public/ folder, and the template's .npmrc (it sets legacy-peer-deps, which `npm ci` needs)
sed -i "s|const nextConfig: NextConfig = {|&\n output: 'standalone',|" next.config.ts
sed -i "s|^COPY package.json yarn.lock|COPY .npmrc package.json yarn.lock|" Dockerfile
# uploads are written to ./media, which the unprivileged runtime user cannot create: give it a folder and a volume
sed -i "s|^USER nextjs|RUN mkdir media \&\& chown nextjs:nodejs media\nUSER nextjs|" Dockerfile
mkdir -p public && printf "node_modules\n.next\n" > .dockerignore
cat > docker-compose.yml <<EOF
services:
payload:
build: .
env_file: .env
ports:
- "3000:3000"
volumes:
- media:/app/media
depends_on:
- postgres
postgres:
image: postgres:17-alpine
environment:
POSTGRES_USER: payload
POSTGRES_PASSWORD: $PGPASS
POSTGRES_DB: payload
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
media:
EOF
# Postgres needs a migration in production (schema push is dev-only): create and apply the first one, then build and start
docker compose up -d postgres
docker run --rm --network payload-app_default -v "$PWD":/app -w /app -u "$(id -u):$(id -g)" -e HOME=/tmp node:22.17.0-alpine sh -c "npm run payload migrate:create initial && npm run payload migrate"
docker compose up -d --build
# open http://SERVER_IP:3000/admin and create the first admin userPayload is a framework you build and operate, so most of the risk is in the deployment details. What we found testing it on Ubuntu 26.04: