# Deploy Payload CMS: 3 Paths & Real Bills

*Originally published at* [*techpotions.com*](https://techpotions.com/lab/how-to-deploy-payload-cms-3-paths-bills)*.*

Knowing how to deploy Payload CMS is the last mile that turns your headless CMS into a live application, and the hosting shape you pick directly dictates your architecture, monthly bill, and operational chores. Because Payload rides on Next.js, you can drop it anywhere Next.js runs (Vercel, a Docker VPS, or even SST on AWS). But that choice forces decisions about the database, file storage, preview URLs, and your migration queue. This guide walks three concrete shapes, what they cost at low traffic, and the config that makes each one hum.

If you want to skip the ops entirely, we handle Payload deployments for startups and studios, [learn more about our web services](https://techpotions.com/services/web).

## How to deploy Payload CMS: three deployment shapes

Each path below is a real, production-grade pattern. We'll detail the setup, the actual bills you'll pay for a site that serves a few thousand visitors a month, and where the sharp edges hide.

## Path 1: Vercel + managed Postgres (serverless front-to-back)

**Takeaway:** the fastest path to production, but your bill is tied to database and media storage, not compute.

Vercel's Hobby plan hosts the Next.js app for free. You pair it with a managed Postgres, Neon, Supabase, or Vercel Postgres, and offload file uploads to an S3-compatible service (Vercel Blob, Cloudflare R2, or AWS S3). The serverless functions that serve Payload's REST/GraphQL API stay well under the Hobby execution limits for low traffic.

### Steps

**1\. Clone the template.** Start from the [official Payload + Vercel starter](https://vercel.com/templates/next.js/payload-website-starter). It wires up the Postgres adapter and includes example collections.

**2\. Provision the database.** Create a project in Neon (free tier: 0.5 GB, 1-branch). Paste the connection string into `DATABASE_URI` in your Vercel environment.

**3\. Plug in the cloud storage adapter.** Payload needs a place to persist uploads. Install the official S3 adapter:

```bash
npm install @payloadcms/plugin-cloud-storage @payloadcms/plugin-cloud-storage-s3
```

In `payload.config.ts`, activate it and point it at your bucket:

```typescript
import { cloudStorage } from '@payloadcms/plugin-cloud-storage'
import { s3Adapter } from '@payloadcms/plugin-cloud-storage-s3'

export default buildConfig({
  plugins: [
    cloudStorage({
      collections: {
        media: {
          adapter: s3Adapter({
            config: {
              endpoint: process.env.S3_ENDPOINT,
              credentials: {
                accessKeyId: process.env.S3_ACCESS_KEY!,
                secretAccessKey: process.env.S3_SECRET!,
              },
              region: 'auto',
            },
            bucket: process.env.S3_BUCKET!,
          }),
        },
      },
    }),
  ],
})
```

(We've detailed this plugin and others we always install in our [Payload plugins breakdown](https://techpotions.com/lab/payload-cms-plugins-we-actually-install).)

**4\. Set the preview URL.** In your Payload config, point `serverURL` to your Vercel deployment domain. The Live Preview will work as long as the Next.js front-end is served from the same origin.

### Bills at low traffic

| Resource | Provider | Cost |
| --- | --- | --- |
| Next.js hosting | Vercel Hobby | Free |
| Database | Neon Free Tier | Free (0.5 GB, 1 branch) |
| Object storage | Cloudflare R2 | Free (10 GB) |
| Total |  | **$0** |

When you outgrow the free tiers, bumping Neon to 1 GB ($19/mo) and R2 to $0.015/GB (or moving to Vercel Blob at $0.30/GB stored) keeps the bill under $25/mo. The serverless database cold-starts are imperceptible for content teams, and you never touch a server.

## Path 2: Docker on a VPS with local disk (self-contained everything)

**Takeaway:** a single-instance VPS gives you a predictable $6-12/mo bill and full control, you trade away zero-downtime scaling for absolute simplicity.

This is our go-to when a client wants a fixed, predictable cost and doesn't need global replication. You run Payload inside a Docker container on a small VPS (DigitalOcean Droplet, Hetzner CX22, etc.), with a companion Postgres container and a mounted volume for uploads.

### Steps

**1\. Write the Dockerfile.** A lightweight multi-stage build that outputs the Next.js standalone server:

```dockerfile
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=base /app/.next/standalone ./
COPY --from=base /app/.next/static ./.next/static
COPY --from=base /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
```

**2\. Docker Compose with Postgres and a volume.**

```yaml
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URI=postgresql://payload:secret@db:5432/payload
      - PAYLOAD_SECRET=your-secret
      - UPLOAD_DIR=/media
    volumes:
      - media:/media
    depends_on:
      - db
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: payload
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: payload
    volumes:
      - pgdata:/var/lib/postgresql/data
volumes:
  media:
  pgdata:
```

**3\. Configure Payload for local disk.** No cloud-storage plugin required, Payload's default upload collection writes to the `UPLOAD_DIR`. In `payload.config.ts`:

```typescript
upload: {
  staticDir: process.env.UPLOAD_DIR || 'media',
},
```

**4\. Automated backups.** A cron job on the host zips the pgdata and media volumes nightly and syncs them to an S3-compatible bucket using rclone. A simple script:

```bash
#!/bin/bash
TIMESTAMP=$(date +%F)
docker run --rm -v payload_pgdata:/pgdata -v payload_media:/media alpine \
  tar czf /backup/payload-backup-$TIMESTAMP.tar.gz /pgdata /media
rclone copy /backup/payload-backup-$TIMESTAMP.tar.gz s3:mybackupbucket/
```

**5\. Preview.** The `serverURL` points to `http://your-domain.com`. Since the VPS serves both the admin panel and the front-end from the same origin, live preview works out of the box.

### Bills at low traffic

| Resource | Provider | Cost |
| --- | --- | --- |
| VPS (1 vCPU, 2 GB) | DigitalOcean Droplet | $6/mo |
| Object storage | DigitalOcean Spaces | $5/mo (250 GB) |
| Total |  | **$11/mo** |

The 2 GB RAM handles Payload's admin with a few concurrent editors; if you start running webhooks or migrations queues heavily, bump to 4 GB ($12/mo). Everything, database, app, and media, lives on one machine, so you avoid network latency between services.

## Path 3: managed Postgres + S3-compatible storage (decoupled database, anywhere hosting)

**Takeaway:** you get a highly-available database and durable media storage that survive any app-layer host change; you can front it with Vercel Hobby, a cheap VPS, or even Cloudflare Workers.

This design separates concerns. The database (Neon, Supabase, Aiven, etc.) stays up even if you tear down the app server. Media lands in an S3-compatible bucket (R2, Spaces, Backblaze B2). The app container can run on any platform, Vercel Hobby, [Fly.io](http://Fly.io), Railway, or a $4-per-month VPS.

### Steps

**1\. Provision the database.** Grab a connection string from Neon or Supabase. Set `DATABASE_URI`.

**2\. Set up S3 and the adapter.** Install the same S3 plugin as in Path 1. Use your bucket provider's endpoint.

**3\. Host the app.** You can deploy via Vercel (free) using a standard `@payloadcms/next` build, or push a Docker image to [Fly.io](http://Fly.io) (free for 256 MB RAM, which is enough for small Payload apps). A minimal `fly.toml`:

```toml
app = "my-payload-app"
kill_signal = "SIGINT"
kill_timeout = 5

[build]
image = "node:20-alpine"
command = "npm run build"

[[services]]
internal_port = 3000
```

**4\. Migrations queue.** Because the database is external, you can run your Payload migrations locally and then push them to the managed DB without worrying about the app container. The `payload migrate` CLI works directly against the remote `DATABASE_URI`.

### Bills at low traffic (Vercel Hobby + Neon + R2)

| Resource | Provider | Cost |
| --- | --- | --- |
| App hosting | Vercel Hobby | Free |
| Database | Neon Free Tier | Free |
| Object storage | Cloudflare R2 | Free (10 GB) |
| Total |  | **$0** |

Even if you later bump the database to a 1-GB plan ($19/mo) and add a backup region ($10/mo), you're still under $30/mo with zero server administration. Because the app tier is disposable, you can switch hosting providers without migrating any data, a benefit we've leaned on when a client's traffic pattern changed.

## FAQ

### Which deployment method is cheapest for Payload CMS?

Usually a VPS running Docker with a local Postgres container and local disk for media costs a flat $6-12/month with no egress fees. Managed Postgres + S3 starts around $5-9/month (DB free tier + object storage) if fronted by Vercel Hobby. The VPS route is cheapest at rest but requires managing backups yourself.

### Can I use the Vercel free tier for Payload CMS production?

Yes, and many small sites do. Vercel Hobby gives 1-second timeout on Serverless Functions, which is enough for Payload's API. You'll still need an external Postgres (Neon free tier works) and, ideally, an S3-compatible bucket for uploads so media survives re-deploys.

### How do you handle media uploads in Payload without cloud storage?

Bind the container's `/media` folder to a Docker volume. Pair that with a nightly cron that zips the volume and syncs it to a cheap object store (or your workstation) using rclone. For redundancy, you can mount the same volume to an offsite server, but a periodic pull backup is usually adequate for low-traffic sites.
