If you want a modern headless CMS stack without handing your content, files, and infrastructure decisions to a SaaS vendor, Payload CMS on Hetzner is a strong setup.
This approach gives you:
- a modern TypeScript/Next.js CMS workflow
- predictable infrastructure costs
- control over where your data lives
- full ownership of your database, media, and deployment pipeline

Payload is open source and can be self-hosted anywhere Next.js runs. Payloadโs own deployment docs explicitly note this, and also remind you that a real production setup needs more than just the app: you also need a database, file storage, email, and a CDN plan.
Why Hetzner is a good fit for a self-hosted headless CMS
Hetzner is attractive for this kind of stack because it combines:
- low monthly pricing
- hourly billing with a monthly cap
- private networking options
- block storage volumes
- optional S3-compatible object storage

Hetznerโs cloud docs also highlight private networking (so your database doesnโt need a public IP), and their pricing pages show hourly billing with a monthly cap.
For teams that care about privacy and data control, Hetzner also states that its Germany and Finland cloud locations run in its own data centers and are operated under strict European data protection regulations. (That doesnโt replace legal review, but itโs a strong infrastructure baseline for EU-focused projects.). Both datacenters are extremely fast from anywhere in Europe. They also have datacenters in North America and Asia but I personally haven’t run anywhere there as my projects are mostly focusing on European based customers.
The setup at a glance
Payload works especially well when embedded into a Next.js app, and Payloadโs docs include production guidance for Docker and Next.js standalone output. They also document the Postgres adapter (@payloadcms/db-postgres) for a clean PostgreSQL-backed setup.
Architecture drawing with GitHub Actions

