Payload CMS в Docker Compose: готовый файл
Payload CMS — это TypeScript-first headless CMS, где схема контента описывается кодом, а не собирается в визуальном конструкторе полей. Это удобно для разработчиков, но неудобно для того, кто ищет готовый рецепт запуска: официальная документация показывает установку через create-payload-app и локальный старт, а не production-конфиг на выделенном сервере. Ниже — рабочий docker-compose.yml с PostgreSQL и S3-совместимым хранилищем для медиа, плюс объяснение, почему собранная сборка отличается от npm run dev и на что обратить внимание при переезде на свой VPS.
Содержание
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Что такое Payload CMS и чем он отличается от Strapi или Directus
Payload CMS (актуальная линейка — третья версия, построенная поверх Next.js) — это не отдельный сервер с админкой поверх БД, а библиотека, которую вы подключаете к своему Next.js-приложению. Админ-панель, REST/GraphQL API и локальный API для серверных компонентов работают в одном процессе Next.js. Это ключевое отличие от Strapi или Directus, где CMS — самостоятельный сервис с собственным Docker-образом.
Практическое следствие: официального payloadcms/payload образа с «уже собранным приложением» не существует — вы либо используете create-payload-app как стартовый Next.js-проект и собираете свой образ, либо встраиваете Payload в существующее Next.js-приложение. Для сервера это означает один контейнер с Node.js/Next.js-приложением плюс отдельные контейнеры для базы данных и (опционально) хранилища медиафайлов.
Из коробки Payload поддерживает MongoDB и PostgreSQL как адаптеры БД. Если проект новый и не тянет legacy-данные из Mongo, для сервера лучше подходит PostgreSQL — проще бэкапить pg_dump, проще следить за состоянием, есть материал про установку PostgreSQL на Ubuntu 24.04, если захотите вынести БД из контейнера на хост.
Структура проекта и Dockerfile
Возьмём стартовый шаблон и добавим сборку под production. Структура каталогов:
payload-project/
├── docker-compose.yml
├── .env
├── Dockerfile
├── next.config.mjs
├── package.json
├── src/
│ ├── payload.config.ts
│ └── ...
└── media/ # монтируется, если храните файлы локально
Dockerfile для Payload на Next.js — многоэтапная сборка, чтобы финальный образ не тащил dev-зависимости:
FROM node:20-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 payload
COPY --from=builder /app/public ./public
COPY --from=builder --chown=payload:nodejs /app/.next/standalone ./
COPY --from=builder --chown=payload:nodejs /app/.next/static ./.next/static
USER payload
EXPOSE 3000
ENV PORT=3000
CMD ["node", "server.js"]
Для standalone-сборки Next.js нужно в next.config.mjs включить output: 'standalone' — иначе финальный образ раздуется за счёт полного node_modules:
import { withPayload } from '@payloadcms/next/withPayload'
const nextConfig = {
output: 'standalone',
}
export default withPayload(nextConfig)
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверГотовый docker-compose.yml
Полный стек: приложение Payload, PostgreSQL для данных, MinIO как S3-совместимое хранилище медиа (чтобы файлы не терялись при пересборке контейнера и не раздували том приложения).
services:
payload:
build: .
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000"
environment:
DATABASE_URI: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
PAYLOAD_SECRET: ${PAYLOAD_SECRET}
NEXT_PUBLIC_SERVER_URL: ${PUBLIC_URL}
S3_ENDPOINT: http://minio:9000
S3_ACCESS_KEY: ${MINIO_ROOT_USER}
S3_SECRET_KEY: ${MINIO_ROOT_PASSWORD}
S3_BUCKET: payload-media
S3_REGION: us-east-1
depends_on:
postgres:
condition: service_healthy
minio:
condition: service_healthy
networks:
- payload-net
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- payload-net
minio:
image: minio/minio:latest
restart: unless-stopped
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
volumes:
- miniodata:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 10s
timeout: 5s
retries: 5
networks:
- payload-net
volumes:
pgdata:
miniodata:
networks:
payload-net:
driver: bridge
Файлы наружу не пробрасываются напрямую — публикуется только payload через обратный прокси на 80/443, а порт приложения слушает 127.0.0.1:3000, чтобы контейнер не торчал в интернет напрямую. Если MinIO не нужен (например, файлов немного и вы готовы хранить их в примонтированном томе), уберите сервис и переключите Payload-адаптер загрузки на локальную файловую систему — но тогда не забудьте volume под media/.
Переменные окружения (.env)
POSTGRES_USER=payload
POSTGRES_PASSWORD=замените-на-сложный-пароль
POSTGRES_DB=payload
PAYLOAD_SECRET=сгенерируйте-32-случайных-символа
MINIO_ROOT_USER=payload-admin
MINIO_ROOT_PASSWORD=замените-на-сложный-пароль
PUBLIC_URL=https://cms.example.com
PAYLOAD_SECRET используется для подписи JWT-токенов авторизации — сгенерировать можно так:
openssl rand -base64 32
Не коммитьте .env в репозиторий — добавьте его в .gitignore и держите отдельно от кода, особенно если репозиторий публичный.
Настройка адаптеров в payload.config.ts
Чтобы приложение реально использовало Postgres и S3 из compose-файла, конфиг Payload должен ссылаться на переменные окружения, а не на хардкод:
import { postgresAdapter } from '@payloadcms/db-postgres'
import { s3Storage } from '@payloadcms/storage-s3'
import { buildConfig } from 'payload'
export default buildConfig({
secret: process.env.PAYLOAD_SECRET,
db: postgresAdapter({
pool: {
connectionString: process.env.DATABASE_URI,
},
}),
plugins: [
s3Storage({
collections: {
media: true,
},
bucket: process.env.S3_BUCKET,
config: {
endpoint: process.env.S3_ENDPOINT,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY,
secretAccessKey: process.env.S3_SECRET_KEY,
},
region: process.env.S3_REGION,
forcePathStyle: true, // обязательно для MinIO
},
}),
],
})
Параметр forcePathStyle: true критичен именно для MinIO — без него AWS SDK попытается обратиться по виртуальному хостовому стилю (bucket.minio:9000), а не по пути (minio:9000/bucket), и загрузка файлов будет падать с ошибкой резолвинга DNS.
Бакет payload-media в MinIO нужно создать один раз при первом запуске — либо через веб-консоль MinIO на порту 9001 (проброшенном временно для настройки), либо через mc:
docker compose exec minio mc alias set local http://localhost:9000 $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD
docker compose exec minio mc mb local/payload-media
Первый запуск и создание администратора
docker compose build
docker compose up -d
docker compose logs -f payload
Первый старт занимает время: Next.js standalone-сервер запускается, Payload прогоняет миграции схемы в PostgreSQL (при первом запуске создаёт таблицы под коллекции, описанные в конфиге). Убедитесь, что в логе нет ошибок подключения к БД — если Payload стартовал раньше, чем PostgreSQL прошёл healthcheck, depends_on: condition: service_healthy в compose-файле выше как раз для этого и нужен.
Первого пользователя-администратора Payload предлагает создать через веб-интерфейс — откройте https://cms.example.com/admin, форма первой регистрации появится автоматически, если в базе ещё нет ни одного пользователя. Дальше добавлять администраторов можно только из уже созданного аккаунта — это осознанное ограничение безопасности, а не баг.
Обратный прокси и SSL
Наружу проект отдаётся через Caddy или nginx — коротко на примере Caddy, у него автоматический SSL из коробки:
cms.example.com {
reverse_proxy 127.0.0.1:3000
}
Если вы ещё не настраивали Caddy на сервере, есть отдельный разбор — Caddy с авто-SSL на Ubuntu 24.04, там же список типичных ошибок с сертификатами. Для nginx подойдёт стандартный proxy_pass на 127.0.0.1:3000 плюс заголовки X-Forwarded-Proto и Host — Next.js их учитывает для генерации абсолютных URL в API-ответах.
Отдельно проверьте NEXT_PUBLIC_SERVER_URL — Payload использует это значение для построения ссылок на медиафайлы и для CORS. Если значение не совпадает с реальным доменом за прокси, админка может подгружать ассеты по неверным адресам.
Бэкапы, обновления и ресурсы
Бэкап затрагивает два места: PostgreSQL (структура и контент) и MinIO (файлы медиа).
# бэкап базы
docker compose exec postgres pg_dump -U payload payload > backup-$(date +%F).sql
# бэкап бакета MinIO
docker compose exec minio mc mirror local/payload-media /backup/media
Обновление версии Payload — это пересборка образа с новым package.json, а не замена образа как в Strapi/Directus. Перед обновлением мажорной версии обязательно смотрите changelog: между второй и третьей версией Payload менялась структура конфига и адаптеров БД, обратная совместимость не гарантирована.
По ресурсам ориентируйтесь на связку Next.js SSR + PostgreSQL + MinIO как минимум на 2 vCPU / 4 GB RAM — это грубый ориентир, у вас цифры будут отличаться в зависимости от объёма контента, количества коллекций с richtext-полями и параллельных запросов к API. Если S3 не нужен и файлы храните на диске, MinIO можно убрать из стека — это снизит нагрузку на память.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Payload обязательно требует Node.js-хостинг, обычный shared-хостинг не подойдёт?
Да, это Next.js-приложение с серверным рендерингом и API-роутами, для него нужен полноценный процесс Node.js — VPS или контейнерная платформа, не статический shared-хостинг.
Можно ли использовать MongoDB вместо PostgreSQL в этом compose-файле?
Да, Payload поддерживает оба адаптера — замените postgresAdapter на mongooseAdapter в конфиге и сервис postgres на mongo в compose-файле, переменную DATABASE_URI оставьте в формате mongodb://.
Нужен ли обязательно MinIO, или можно хранить файлы прямо в контейнере?
Можно хранить на локальном томе через дефолтный upload-адаптер Payload, но тогда при пересборке контейнера важно не потерять volume с media/ — MinIO снимает эту заботу и упрощает бэкапы файлов отдельно от кода.
Как поведёт себя Payload при нехватке памяти на сервере?
Next.js-процесс упадёт с OOM-килом от ядра, контейнер перезапустится по restart: unless-stopped, но пока идёт пересборка кэша страниц — возможны временные 502 от прокси. Смотрите docker stats и не экономьте на RAM ниже ориентира.
Отличается ли эта установка от Strapi по сложности?
Да, заметно: Strapi — готовый образ и отдельный процесс, у Payload вы собираете и деплоите полноценное Next.js-приложение сами. Если хочется более простого старта headless CMS, посмотрите материал про Strapi в Docker Compose.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →