Пошаговая инструкция · Самостоятельная установка
Как установить 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 базы здесь только для генерации проекта; рабочие реквизиты зададим отдельно.
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 не заменяйте при обычном перезапуске.
.envrm -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. Сохраните ключ и пароль базы.
nano .envРабочих replace-me не осталось; SERVER_URL — ваш HTTPS-адрес.
Шаг 5. Сохраните конфигурацию CMS #
Откройте nano site/src/payload.config.ts и замените файл кодом ниже. Коллекция posts хранит title/slug/body, читатель может читать записи, изменение требует входа. Push=false означает, что схему базы меняем явными миграциями. Media сохраняется в постоянном site/media.
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. Скобки в пути требуют кавычек в команде терминала.
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 и наличие переменных до запуска базы.
docker compose config --quietconfig --quiet завершился без ошибки.
Шаг 9. Запустите PostgreSQL #
База нужна командам миграции; веб-приложение ещё не запускается.
docker compose up -d dbКонтейнер db создан и работает.
Шаг 10. Установите SMTP-адаптер #
Одноразовый web-контейнер устанавливает зависимости и закреплённый nodemailer-адаптер. Package-lock должен сохраниться вместе с проектом.
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:typespayload-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-режим; ошибки импорта или типов нужно исправить до запуска.
docker compose run --rm web npm run buildnpm 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-nextjsmap $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 проверяет ответ для вашего домена. При ошибке остановитесь, исправьте указанную строку и повторите проверку.
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.
sudo /snap/bin/certbot --nginx -d cms.example.com --redirectCertbot сообщил об успешной установке. Откройте 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 webWeb остановлен; 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Сайт работает, запись и изображение доступны.