Dockerising a Next.JS website with Payload CMS

Dockerising a Next.JS website with Payload CMS
Photo by Ian Taylor / Unsplash

Next.JS and Payload CMS can be a powerful combination to build a site driven by a free and opensource CMS. Payload can now run directly inside your Next.JS app (since version 3) making it even easier to work with and deploy.

Setup

First we need to setup Payload so run the following command:

npx create-payload-app

In the tool provide the following options:

  • Project Name: my-app
  • Choose a template: blank
  • Select a database: sqlite
  • SQLite Connection string: file:./my-app-data/data.db

SQLite is selected in this step, but if you have another database already (such as PostgreSQL) or want to use one feel free to change it.

Once the tool has finished we want to update our Media collection to use the my-app-data folder as well, so replace it with the following:

import type { CollectionConfig } from 'payload'

export const Media: CollectionConfig = {
  slug: 'media',
  access: {
    read: () => true,
  },
  fields: [
    {
      name: 'alt',
      type: 'text',
      required: true,
    },
  ],
  upload: {
    staticDir: 'my-app-data/media',
    imageSizes: [
      {
        name: "thumbnail",
        width: 150,
        height: 150,
        position: 'centre',
      }
    ],
    adminThumbnail: 'thumbnail',
  }
}

Media.ts

We will also need to manually create the my-app-data folder in the root of our project, feel free to add it to your .gitignore.

Testing the Template

Lets spin up the site to ensure all if well, start the development mode with pnpm run dev. The site should load without any issues.

Navigate to /admin to create the first admin user. Once created we should be able to access the payload admin page and upload a sample media file. Once uploaded you will see it in the my-app/my-app-data/media directory.

Preparing for Release

In development mode PayloadCMS will apply any changes to the schema to our DB on the fly, however for production instances we need to use migrations. The approach we are going to take is to have the app run the migrations automatically when it starts. First lets add the following scripts to our package.json and also update our start command:

{
  "scripts": {
    // ...
    "start": "payload migrate && next start",
    "payload:generate:types": "payload generate:types",
    "payload:migrate:create": "payload migrate:create"
  }
}

package.json

Before we dockerise the app we need to run our migrations to generate the initial migration: pnpm run payload:migrate:create.

Dockerising the App

Replace the default Dockerfile content with the below:

FROM node:24-alpine AS base
ENV NODE_ENV=production

FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json pnpm-lock.yaml* .npmrc* ./
RUN corepack enable pnpm && pnpm i --frozen-lockfile;

FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN corepack enable pnpm && pnpm build

FROM base AS runner
WORKDIR /app
RUN corepack enable pnpm
RUN mkdir -p /app/my-app-data && chown node:node /app/my-app-data
COPY --chown=node --from=deps /app/node_modules ./node_modules
COPY --chown=node --from=builder /app/public ./public
COPY --chown=node --from=builder /app/next.config.ts ./next.config.ts
COPY --chown=node --from=builder /app/.next ./.next
COPY --chown=node --from=builder /app/package.json ./package.json
COPY --chown=node --from=builder /app/tsconfig.json ./tsconfig.json
COPY --chown=node --from=builder /app/src ./src
USER node
EXPOSE 3000
CMD ["pnpm", "start"]

Dockerfile

Create a .dockerignore with the following contents:

.next
.vscode
my-app-data
node_modules
tests
.env
.env.example
test.env
docker-compose.yml

.dockerignore

This dockerfile does the following:

  • Install the dependencies using PNPM
  • Builds the Next.JS app
  • Creates the data folder in the container (if not passed in from a volume)
  • Starts the app using next start.

Lastly we just need to confirm that our image builds and then runs as expected, to build the image run the following docker command: docker build -t my-app ..

To run the docker app locally to confirm it works we can use the command: docker run -v my-app-data:/app/my-app-data -e DATABASE_URI=file:./my-app-data/data.db -e PAYLOAD_SECRET=123456789 -P my-app.

Now we can use this image to deploy our site to a simple VPS or EC2 instance, or something more complicated if desired. For more complex scenarios using MongoDB or Postgres will probably be a better long term choice, but for simple applications SQLite gets the job done.