Удельта

Пошаговая инструкция · Самостоятельная установка

Как установить Payload CMS и Next.js на сервер Ubuntu

Пошаговая инструкция для самостоятельной установки. Соберём Payload CMS внутри Next.js, подключим PostgreSQL и запустим production-сервер. Создадим запись в панели, покажем её на публичной странице и подготовим сохранение базы, медиа и исходного проекта.

Публикация первой страницы: структурированный материал проходит из редакционного архива в сайт

Что подготовить и как читать инструкцию #

Payload — часть вашего Next.js-приложения, а не отдельный готовый редактор, который автоматически создаёт сайт. В этой инструкции будет одна сборка с панелью /admin, REST API и простой публичной страницей записей. Модель контента и внешний вид дальше развиваются в исходном коде. Готовой системы ролей для вашей организации blank-шаблон не задаёт.

Подготовьте Ubuntu 26.04 LTS, Docker Engine, Compose v2 и Nginx. Node.js 24 запускаем внутри контейнера, поэтому версия Node на хосте не участвует в сборке. Для сборки на том же сервере предусмотрите 4 vCPU и 8–12 ГБ памяти; при недостатке памяти собирайте релиз на отдельной машине. Нагрузка после сборки зависит от запросов и работы с изображениями.

Команды выполняйте в терминале сервера после подключения по SSH, если шаг не указывает другое место. sudo даёт команде права администратора. Действия в браузере выполняйте на своём компьютере. Домены, IP-адреса и значения ВАШ_ПАРОЛЬ из примеров заменяйте своими до выполнения команды.

Двигайтесь по одному шагу и проверяйте результат перед следующим. Блок с <<EOF или другим именем завершения копируйте целиком: он записывает содержимое одного файла. При работе в nano сохраняйте файл клавишами Ctrl+O, Enter и выходите Ctrl+X. Если прервали SSH-сеанс, снова перейдите в указанный каталог; переменные предыдущего сеанса потребуется задать заново.

Настройка внешнего доступа использует обратный прокси Nginx и Certbot для Nginx. Приложение остаётся доступным на локальном порту; снаружи открыт его HTTPS-домен.

Дополнительные официальные источники: Контроль доступа, Настройка SMTP, Релиз Payload 3.90.2, Генератор проекта этого релиза.

Шаг 1. Создайте каталог проекта #

В ~/apps/payload будет конфигурация установки и папка site с Next.js/Payload. Команды Compose выполняются из внешнего каталога.

Создайте каталог проекта
mkdir -p ~/apps/payload
cd ~/apps/payload

Вы в ~/apps/payload.

Шаг 2. Создайте официальный blank-проект #

Одноразовый Node.js-контейнер запускает create-payload-app 3.90.2. Он создаёт site без установки зависимостей. Placeholder базы здесь только для генерации проекта; рабочие реквизиты зададим отдельно.

Создайте официальный blank-проект
docker run --rm -it --user "$(id -u):$(id -g)" -e npm_config_cache=/tmp/npm-cache -v "$PWD":/workspace -w /workspace node:24-bookworm-slim \
  npx --yes create-payload-app@3.90.2 site --template blank --db postgres \
  --db-connection-string postgres://payload:placeholder@db:5432/payload \
  --branch v3.90.2 --version 3.90.2 --use-npm --no-deps --no-agent --no-git

Проект создан в site; CLI не завершился ошибкой.

Шаг 3. Создайте рабочий .env #

Удалим только примерный site/.env, затем создадим внешний .env с паролем базы и ключом Payload. UID/GID сохраняют владельца создаваемых файлов. Ключ PAYLOAD_SECRET не заменяйте при обычном перезапуске.

Создайте рабочий .env
rm -f site/.env
umask 077
cat > .env <<EOF
PAYLOAD_UID=$(id -u)
PAYLOAD_GID=$(id -g)
POSTGRES_PASSWORD=$(openssl rand -hex 32)
PAYLOAD_SECRET=$(openssl rand -hex 32)
SERVER_URL=https://cms.example.com
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=replace-me
SMTP_PASS=replace-me
SMTP_FROM=cms@example.com
EOF
chmod 600 .env

Рабочий .env создан и защищён, ключи заполнены.

Шаг 4. Заполните домен и SMTP #

Откройте .env, замените cms.example.com, SMTP-примеры и replace-me реальными данными. SMTP_SECURE соответствует режиму вашего провайдера; пример на 587 использует STARTTLS. Сохраните ключ и пароль базы.

