03/19/2025

Development

PayloadCMS

Deploy Payload CMS with Next.js: Complete Guide from Local Development to Production (VPS, Docker & HTTPS)

Discover how to set up a modern CMS with Payload and Next.js, from local installation to secure deployment on a VPS with Docker, Nginx, and an SSL certificate. Follow our step-by-step tutorial to master deployment, content management, and automated version control with Git.

Table of Contents

  • Run Payload locally
  • Create your first content type in Payload
  • Initialize the Git repository for version control
  • Configure Docker
  • Configure the VPS
  • Configure DNS
  • Create an SSL certificate to enable HTTPS

Run Payload locally

1. Prerequisites: Install the basic tools

  • Install Homebrew (the package manager for macOS): https://brew.sh/
  • Install Node.js and npm via Homebrew:
1 brew install node
  • Check the installed versions:
1 2 node -v npm -v

2. Install & launch MongoDB via Brew

  • Install MongoDB Community Edition:
(Example: MongoDB 8.0 — make sure to adapt the command according to the desired version)
1 2 brew tap mongodb/brew brew install [email protected]
  • Start MongoDB with Brew Services:
1 brew services start [email protected]
  • Check that MongoDB is running:
1 brew services list
MongoDB will be available at: mongodb://localhost:27017 (by default).

Official documentation:

Install MongoDB on macOS

3. Create the Next.js project

In your working directory, run:

1 npx create-payload-app

Answer the questions:

  • Project name
  • Database choice (MongoDB)
  • Template choice (for example, blank if you want a minimal project)

4. Install a code editor

You can use Visual Studio Code, for example:

Download VSCode

5. Run the project locally

  • Open the project folder with VSCode.
  • Open the terminal (shortcut: Cmd + J).
  • Install dependencies:Installe les dépendances :
1 npm i
  • Start the project:
1 npm run dev
  • In your browser, go to: : http://localhost:3000

6. First steps in the Payload interface

  • Click on: “Go to Admin Panel
  • Create your admin account.

Create your first content type in Payload

1. Create the Todo collection in the backend

Goal: Create a “Todo” content type (a simple task list) that can be displayed on your website and managed directly from Payload.
  • In VSCode, locate the folder: src/collections.
  • Create a file: Todo.ts :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import type { CollectionConfig } from 'payload' export const Todo: CollectionConfig = { slug: 'todo', admin: { useAsTitle: 'Task', }, fields: [ { name: 'Task', type: 'text', required: true, }, ], }
  • In the payload.config.ts file (inside src), add the Todo collection Todo :
1 2 3 4 5 //before: collections: [Users, Media], //after: collections: [Users, Media, Todo],
  • Remember to import Todo at the top of the file payload.config.ts :
1 import { Todo } from './collections/Todo'
Once the project is restarted, you will see your new Todo collection appear in the admin panel.

2. Display the Todo list on the frontend

  • Open the main website page : src/app/(frontend)/page.tsx and modify its content to retrieve and display tasks:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import { getPayload } from 'payload' import React from 'react' import config from '@/payload.config' import './styles.css' export const dynamic = 'force-dynamic' export default async function HomePage() { const payload = await getPayload({ config }); const Todos = await payload.find({ collection: 'todo', }) return ( <div> { Todos.docs?.map((todo) => ( <div key={todo.id}> <h2>{todo.task}</h2> </div> )) } </div> ) }
Now every task created in Payload will appear on the homepage.

Initialize the Git repository for version control

1. Create a GitHub account

Create a GitHub account

2. Create an SSH key

1 ssh-keygen -t rsa -b 4096 -C "[email protected]"
  • When prompted to specify a file path for the key, press Enter Enter to accept the default location (~/.ssh/id_rsa).
  • Then choose a secure password for the SSH key or press Enter to skip it (optional but recommended).

3. Start the SSH agent

Run the SSH agent to manage your key:

1 eval "$(ssh-agent -s)"

4. Add the SSH key to the agent

Add the private key to the agent so it can be used automatically:

1 ssh-add ~/.ssh/id_rsa

5. Copy the public SSH key

1 cat ~/.ssh/id_rsa.pub

Select and copy the displayed content.

