How to Run Payload CMS in Docker



This content originally appeared on DEV Community and was authored by Lukas Mauser

Payload is an open source backend framework and it is mainly used as a content management system.

You can use Docker to run your own instance of Payload on Sliplane, however, when I tried using the Dockerfile that gets created using the pnpx create-payload-app it did not work for me right away and I had to apply a few tweaks and settings in order to get payload running.

Here’s how you can run Payload with Docker:

Prerequisites

NodeJs and Docker should be installed on your system. For the demo I use pnpm as a package manager so make sure to install it as well or tweak the installation instructions to use your package manager of choice.

In my case I used:

  • Node version: v22.12.0
  • pnpm version: 9.13.2

Create a new Payload App

Open a new terminal in the parent folder where your Payload project should be created. Run the install wizard with:

# npx
npx create-payload-app

# or pnpx
pnpx create-payload-app

You will be guided through a series of short questions, I’ll use:

  • “Payload Demo” as my project name
  • setup a blank project
  • go with MongoDB as my database and use the default connection setting to begin with
  • and pnpm as the package manager

To install payload into an existing project, please follow the installation instructions on the official payload documentation.

After the wizard has finished, we can see that the project includes a Dockerfile out of the box. However, when I tried to use this Dockerfile, I ran into several issues that prevented it from working properly in my environment.

Issues I Encountered

When trying to run the generated Dockerfile, I encountered a few problems:

  1. Unpinned pnpm version – The Dockerfile didn’t specify a specific pnpm version, which caused my Docker builds to fail
  2. Missing standalone mode – My builds also failed because Next.js wasn’t configured for standalone output
  3. Public folder issues – The Dockerfile tried to copy a public folder that didn’t exist in my setup in the beginning
  4. Database connection issues – I missed the authSource connection parameter in the MongoDB connection URI
  5. File upload permissions – Media uploads failed due to incorrect folder permissions

Let me show you how I fixed these issues one by one.

My Fixed Dockerfile

Here’s the corrected Dockerfile that resolved the issues I encountered:

# To use this Dockerfile, you have to set `output: 'standalone'` in your next.config.mjs file.
# From https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile

FROM node:22.12.0-alpine AS base

# Install dependencies only when needed
FROM 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-compat
WORKDIR /app

# Install dependencies based on the preferred package manager
COPY 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 && corepack prepare pnpm@9.13.2 --activate && pnpm i --frozen-lockfile; \
  else echo "Lockfile not found." && exit 1; \
  fi


# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED 1

RUN \
  if [ -f yarn.lock ]; then yarn run build; \
  elif [ -f package-lock.json ]; then npm run build; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && corepack prepare pnpm@9.13.2 --activate && pnpm run build; \
  else echo "Lockfile not found." && exit 1; \
  fi

# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Remove this line if you do not have this folder
COPY --from=builder /app/public ./public

# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next



# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

RUN mkdir -p media && chown -R nextjs:nodejs media

USER nextjs


EXPOSE 3000

ENV PORT 3000

# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD HOSTNAME="0.0.0.0" node server.js

Building and Running

Here’s how you can build and run your Payload CMS application:

# Build the Docker image
docker build -t payload-cms .

# Run the container 
docker run -p 3000:3000 \
  -e DATABASE_URI=mongodb://your-mongo-host:27017/payload?authSource=admin \
  -e PAYLOAD_SECRET=your-secret-key \
  payload-cms

Note: If you want to persist uploaded files, you can mount a volume to /app/media, however in a more production-ready setup, you might want to use an object storage solution

Pinning the pnpm Version

To fix the pnpm version issue, I pinned the specific version in the Dockerfile in the deps and builder stages:

RUN corepack enable pnpm && corepack prepare pnpm@9.13.2 --activate

This ensures the same pnpm version is used consistently across all environments.

Configuring Next.js for Standalone Output

You’ll also need to update your next.config.mjs file to enable standalone output mode:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
  // ... other configuration options
}

export default nextConfig

This tells Next.js to create a standalone version of your application that includes all the necessary dependencies.

Setting up the Public Folder

If your project doesn’t have a public folder yet, create one:

mkdir public
echo "" > public/.gitkeep

This helps ensure the COPY command in the Dockerfile doesn’t fail.

Database Configuration

I used MongoDB in a container, and had to include the authSource=admin parameter in the connection URI in order to connect:

DATABASE_URI=mongodb://username:password@localhost:27017/payload?authSource=admin

File Upload Permissions

I attached the media folder to the Docker container using a volume mount. This allows you to persist uploaded files outside the container.

For uploads to work correctly, I had to ensure the media folder had the right permissions, which I set in the Dockerfile with:

RUN mkdir -p media && chown -R nextjs:nodejs media

This creates the media directory and sets the proper ownership so the nextjs user can write uploaded files to it.

Object Storage for Media Files

At some point, you might want to configure object storage for media files instead of storing them in a volume:

// payload.config.ts
import { s3Adapter } from '@payloadcms/plugin-cloud-storage/s3'

export default buildConfig({
  plugins: [
    cloudStorage({
      collections: {
        media: {
          adapter: s3Adapter({
            config: {
              endpoint: process.env.S3_ENDPOINT,
              region: process.env.S3_REGION,
              credentials: {
                accessKeyId: process.env.S3_ACCESS_KEY_ID,
                secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
              },
            },
            bucket: process.env.S3_BUCKET,
          }),
        },
      },
    }),
  ],
  // ... rest of your config
})

Deploy to Sliplane

You can deploy your Payload CMS instance to Sliplane by following a few sim

  1. Create a new project and give it a name of your choice
  2. Next we will deploy a MongoDB database
  3. Navigate into the project and click “Deploy Service” choose a server and select the MongoDB preset. You can make the service “private” since we don’t want to expose it to the public internet
  4. Deploy the database, alternatively, you can tweak the connection settings like user, password and database name
  5. Next we will deploy Payload CMS and connect it to the MongoDB instance we just created
  6. In the project, click “Deploy Service” again, choose the same server, where your database is running on and select “Repository” as the deployment method
  7. In the repository URL field, look for your Payload CMS repository. If it does not show up in the list, make sure you granted Sliplane access to the repo using the “Configure Repository Access” button
  8. Add a volume, give it a name of your choice and mount it to /app/media
  9. Add the PAYLOAD_SECRET and DATABASE_URI environment variables. PAYLOAD_SECRET is an arbitrary password. The database URI should look like this: mongodb://username:password@mongodb:27017/payload?authSource=admin – You can find all connection settings like user, password, database and internal host name in the MongoDB service settings
  10. Click “Deploy” and wait for the deployment to finish

Conclusion

Running Payload CMS in Docker requires a few configuration tweaks beyond the default setup.

The key fixes include pinning the pnpm version, enabling Next.js standalone mode, setting proper file permissions, and optionally configuring object storage for production use.


This content originally appeared on DEV Community and was authored by Lukas Mauser