Заполните домен и SMTP
nano .env

Рабочих replace-me не осталось; SERVER_URL — ваш HTTPS-адрес.

Шаг 5. Сохраните конфигурацию CMS #

Откройте nano site/src/payload.config.ts и замените файл кодом ниже. Коллекция posts хранит title/slug/body, читатель может читать записи, изменение требует входа. Push=false означает, что схему базы меняем явными миграциями. Media сохраняется в постоянном site/media.

Сохраните конфигурацию CMS
import { postgresAdapter } from '@payloadcms/db-postgres'
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import { buildConfig, type CollectionConfig } from 'payload'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import sharp from 'sharp'
import { Users } from './collections/Users'
import { Media } from './collections/Media'

const dirname = path.dirname(fileURLToPath(import.meta.url))
const signedIn = ({ req }: { req: { user?: unknown } }) => Boolean(req.user)
const Posts: CollectionConfig = {
  slug: 'posts',
  admin: { useAsTitle: 'title' },
  access: { read: () => true, create: signedIn, update: signedIn, delete: signedIn },
  fields: [
    { name: 'title', type: 'text', required: true },
    { name: 'slug', type: 'text', required: true, unique: true },
    { name: 'body', type: 'textarea', required: true },
  ],
}
const serverURL = process.env.SERVER_URL || 'https://cms.example.com'
export default buildConfig({
  serverURL,
  cors: [serverURL],
  csrf: [serverURL],
  admin: { user: Users.slug, importMap: { baseDir: dirname } },
  collections: [
    Users,
    { ...Media, upload: { staticDir: path.resolve(dirname, '../media') } },
    Posts,
  ],
  secret: process.env.PAYLOAD_SECRET || '',
  editor: lexicalEditor(),
  typescript: { outputFile: path.resolve(dirname, 'payload-types.ts') },
  db: postgresAdapter({
    pool: { connectionString: process.env.DATABASE_URL },
    push: false,
  }),
  email: nodemailerAdapter({
    defaultFromAddress: process.env.SMTP_FROM || 'cms@example.com',
    defaultFromName: 'Редакция',
    transportOptions: {
      host: process.env.SMTP_HOST,
      port: Number(process.env.SMTP_PORT || 587),
      secure: process.env.SMTP_SECURE === 'true',
      auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
    },
  }),
  sharp,
})

Файл сохранён без пропущенных импортов и скобок; закрытые документы в публичную posts не помещаются.

Шаг 6. Сохраните страницу Next.js #

Откройте nano "site/src/app/(frontend)/page.tsx" и замените страницу кодом. Она получает записи на сервере и возвращает HTML. Скобки в пути требуют кавычек в команде терминала.

Сохраните страницу Next.js
import config from '@payload-config'
import { getPayload } from 'payload'

export const dynamic = 'force-dynamic'

export default async function HomePage() {
  const payload = await getPayload({ config })
  const { docs } = await payload.find({
    collection: 'posts', limit: 20, sort: '-createdAt',
    depth: 0, overrideAccess: false,
  })
  return (
    <main style={{ maxWidth: 760, margin: '40px auto', padding: 24 }}>
      <h1>Публикации</h1>
      {docs.length === 0 && <p>Первая запись появится после сохранения в CMS.</p>}
      {docs.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p style={{ whiteSpace: 'pre-wrap' }}>{post.body}</p>
        </article>
      ))}
    </main>
  )
}

Страница сохранена; overrideAccess=false сохраняет проверку доступа коллекции.

Шаг 7. Сохраните описание контейнеров #

Откройте nano compose.yaml и вставьте YAML. PostgreSQL хранит данные, web запускает приложение; постоянные каталоги сохраняют исходники и медиа. Веб-порт 3010 привязан к localhost.

Сохраните описание контейнеров
services:
  db:
    image: postgres:17-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: payload
      POSTGRES_USER: payload
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set password}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: [CMD-SHELL, "pg_isready -U payload -d payload"]
      interval: 10s
      timeout: 5s
      retries: 10
  web:
    image: node:24-bookworm-slim
    restart: unless-stopped
    user: "${PAYLOAD_UID:?set UID}:${PAYLOAD_GID:?set GID}"
    working_dir: /workspace
    env_file: .env
    environment:
      NODE_ENV: production
      DATABASE_URL: postgres://payload:${POSTGRES_PASSWORD}@db:5432/payload
      NEXT_TELEMETRY_DISABLED: "1"
      npm_config_cache: /tmp/npm-cache
    volumes:
      - ./site:/workspace
    ports:
      - "127.0.0.1:3010:3000"
    depends_on:
      db:
        condition: service_healthy
    command: [npm, run, start, --, --hostname, 0.0.0.0]
volumes:
  pgdata:

Сохранены нужные переменные и постоянные каталоги.

Шаг 8. Проверьте Compose #

Проверим YAML и наличие переменных до запуска базы.

Проверьте Compose
docker compose config --quiet

config --quiet завершился без ошибки.

Шаг 9. Запустите PostgreSQL #

База нужна командам миграции; веб-приложение ещё не запускается.

Запустите PostgreSQL
docker compose up -d db

Контейнер db создан и работает.

Шаг 10. Установите SMTP-адаптер #

Одноразовый web-контейнер устанавливает зависимости и закреплённый nodemailer-адаптер. Package-lock должен сохраниться вместе с проектом.

Установите SMTP-адаптер
docker compose run --rm web npm install --include=dev --save-exact @payloadcms/email-nodemailer@3.90.2

Установка завершилась без ошибки npm.

Шаг 11. Сгенерируйте типы коллекций #

Payload создаёт TypeScript-типы из вашей конфигурации. После дальнейших изменений коллекций эту команду нужно повторять.

Сгенерируйте типы коллекций
docker compose run --rm web npm run generate:types

payload-types.ts обновлён без ошибки.

Шаг 12. Создайте и примените первую миграцию #

Первая команда создаёт описание изменений базы, вторая применяет его. При ошибке создания не запускайте применение. В дальнейшем миграции сохраняются вместе с исходниками проекта.

Создайте и примените первую миграцию
docker compose run --rm web npm run payload -- migrate:create initial
docker compose run --rm web npm run payload -- migrate

Начальная миграция создана и применена без ошибки.

Шаг 13. Соберите production-приложение #

Сборка проверяет TypeScript и создаёт рабочие файлы Next.js. Это не dev-режим; ошибки импорта или типов нужно исправить до запуска.

Соберите production-приложение
docker compose run --rm web npm run build

npm run build завершился успешно.

Шаг 14. Запустите сайт локально #

Запустим web и проверим HTTP на сервере. При первом старте дайте процессу время; если curl ещё не отвечает, посмотрите docker compose logs --tail=80 web и повторите проверку.

Запустите сайт локально
docker compose up -d web
curl -I http://127.0.0.1:3010

Локальный 3010 отвечает.

Шаг 15. Ограничьте создание первого администратора #

До первого входа разрешите HTTPS только своему IP в firewall провайдера. TCP 80 нужен для сертификата. Создание первого пользователя нельзя оставлять открытым случайным посетителям.

Первоначальную настройку можете открыть только вы.

Шаг 16. Сохраните сайт Nginx #

Установите Nginx и Certbot по подготовке Ubuntu, если ещё не установили. Откройте sudo nano /etc/nginx/sites-available/payload-cms-nextjs и вставьте файл ниже. Замените все домены своими. Nginx передаёт приложению домен, адрес клиента и протокол; Upgrade и Connection сохраняют WebSocket. До получения сертификата условие $scheme блокирует обычные HTTP-запросы кодом 403, чтобы вход не происходил без шифрования.

Файл /etc/nginx/sites-available/payload-cms-nextjs
map $http_upgrade $payload_cms_nextjs_connection_upgrade {
    default upgrade;
    "" close;
}

server {
    listen 80;
    server_name cms.example.com;
    client_max_body_size 100m;
    location / {
        if ($scheme = http) { return 403; }
        proxy_pass http://127.0.0.1:3010;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $payload_cms_nextjs_connection_upgrade;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
        proxy_buffering off;
    }
}

Сохраните Ctrl+O, Enter, Ctrl+X. В файле пока нет путей к сертификатам: Nginx сможет проверить его до выпуска HTTPS.

Шаг 17. Подключите сайт и проверьте HTTP #

Ссылка подключает сохранённый сайт. nginx -t проверяет все настройки сервера; && разрешает перечитать их только при успешной проверке. curl проверяет ответ для вашего домена. При ошибке остановитесь, исправьте указанную строку и повторите проверку.

Подключите сайт и проверьте HTTP
sudo ln -sfn /etc/nginx/sites-available/payload-cms-nextjs /etc/nginx/sites-enabled/payload-cms-nextjs
sudo nginx -t && sudo systemctl reload nginx
curl -I http://cms.example.com/