6. Add the SSH key to GitHub (or GitLab, etc.)

  • In GitHub: : Settings > SSH and GPG keys > New SSH key.
  • Paste your public key, give it a clear name (for example: “VPS” or “MacBook”), then confirm.

7. Test the connection

A welcome message should appear.

8. Create a repository

  • Create a repository : https://github.com/new
    • Choose the repository name, visibility (private or public), etc.
    • Once created, follow the instructions to link your project:
1 2 3 git remote add origin [email protected]:beease/payload-test.git git branch -M main git push -u origin main
  • Commit your changes:
1 2 3 git add . git commit -am "todo" git push
Congratulations, your changes are now online. Other developers with access to the project can update their version with: git pull

Docker Configuration

1. Configure Docker Compose

  • At the root of the project, you will find the docker-compose.yml file. It is used to manage Docker containers. The default Payload template already includes a configuration. We will replace it with the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 services: payload: build: context: . volumes: - ./media:/app/media ports: - '3000:3000' depends_on: - mongo mongo: container_name: mongo image: mongo:latest restart: always environment: MONGO_INITDB_ROOT_USERNAME: username MONGO_INITDB_ROOT_PASSWORD: password ports: - "27017:27017" volumes: - mongodb_data:/data/db nginx: container_name: webserver restart: always image: nginx:latest ports: - '80:80' volumes: - ./nginx.conf:/etc/nginx/conf.d/nginx.conf:ro depends_on: - payload volumes: mongodb_data:

2. Docker image configuration

  • Replace the content of the Dockerfile file located at the root of the project:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 FROM node:22-alpine AS builder WORKDIR /app COPY . . RUN npm install RUN npm run build:compile FROM node:22-alpine AS runner WORKDIR /app COPY . . COPY --from=builder /app/package-lock.json ./ COPY --from=builder /app/.next ./.next COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static RUN npm ci --omit=dev EXPOSE 3000 ENV PORT 3000 CMD ["sh", "-c", "npm run payload migrate && npm run build:generate && node server.js"]
(Here, I am using Node 22 as an example. Adapt it if necessary.)

3. Configure the Next.js project for production

  • In the next.config.mjs file, enable standalone mode:
1 2 3 4 5 6 7 8 import { withPayload } from '@payloadcms/next/withPayload' /** @type {import('next').NextConfig} */ const nextConfig = { output: "standalone", } export default withPayload(nextConfig, { devBundleServerPackages: false })
  • In the package.json file at the root of the project, add these script lines:
1 2 3 4 5 6 "scripts": { ... "build:compile": "next build --experimental-build-mode compile", "build:generate": "next build --experimental-build-mode generate", ... },

