Пошук уроків, статей та іншого контенту
Створите CI/CD pipeline для перевірок, production-збірки, міграцій і безпечного автоматичного деплою.
Для Next.js production pipeline доцільно розділити на три етапи:
Перевірки — встановлення залежностей, lint і тести.
Збірка — production-збірка Next.js і Docker-образу.
Деплой — публікація образу, виконання міграцій і запуск нової версії.
Приклад нижче використовує:
GitHub Actions;
Docker і GitHub Container Registry;
сервер із Docker Compose;
Prisma для міграцій бази даних;
деплой лише після успішного проходження перевірок.
Кожна версія образу позначається SHA коміту. Це важливо: деплой не залежить від змінного тегу latest, а конкретну версію можна швидко повернути.
У package.json мають бути production-команди:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "vitest run",
"db:migrate:deploy": "prisma migrate deploy"
},
"dependencies": {
"next": "^15.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"@prisma/client": "^6.0.0"
},
"devDependencies": {
"prisma": "^6.0.0",
"vitest": "^2.0.0"
}
}Команда db:migrate:deploy повинна застосовувати вже створені міграції, але не створювати нові. Для Prisma це саме prisma migrate deploy.
Міграції створюються локально або в окремому процесі розробки:
npx prisma migrate dev --name add_ordersДо репозиторію додається каталог prisma/migrations. У production запускається тільки:
npm run db:migrate:deployДля зменшення розміру образу Next.js можна використовувати standalone-режим.
У next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone"
};
module.exports = nextConfig;Приклад Dockerfile:
FROM node:20-alpine AS dependencies
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=dependencies /app/node_modules ./node_modules
COPY . .
RUN mkdir -p public
RUN npx prisma generate
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/package-lock.json ./package-lock.json
RUN npm ci --omit=dev \
&& npx prisma generate \
&& chown -R nextjs:nodejs /app
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]Під час збірки:
залежності встановлюються через npm ci;
Prisma Client генерується за схемою;
виконується next build;
у фінальний образ потрапляє standalone-сервер;
застосунок запускається не від імені root.
Файл .dockerignore не повинен дозволяти випадково копіювати секрети:
node_modules
.next
.git
.env
.env.*
coverage
npm-debug.logСтворіть файл .github/workflows/ci-cd.yml:
name: CI/CD
on:
pull_request:
push:
branches:
- main
concurrency:
group: production-${{ github.ref }}
cancel-in-progress: true
jobs:
quality:
name: Перевірки
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Отримання коду
uses: actions/checkout@v4
- name: Встановлення Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Встановлення залежностей
run: npm ci
- name: Перевірка стилю
run: npm run lint
- name: Запуск тестів
run: npm test
- name: Перевірка production-збірки
run: npm run build
image:
name: Збірка Docker-образу
needs: quality
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Отримання коду
uses: actions/checkout@v4
- name: Вхід до GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Створення метаданих образу
id: metadata
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=sha,format=long
- name: Збірка і публікація образу
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
deploy:
name: Production-деплой
needs: image
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: production
permissions:
contents: read
steps:
- name: Деплой на сервер
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.PRODUCTION_SSH_KEY }}
fingerprint: ${{ secrets.PRODUCTION_HOST_FINGERPRINT }}
script: |
set -e
export IMAGE="ghcr.io/${{ github.repository }}:sha-${{ github.sha }}"
cd /opt/my-next-app
echo "${{ secrets.GHCR_READ_TOKEN }}" | docker login ghcr.io \
--username "${{ secrets.GHCR_READ_USERNAME }}" \
--password-stdin
docker compose pull app
docker compose run --rm app npm run db:migrate:deploy
docker compose up -d --remove-orphans app
docker image prune -fquality запускається:
для кожного Pull Request;
для кожного push у main.
image і deploy запускаються тільки після push у main. Pull Request може перевірити код, але не має доступу до production.
needs: quality гарантує, що образ не буде опублікований, якщо lint, тести або production-збірка завершилися помилкою.
concurrency скасовує застарілий запуск для тієї самої гілки. Це запобігає ситуації, коли старий pipeline завершується після нового і випадково деплоїть попередню версію.
На сервері створіть /opt/my-next-app/compose.yml:
services:
app:
image: ${IMAGE}
restart: unless-stopped
env_file:
- .env
ports:
- "127.0.0.1:3000:3000"Файл /opt/my-next-app/.env зберігається тільки на сервері:
NODE_ENV=production
DATABASE_URL=postgresql://app:strong-password@database:5432/appЯкщо база даних запускається в іншому контейнері або на зовнішньому сервісі, у DATABASE_URL потрібно вказати відповідний хост.
Файл .env не повинен зберігатися в Git і не повинен передаватися через аргументи командного рядка pipeline. Аргументи команд можуть потрапити в журнали або список процесів.
Сервер повинен мати доступ до образу в GHCR. Для цього використовуються окремі credentials із правом читання пакетів:
GHCR_READ_USERNAME;
GHCR_READ_TOKEN.
Токен для читання образів не повинен мати права на запис або керування репозиторієм.
Для job deploy використовується GitHub Environment із назвою production.
У цьому environment налаштовуються:
required reviewers для ручного підтвердження production-деплою;
обмеження гілки — тільки main;
production secrets.
Рекомендований набір секретів:
PRODUCTION_HOST — адреса сервера;
PRODUCTION_USER — окремий користувач для деплою;
PRODUCTION_SSH_KEY — приватний SSH-ключ;
PRODUCTION_HOST_FINGERPRINT — fingerprint SSH-хоста;
GHCR_READ_USERNAME;
GHCR_READ_TOKEN.
Значення DATABASE_URL краще залишати на сервері або в секретному сховищі. Воно не потрібне GitHub Actions для побудови образу.
Після push у main pipeline виконує такі кроки:
Перевіряє код.
Створює production-збірку Next.js.
Збирає Docker-образ.
Публікує образ із тегом SHA коміту.
Підключається до production-сервера.
Завантажує саме цей образ.
Виконує міграції бази даних.
Перезапускає застосунок.
Міграції запускаються перед оновленням застосунку:
docker compose run --rm app npm run db:migrate:deployЦе дає змогу новому коду працювати зі схемою, для якої він був підготовлений.
Для змін, що ламають сумісність, використовуйте поетапний підхід:
додайте нову колонку або таблицю, не видаляючи стару;
розгорніть код, який підтримує обидві версії;
перенесіть дані;
у наступному релізі видаліть стару структуру.
Не слід одночасно видаляти колонку міграцією та розгортати код, який ще може її читати.
Оскільки образ має тег SHA, попередню версію можна запустити явно:
export IMAGE="ghcr.io/owner/my-next-app:sha-previous-commit"
docker compose pull app
docker compose up -d --remove-orphans appВідкат контейнера не скасовує міграції бази даних. Тому міграції мають бути зворотно сумісними на час розгортання.
Автоматичний prisma migrate reset у production неприпустимий: ця команда видаляє дані. Для production використовуйте тільки:
npx prisma migrate deployДля кожного job задавайте permissions явно:
permissions:
contents: readJob, який публікує образ, додатково отримує:
permissions:
contents: read
packages: writeJob деплою не повинен мати права на запис у репозиторій або пакети, якщо це не потрібно.
Не використовуйте секрети:
у назвах Docker-тегів;
у тексті комітів;
у echo;
у значеннях, які передаються в docker build --build-arg;
у відкритих Pull Request із fork-репозиторіїв.
Секрети GitHub Actions маскуються не в усіх похідних формах. Наприклад, перетворене або частково виведене значення може не бути замасковане.
Параметр fingerprint захищає від підключення до неправильного SSH-хоста. Fingerprint потрібно отримати під час налаштування сервера та зберегти в GitHub Secret.
Не вимикайте перевірку host key через параметри на кшталт StrictHostKeyChecking=no.
Повторний запуск pipeline не повинен ламати production:
npm ci встановлює залежності з lock-файла;
prisma migrate deploy застосовує тільки ще не виконані міграції;
docker compose up -d приводить сервіс до описаного стану;
тег SHA завжди вказує на конкретний образ.
До push перевірте ті самі команди, які запускає CI:
npm ci
npm run lint
npm test
npm run build
docker build -t my-next-app:local .Для перевірки production-контейнера:
docker run --rm \
-p 3000:3000 \
--env-file .env.production \
my-next-app:localПісля запуску застосунок має бути доступний на http://localhost:3000.
Причина — відсутня умова для image і deploy.
Використовуйте перевірку:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'latestТег latest може змінитися, тому важко визначити, яка версія зараз працює.
Краще використовувати:
ghcr.io/owner/app:sha-<commit-sha>Якщо міграція є частиною CMD, кілька реплік можуть одночасно намагатися змінити схему бази.
Запускайте міграцію окремим одноразовим кроком до docker compose up.
prisma migrate devЦя команда призначена для розробки та може змінювати схему способом, непридатним для production.
Для деплою використовуйте:
prisma migrate deployНе копіюйте .env у build context і не використовуйте production-секрети під час next build, якщо вони не потрібні для статичної генерації.
Секрети runtime мають передаватися через environment контейнера.
Створіть окремого користувача і запускайте Next.js від його імені:
USER nextjsМіграція та код мають розгортатися у сумісній послідовності. Для небезпечних змін застосовуйте поетапні міграції та не видаляйте старі поля в тому самому релізі, де вони ще використовуються.
Надійний CI/CD pipeline для Next.js повинен:
перевіряти lint, тести і production-збірку до деплою;
використовувати npm ci та lock-файл;
збирати immutable Docker-образ із тегом SHA коміту;
публікувати образ із мінімальними правами GitHub Token;
запускати production-деплой тільки з main;
зберігати secrets у GitHub Environment або на сервері;
виконувати prisma migrate deploy перед запуском нової версії;
використовувати SSH host fingerprint;
підтримувати ручне підтвердження production-деплою;
враховувати міграції під час відкату застосунку.