Ожидается successful от nginx -t и HTTP 403 для обычного запроса: до HTTPS это намеренная блокировка. DNS должен указывать на сервер, TCP 80/443 — быть доступен у провайдера. Ничего не вводите в форму входа по HTTP.

Шаг 18. Получите сертификат и включите HTTPS #

Каждое имя из команды должно указывать на этот сервер. Certbot проверит домены, сам добавит сертификат в сайт Nginx и включит перенаправление HTTP → HTTPS. Укажите свою почту при запросе и подтвердите условия Let’s Encrypt. Это отдельное действие после успешной проверки HTTP.

Получите сертификат и включите HTTPS
sudo /snap/bin/certbot --nginx -d cms.example.com --redirect

Certbot сообщил об успешной установке. Откройте HTTPS-адрес со своего компьютера: браузер не должен предупреждать о сертификате. Для закрытой панели используйте разрешённый IP. При ошибке исправьте DNS, в том числе AAAA, либо доступ к TCP 80; не повторяйте запросы выпуска подряд.

Шаг 19. Проверьте продление сертификата #

Certbot из snap создаёт службу автоматического продления. Dry-run выполняет пробное продление: настоящие сертификаты не заменяются. Затем проверьте, что таймер планирует следующий запуск. Сохраняйте доступ к TCP 80 для следующих проверок домена.

Проверьте продление сертификата
sudo /snap/bin/certbot renew --dry-run
systemctl list-timers --all 'snap.certbot.renew*'

Пробное продление успешно, у таймера есть следующий запуск. Если тест не прошёл, проверьте /var/log/letsencrypt/letsencrypt.log; сначала исправьте причину. После этого можно создавать учётную запись и приглашать пользователей.

Шаг 20. Создайте пользователя CMS #

В браузере откройте https://cms.example.com/admin. Создайте первого пользователя с уникальным паролем. В этой минимальной схеме все допущенные редакторы доверенные; более детальные роли вводятся отдельной настройкой доступа.

Администратор входит после выхода из первоначальной сессии.

Шаг 21. Создайте и проверьте запись #

В Posts добавьте title, уникальный slug и body. Сохраните запись, затем в приватном окне откройте главную страницу. Эта простая коллекция публикует текст сразу после сохранения, без черновиков. Загрузите небольшое тестовое изображение в Media.

Текст виден посетителю; медиа открывается после повторного входа.

Шаг 22. Подготовьте согласованную копию #

Создадим закрытый каталог и остановим web, чтобы не менять записи и медиа во время копирования. База остаётся работать.

Подготовьте согласованную копию
umask 077
backup_dir="$HOME/backups/payload/$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$backup_dir"
docker compose stop web

Web остановлен; backup_dir задан в текущем SSH-сеансе.

Шаг 23. Сохраните базу и проект #

Дамп хранит записи, архив — код, миграции, медиа и секреты. Node_modules и .next можно пересоздать, поэтому исключаем их. Перенесите обе части в закрытое внешнее хранилище.

Сохраните базу и проект
docker compose exec -T db pg_dump -U payload payload > "$backup_dir/database.sql"
sudo tar --exclude=site/node_modules --exclude=site/.next \
  -czf "$backup_dir/project.tar.gz" site .env compose.yaml

Дамп и project.tar.gz созданы без ошибки.

Шаг 24. Верните сайт и проверьте копию #

Запустите web. Восстановление проверяйте отдельно с той же версией, кодом/миграциями и тестовым SMTP; затем проверьте запись и медиа.

Настройки внешнего доступа храните вместе с копией приложения: /etc/nginx/sites-available/payload-cms-nextjs и /etc/letsencrypt содержат сайт и сертификаты с закрытыми ключами. Архивируйте их с sudo в закрытое хранилище. После восстановления подключите сайт через sites-enabled, выполните sudo nginx -t, затем перечитайте Nginx. Для ошибки 502 сначала проверьте локальное приложение и /var/log/nginx/error.log; для сертификата — /var/log/letsencrypt/letsencrypt.log и таймер продления.

Верните сайт и проверьте копию
docker compose start web

Сайт работает, запись и изображение доступны.

Обсудить установку и настройку

Материал был полезен?

Сообщить об ошибке или задать вопрос редакции

Сообщение увидит редакция. Не указывайте пароли и личные данные. Текст удаляется через 180 дней.

О материале

ИнфраструктураРуководствоЗадача: установитьУровень: практикаPayload CMS + Next.js

Последнее обновление:

Обсуждение

Комментарии проходят подтверждение адреса и проверку редакцией.