GitHub Actions is a good fit here because workflows are YAML-based, can be triggered by repository events (including pushes), and can run build/test/deploy jobs in sequence.
How to set up Payload CMS on Hetzner
1) Start with one VPS and Docker
For a cost-effective start, run this on a single Hetzner cloud server:
- Next.js + Payload (same container/app)
- PostgreSQL (separate container)
- Nginx reverse proxy
- Docker volumes for persistent data
This is simple to operate and easy to split later (for example, moving PostgreSQL to a separate server).
2) Use PostgreSQL from day one
Payload supports PostgreSQL via @payloadcms/db-postgres (using Drizzle + node-postgres), which is a strong fit for a production CMS setup where you want full control of your schema and migrations.
This is one of the main advantages of self-hosting: your content database is your database.
3) Handle uploads correctly (this is important)
Payloadโs production docs make a key point: if you use Payload uploads, you must think about permanent file storage.
They explicitly warn that some hosts use ephemeral filesystems, where uploaded files disappear on restart. Their recommendation is to either use a persistent filesystem or integrate with external object storage (like S3-compatible storage).
On Hetzner, you have two good options:
Option A โ Hetzner Volume (simple)
Mount a persistent block storage volume into your app container and store uploads there.
- Cheap
- Simple
- Great for single-server deployments
Option B โ Hetzner Object Storage (better for scaling)
Hetzner now offers S3-compatible Object Storage, which starts at โฌ4.99/month and includes 1 TB storage + 1 TB egress in the base plan. Their FAQ also notes that ingress and S3 API calls are free.
This is the better option if you later run multiple app containers or want cleaner separation between app and media.
4) Build Payload for Docker production
Payloadโs docs include a production Docker example and recommend using Next.js output: 'standalone', which is ideal for Docker deployments. They also call out the important environment variables (PAYLOAD_SECRET, PAYLOAD_CONFIG_PATH, DATABASE_URL).
That gives you a portable build artifact you can deploy consistently across staging and production.
5) Put a reverse proxy in front
Use Nginx in front of the app to handle:
- TLS termination
- path routing
- headers
- request limits
- optional WAF integration
You can also place Cloudflare in front for DNS, CDN, and edge protection. Hetzner also notes built-in DDoS protection for cloud servers, which is a nice extra layer.
6) Keep the database private
Hetzner supports private networks, and their docs explicitly note you can even create a private-network-only server without a Primary IP. This is exactly what you want for PostgreSQL.
A good rule:
- App server: public (via reverse proxy)
- Database server: private only
Hetzner hardware costs for a modern CMS setup
Hetzner pricing is one of the biggest reasons this setup is attractive.
Relevant pricing details from Hetzner:
- hourly billing with monthly cap for cloud servers
- backups cost 20% of instance price (up to 7 backup slots)
- snapshots billed per GB/month
- block storage volumes are โฌ0.044/GB/month
- load balancer starts at โฌ5.39/month
- object storage starts at โฌ4.99/month
- primary IPs are billed separately (IPv6 is free)
Hetzner also states EU cloud locations include at least 20 TB traffic, with lower included traffic in US/Singapore regions.
Suggested server sizes (Payload + Next.js context)
From Hetznerโs published plan table (prices vary by region/network option), the common CPX sizes are roughly:
- CPX21 โ 3 vCPU / 4 GB RAM / 80 GB SSD: about โฌ8.99โโฌ10.59/mo
- CPX31 โ 4 vCPU / 8 GB RAM / 160 GB SSD: about โฌ15.99โโฌ18.59/mo
- CPX41 โ 8 vCPU / 16 GB RAM / 240 GB SSD: about โฌ29.99โโฌ34.09/mo
Cost scenarios
1) Starter setup (single server, local uploads on volume)
Good for a solid website, company site, or first production rollout.
- CPX21: โฌ8.99โโฌ10.59
- 40 GB volume: 40 ร โฌ0.044 = โฌ1.76
- Backups (20%): โฌ1.80โโฌ2.12
Estimated monthly total: โฌ12.55โโฌ14.47 (plus any paid Primary IPv4 and VAT)
2) Better production baseline (single server, more headroom)
Good for a busier site or heavier admin/editor usage.
- CPX31: โฌ15.99โโฌ18.59
- 80 GB volume: 80 ร โฌ0.044 = โฌ3.52
- Backups (20%): โฌ3.20โโฌ3.72
Estimated monthly total: โฌ22.71โโฌ25.83 (plus any paid Primary IPv4 and VAT)
3) Privacy-first split setup (app + db separated)
A stronger production design with a private DB server.
- App server (CPX31): โฌ15.99โโฌ18.59
- DB server (CPX21): โฌ8.99โโฌ10.59
- Volumes: 40 GB app + 100 GB db = 140 GB โ โฌ6.16
- Backups (20% on both servers): โฌ5.00โโฌ5.84
Estimated monthly total: โฌ36.14โโฌ41.18 (plus any paid Primary IPv4, optional LB, and VAT)
If you add:
- Hetzner Load Balancer: from โฌ5.39/mo
- Hetzner Object Storage: from โฌ4.99/mo
โฆyou still end up with a very cost-efficient production stack compared to many managed setups.
Pros and cons vs Vercel, SST, and Cloudflare
Payloadโs docs explicitly note you can deploy Payload anywhere Next.js runs, including Vercel and SST, which is a nice reminder that this is about tradeoffs, not โone right answer.โ
Hetzner (self-hosted) โ best for ownership and predictable cost
Pros
- You own the infrastructure, DB, and storage choices but still everything is automated
- Easy to keep database private
- Predictable monthly cost
- Great fit for teams that want privacy/data control
Cons
- You handle patching, monitoring, backups, and incidents
- More ops responsibility than a managed platform
- Slower to set up than โpush to deployโ hosting
Vercel โ best for front-end DX
Pros
- Excellent Next.js developer experience
- Fast preview deployments
- Great for frontend teams moving quickly
Cons
- Youโll still need a DB/storage strategy outside the app runtime
- Less infrastructure control
- Cost can become more usage-driven as the project grows
SST โ best for infra-as-code teams
Pros
- Strong infrastructure-as-code workflow
- Good fit if you already want cloud-native automation patterns
- Scales well organizationally for platform-minded teams
Cons
- More moving parts
- More cloud architecture decisions up front
- Overkill for many small/medium CMS deployments
Cloudflare โ best at the edge layer
Pros
- Excellent DNS/CDN/WAF/performance edge
- Great in front of a Hetzner-hosted app
- Strong security/performance layer
Cons
- As a primary runtime, it can require more adaptation for traditional stateful app patterns
- You still need a clear database and file storage architecture
Final takeaway
If your goal is:
- modern headless CMS
- strong privacy and data ownership
- low and predictable cost
- no vendor lock-in
โฆthen Payload CMS on Hetzner is a very practical setup.
It gives you a modern developer experience, keeps your content and media under your control, and stays affordable even when you add backups, volumes, and a proper deployment pipeline.
Sample Docker Compose for a Payload CMS + Next.js + PostgreSQL + Nginx setup on Hetzner.
docker-compose.yml
services:
nginx:
image: nginx:1.27-alpine
container_name: payload_nginx
restart: unless-stopped
depends_on:
- app
ports:
- "80:80"
# - "443:443" # enable if you terminate TLS in Nginx
volumes:
- ./infra/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
# - ./infra/nginx/certs:/etc/nginx/certs:ro # optional if using local TLS certs
networks:
- web
- internal
app:
build:
context: .
dockerfile: Dockerfile
container_name: payload_app
restart: unless-stopped
env_file:
- .env
environment:
NODE_ENV: production
PORT: 3000
# Payload / Next.js
PAYLOAD_SECRET: ${PAYLOAD_SECRET}
PAYLOAD_CONFIG_PATH: ${PAYLOAD_CONFIG_PATH:-dist/payload.config.js}
NEXT_PUBLIC_SERVER_URL: ${NEXT_PUBLIC_SERVER_URL}
# DB
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
# Optional: local uploads path used by Payload collection upload config
UPLOAD_DIR: /app/uploads
depends_on:
db:
condition: service_healthy
expose:
- "3000"
volumes:
- payload_uploads:/app/uploads
networks:
- internal
- web
db:
image: postgres:16-alpine
container_name: payload_db
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
TZ: Europe/Copenhagen
volumes:
- pg_data:/var/lib/postgresql/data
expose:
- "5432" # internal only
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 10
networks:
- internal
volumes:
pg_data:
payload_uploads:
networks:
web:
driver: bridge
internal:
driver: bridge
internal: true
It keeps PostgreSQL private (no public port), mounts persistent storage for uploads, and puts Nginx in front.
infra/nginx/default.conf
server {
listen 80;
server_name _;
client_max_body_size 25M;
# If behind Cloudflare, pass real IP (optional, add CF IP ranges if needed)
# real_ip_header CF-Connecting-IP;
location / {
proxy_pass http://app:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support (useful if you later add realtime features)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
.env.example
# App
NEXT_PUBLIC_SERVER_URL=https://your-domain.com
PAYLOAD_SECRET=change-this-to-a-long-random-secret
PAYLOAD_CONFIG_PATH=dist/payload.config.js
# Postgres
POSTGRES_DB=payload
POSTGRES_USER=payload_user
POSTGRES_PASSWORD=change-this-password
Notes for your Payload setup
- In Payload upload collections, point storage to a path like
/app/uploads(or useUPLOAD_DIR). - If you switch to Hetzner Object Storage (S3-compatible) later, you can remove
payload_uploadsand use an S3 adapter instead. - Keep the
dbservice unpublished (noports:) so it stays private.

