Add Docker file to your Payload code
Self hosted PayloadCMS and PostgreSQL website on Docker
1 min read
Published Jun 17 2025, updated Aug 17 2026
Guide Sections
Guide Comments
Now we need to add a docker file to the root of your Payload website code.
First we need to set the standalone flag in the next.config.mjs file at the route of the project:
const nextConfig = { output: 'standalone',}If you have other config already then merge appropriately.
Then you need to add a new script line in the package.json file runs both database migrate and then build, call it ci as thats what we will reference in the docker file when building:
"scripts": { ... "ci": "payload migrate && pnpm build", ... },Next add a Dockerfile to the route of the project:
FROM node:22.12.0-alpine AS base# Install dependencies only when neededFROM base AS deps# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.RUN apk add --no-cache libc6-compatWORKDIR /appENV COREPACK_DEFAULT_TO_LATEST=0# Install dependencies based on the preferred package managerCOPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./RUN \ if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ elif [ -f package-lock.json ]; then npm ci; \ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ else echo "Lockfile not found." && exit 1; \ fi# Rebuild the source code only when neededFROM base AS builderWORKDIR /app# Database and payload secret are required to build the application.# They are passed as build arguments to ensure that the application can connect to the database during the build process.# This is in an early stage so doesn't persist in the final image.ARG DATABASE_URIARG PAYLOAD_SECRETENV DATABASE_URI=$DATABASE_URIENV PAYLOAD_SECRET=$PAYLOAD_SECRETCOPY --from=deps /app/node_modules ./node_modulesCOPY . .ENV COREPACK_DEFAULT_TO_LATEST=0RUN \ if [ -f yarn.lock ]; then yarn run ci; \ elif [ -f package-lock.json ]; then npm run ci; \ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run ci; \ else echo "Lockfile not found." && exit 1; \ fi# Production image, copy all the files and run nextFROM base AS runnerWORKDIR /appENV NODE_ENV productionRUN addgroup --system --gid 1001 nodejsRUN adduser --system --uid 1001 nextjs# Set the correct permission for prerender cacheRUN mkdir .nextRUN chown nextjs:nodejs .next# Automatically leverage output traces to reduce image size# https://nextjs.org/docs/advanced-features/output-file-tracingCOPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/staticCOPY --from=builder --chown=nextjs:nodejs /app/public ./publicUSER nextjsEXPOSE 3000ENV PORT 3000# server.js is created by next build from the standalone output# https://nextjs.org/docs/pages/api-reference/next-config-js/outputCMD HOSTNAME="0.0.0.0" node server.jsThis is a multi-stage build file that will create a small final image size, and will also run the data base migrations as part of the build process.
In order to build the project in the docker file, the payload secret and database connection string env values are both required, so we will pass them in from GitHub actions as build arguments, however they will not be present in the final image and will be set in Docker/Portainer with the other env values when deploying the service.