Dockerising a Next.JS website with Payload CMS
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.