4. Web server configuration

  • Still at the root of the project, create an nginx.conf file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 upstream payload { server payload:3000; } server { listen 80; listen [::]:80; server_name [domain] www.[domain]; server_tokens off; location / { proxy_pass http://payload; 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-Host $host; proxy_set_header X-Forwarded-Proto $scheme; } }
  • Once everything is configured, commit and push your changes to your repository.
1 2 3 git add . git commit -am "setup docker & nginx" git push

Configure the VPS

On the VPS, Docker will install everything you need inside the containers. You only need to install Git and Docker on the host machine.

1. Install Git

1 2 3 sudo apt update sudo apt install git -y git --version

2. Install Docker

Follow the official Docker documentation:

Install Docker Engine

1 2 3 4 5 6 7 8 9 10 11 12 13 # Add Docker's official GPG key: sudo apt-get update sudo apt-get install ca-certificates curl sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc # Add the repository to Apt sources: echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update
  • Install the Docker packages:
1 sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
  • Test the installation:
1 sudo docker run hello-world

3. Create a Git SSH key on your VPSur ton VPS

Follow the same process as on your local machine (see section C), so you can clone your repository via SSH.

4. Clone the Git project

  • On GitHub, retrieve the SSH clone URL:
github-payload-test


  • On your VPS, navigate to the folder where you want to clone the project:
1 git clone [email protected]:MonCompte/mon-projet.git

5. Create the .env file

1 2 cd mon-projet nano .env
  • Write the environment variables:
1 2 DATABASE_URI=mongodb://username:password@mongo:27017/payload?authSource=admin PAYLOAD_SECRET=[Clef secrète payload]

(In this example, mongo corresponds to the Mongo container name defined in docker-compose.yml.)

To save and exit Nano : Ctrl + O then Ctrl + X (or Cmd + S, Cmd + X on Mac if you are connected through iTerm, etc.).

6. Start the Docker containers

  • Create the Docker images:
1 sudo docker compose build
  • Start the Docker containers:
1 sudo docker compose up
Your website is almost online! The only remaining step is configuring DNS to link your domain name to the VPS public IP address.

Configure DNS

In your domain name DNS settings, add an A record pointing to your VPS public IP address.

For example:

  • Type : A
  • Value: 123.456.789.000 (your VPS IP address)
You can then access your admin interface at: http://your-domain.com/admin

Create an SSL certificate to enable HTTPS

1. Basic Nginx configuration for HTTP

  • In your docker-compose.yml file, add (or update) the configuration for Nginx and Certbot:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 services: nginx: container_name: webserver restart: always image: nginx:latest ports: - '80:80' - '443:443' volumes: - ./nginx.conf:/etc/nginx/conf.d/nginx.conf:ro - ./certbot/www:/var/www/certbot/:ro - ./certbot/conf/:/etc/nginx/ssl/:ro depends_on: - payload entrypoint: "/bin/sh -c 'while :; do sleep 6h; nginx -s reload; done & nginx -g \"daemon off;\"'" certbot: restart: always image: certbot/certbot:latest volumes: - ./certbot/www/:/var/www/certbot/:rw - ./certbot/conf/:/etc/letsencrypt/:rw #entrypoint: "/bin/sh -c 'while :; do certbot renew; sleep 12h; done'"
  • Configure nginx.conf (initial version for the HTTP phase):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 server { listen 80; listen [::]:80; server_name [domain].fr www.[domain].fr; server_tokens off; location /.well-known/acme-challenge/ { root /var/www/certbot; } location / { return 301 https://[domain]$request_uri; } }
  • Commit and push your changes.
  • On your VPS, run git pull and restart Docker Compose:
1 2 git pull sudo docker compose down && sudo docker compose up

2. Generate the certificate with Certbot

  • First, test in dry-run mode: (Replace the domain name with yours)
1 2 3 sudo docker compose run --rm certbot certonly \ --webroot --webroot-path /var/www/certbot/ \ --dry-run -d mon-domaine.fr

The message : The dry run was successful should appear.

  • If everything works correctly, create the certificates:
1 2 3 sudo docker compose run --rm certbot certonly \ --webroot --webroot-path /var/www/certbot/ \ -d mon-domaine.fr

3. Update the Nginx configuration for HTTPS

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 upstream payload { server payload:3000; } # Redirection automatique HTTP -> HTTPS server { listen 80; listen [::]:80; server_name [domain].fr www.[domain].fr; server_tokens off; location /.well-known/acme-challenge/ { root /var/www/certbot; } location / { return 301 https://[domain]$request_uri; } } # Serveur HTTPS server { listen 443 default_server ssl http2; listen [::]:443 ssl http2; server_name [domain].fr www.[domain].fr; ssl_certificate /etc/nginx/ssl/live/[domain.fr]/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/live/[domain.fr]/privkey.pem; location / { proxy_pass http://payload; 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-Host $host; proxy_set_header X-Forwarded-Proto $scheme; } }
  • In docker-compose.yml, uncomment this line to enable automatic certificate renewal:
1 2 3 4 5 6 7 8 certbot: restart: always image: certbot/certbot:latest volumes: - ./certbot/www/:/var/www/certbot/:rw - ./certbot/conf/:/etc/letsencrypt/:rw ==> entrypoint: "/bin/sh -c 'while :; do certbot renew; sleep 12h; done'"
  • Commit and push your changes.
  • On your VPS, run git pull and restart Docker Compose:
1 2 git pull sudo docker compose down && sudo docker compose up
Congratulations: your Payload application is now accessible via HTTPS at : https://[your-domain]/admin

Conclusion

You now have a complete environment to:

  • Develop locally with Payload, MongoDB, and Next.js.
  • Version your changes using Git and GitHub.
  • Deploy to a VPS with Docker + Nginx + Certbot to obtain a secure HTTPS website.

Good luck! 🐝