Пошук уроків, статей та іншого контенту
Побудуйте pipeline для перевірок, тестування, складання образу та автоматичної доставки Node.js-застосунку.
CI/CD — це автоматизований процес перевірки, складання та доставки застосунку.
CI (Continuous Integration) — кожна зміна перевіряється в ізольованому середовищі:
встановлюються залежності;
запускається статичний аналіз;
виконуються тести;
перевіряється складання застосунку або Docker-образу.
CD (Continuous Delivery/Deployment) — перевірена версія автоматично публікується та доставляється на сервер.
Для Node.js типовий pipeline може мати такі етапи:
Встановити Node.js.
Встановити залежності командою npm ci.
Запустити перевірку коду.
Запустити тести.
Скласти Docker-образ.
Опублікувати образ у Container Registry.
Підключитися до сервера та перезапустити застосунок.
У цьому уроці використаємо:
GitHub Actions як CI/CD-систему;
Docker для пакування застосунку;
GitHub Container Registry для зберігання образу;
віддалений сервер із Docker Compose для розгортання.
Pipeline має викликати стандартні npm-скрипти. Наприклад, package.json може містити такі команди:
{
"name": "node-ci-cd-example",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"start": "node src/server.js",
"lint": "eslint .",
"test": "node --test",
"check": "npm run lint && npm test"
},
"dependencies": {
"express": "^4.21.2"
},
"devDependencies": {
"eslint": "^9.17.0"
}
}Файл src/server.js:
import express from 'express';
const app = express();
const port = process.env.PORT || 3000;
app.get('/health', (_request, response) => {
response.json({ status: 'ok' });
});
app.get('/', (_request, response) => {
response.send('Node.js application is running');
});
app.listen(port, () => {
console.log(`Server is listening on port ${port}`);
});Для ESLint 9 створимо eslint.config.js:
export default [
{
files: ['**/*.js'],
ignores: ['node_modules/**'],
rules: {
'no-unused-vars': 'error',
'no-console': 'off',
semi: ['error', 'always'],
quotes: ['error', 'single']
}
}
];Перевірка може виконуватися локально:
npm ci
npm run checkКоманда npm ci використовує package-lock.json і встановлює саме ті версії залежностей, які зафіксовані в lock-файлі. Саме її зазвичай використовують у CI замість npm install.
Dockerfile описує, як скласти образ застосунку:
FROM node:22-alpine AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:22-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY src ./src
USER node
EXPOSE 3000
CMD ["node", "src/server.js"]У цьому прикладі:
використовується офіційний образ Node.js на базі Alpine Linux;
залежності встановлюються в окремих етапах;
у production-образ потрапляють лише production-залежності;
застосунок запускається від імені користувача node, а не root;
порт контейнера оголошено через EXPOSE 3000.
Для локальної перевірки:
docker build -t node-ci-cd-example:local .
docker run --rm -p 3000:3000 node-ci-cd-example:localПісля запуску endpoint перевірки доступності можна викликати так:
curl http://localhost:3000/healthОчікувана відповідь:
{"status":"ok"}GitHub Actions шукає workflow-файли в каталозі .github/workflows.
Створимо файл .github/workflows/ci-cd.yml:
name: CI/CD
on:
pull_request:
push:
branches:
- main
permissions:
contents: read
jobs:
quality:
name: Перевірка коду та тести
runs-on: ubuntu-latest
steps:
- name: Отримати код
uses: actions/checkout@v4
- name: Налаштувати Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Встановити залежності
run: npm ci
- name: Перевірити код
run: npm run lint
- name: Запустити тести
run: npm testЦей job запускається:
для кожного Pull Request;
для кожного push у гілку main.
cache: npmactions/setup-node може кешувати npm-кеш. Це не замінює npm ci, але зменшує час завантаження пакетів між запусками workflow.
Кешування не повинно бути єдиним механізмом відновлення залежностей. У pipeline все одно потрібно виконувати:
npm ciДля простого застосунку можна використовувати вбудований модуль node:test.
Наприклад, файл test/health.test.js:
import test from 'node:test';
import assert from 'node:assert/strict';
test('health endpoint має повертати статус ok', () => {
const response = {
status: 'ok'
};
assert.deepEqual(response, { status: 'ok' });
});Запуск:
npm testУ реальному застосунку тести зазвичай перевіряють окремі функції, сервіси або HTTP endpoint-и. Важливо, щоб тестова команда:
завершувалася кодом 0, якщо всі тести успішні;
завершувалася ненульовим кодом, якщо хоча б один тест не пройдено;
не вимагала ручного введення під час виконання.
CI-система орієнтується саме на код завершення команди.
Після успішної перевірки можна скласти образ і опублікувати його в GitHub Container Registry.
Додамо до workflow окремий job:
image:
name: Складання та публікація образу
runs-on: ubuntu-latest
needs: quality
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
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 }}
- name: Скласти та опублікувати образ
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
${{ steps.metadata.outputs.tags }}
ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
labels: ${{ steps.metadata.outputs.labels }}needs: qualityозначає, що job image почнеться лише після успішного завершення quality.
Умова:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'забороняє публікувати образ для Pull Request. Це важливо, оскільки Pull Request може походити з неперевіреної гілки або fork-репозиторію.
Для образу створюються два теги:
тег, сформований docker/metadata-action;
тег із SHA коміту:
sha-<commit-sha>Тег за SHA дає змогу однозначно визначити, з якого коміту було складено образ. На сервері краще розгортати конкретний immutable-тег, а не постійно змінний latest.
На сервері мають бути встановлені:
Docker;
Docker Compose v2;
доступ до GitHub Container Registry.
Створимо каталог застосунку:
mkdir -p /opt/node-ci-cd
cd /opt/node-ci-cdФайл compose.yml:
services:
app:
image: ${IMAGE_NAME}
restart: unless-stopped
ports:
- "3000:3000"
environment:
NODE_ENV: production
PORT: 3000На сервері також потрібно створити файл .env:
IMAGE_NAME=ghcr.io/owner/repository:sha-0000000000000000000000000000000000000000Значення IMAGE_NAME pipeline змінюватиме перед кожним розгортанням.
Якщо репозиторій або образ є приватним, Docker на сервері потрібно авторизувати в реєстрі. Для GitHub Container Registry використовується Personal Access Token із правом читання пакетів:
echo "$CR_PAT" | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdinТокен не потрібно зберігати у файлах репозиторію.
Додамо job deploy:
deploy:
name: Розгортання на сервер
runs-on: ubuntu-latest
needs: image
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
environment: production
steps:
- name: Розгорнути нову версію
uses: appleboy/ssh-action@v1.2.0
env:
IMAGE_NAME: ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
envs: IMAGE_NAME
script: |
set -e
cd /opt/node-ci-cd
printf 'IMAGE_NAME=%s\n' "$IMAGE_NAME" > .env
docker compose pull
docker compose up -d --remove-orphans
docker image prune -fПовний workflow матиме такий вигляд:
name: CI/CD
on:
pull_request:
push:
branches:
- main
permissions:
contents: read
concurrency:
group: production
cancel-in-progress: true
jobs:
quality:
name: Перевірка коду та тести
runs-on: ubuntu-latest
steps:
- name: Отримати код
uses: actions/checkout@v4
- name: Налаштувати Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Встановити залежності
run: npm ci
- name: Перевірити код
run: npm run lint
- name: Запустити тести
run: npm test
image:
name: Складання та публікація образу
runs-on: ubuntu-latest
needs: quality
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
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 }}
- name: Скласти та опублікувати образ
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
${{ steps.metadata.outputs.tags }}
ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
labels: ${{ steps.metadata.outputs.labels }}
deploy:
name: Розгортання на сервер
runs-on: ubuntu-latest
needs: image
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
environment: production
steps:
- name: Розгорнути нову версію
uses: appleboy/ssh-action@v1.2.0
env:
IMAGE_NAME: ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
envs: IMAGE_NAME
script: |
set -e
cd /opt/node-ci-cd
printf 'IMAGE_NAME=%s\n' "$IMAGE_NAME" > .env
docker compose pull
docker compose up -d --remove-orphans
docker image prune -fУ GitHub Actions відкрийте налаштування репозиторію та додайте Secrets для environment production:
DEPLOY_HOST — адреса сервера;
DEPLOY_USER — користувач для SSH;
DEPLOY_SSH_KEY — приватний SSH-ключ.
Приватний ключ:
не можна додавати до Git;
не можна виводити через echo у workflow;
потрібно обмежити відповідним користувачем і сервером;
бажано використовувати окремий ключ лише для deployment.
GITHUB_TOKEN створюється GitHub автоматично та використовується для публікації образу в цьому repository.
Розділення секретів за environment дає змогу:
мати різні credentials для staging і production;
додавати ручне підтвердження для production;
обмежувати, які job можуть використовувати секрети.
Для Pull Request pipeline виконує:
checkout
↓
npm ci
↓
lint
↓
testЯкщо перевірка не пройшла, Pull Request не повинен бути об'єднаний у main.
Після push у main виконується:
quality
↓
docker build
↓
docker push
↓
SSH deployment
↓
docker compose pull
↓
docker compose up -dЗавдяки needs нова версія не буде доставлена, якщо код або тести не пройшли перевірку.
Після завершення workflow на сервері можна перевірити стан контейнера:
cd /opt/node-ci-cd
docker compose ps
docker compose logs --tail=100 appHTTP-перевірка:
curl http://SERVER_HOST:3000/healthОчікувана відповідь:
{"status":"ok"}Для production-застосунку endpoint /health має перевіряти мінімально необхідну працездатність процесу. У цьому прикладі він лише підтверджує, що Node.js-сервер запущений і відповідає на запити.
Версія Node.js у локальному середовищі, CI та Docker-образі має бути узгодженою:
node-version: 22FROM node:22-alpineЦе зменшує кількість відмінностей між локальним запуском і production.
Тег на основі SHA:
sha-abc123...кращий для deployment, ніж latest, оскільки:
його значення не змінюється;
легко визначити конкретну версію;
можна повернути попередній тег;
простіше аналізувати, яка версія працює на сервері.
У deployment-скрипті:
set -eзупиняє виконання після першої команди з помилкою. Без цього наступні команди могли б виконуватися навіть після невдалого завантаження образу або іншої критичної помилки.
Налаштування:
concurrency:
group: production
cancel-in-progress: trueне дає кільком deployment-процесам одночасно змінювати production. Якщо з'явився новий commit, старий запуск, який ще не завершився, буде скасовано.
npm install у CInpm install може оновити залежності відповідно до правил semver. Для відтворюваних збірок використовуйте:
npm ciі додавайте package-lock.json до репозиторію.
package-lock.jsonБез lock-файлу npm ci завершиться помилкою. Створіть його локально:
npm installпісля чого додайте package-lock.json до Git.
Job складання повинен залежати від job перевірок:
needs: qualityІнакше несправний код може потрапити до реєстру та бути розгорнутим.
Для GitHub Container Registry використовується формат:
ghcr.io/OWNER/REPOSITORY:TAGНазва образу повинна бути коректним Docker image reference. Не використовуйте пробіли або довільні спеціальні символи.
Не записуйте SSH-ключі, токени реєстру чи паролі в:
compose.yml;
.env, який комітиться до Git;
Dockerfile;
workflow у відкритому вигляді.
Для цього призначені GitHub Secrets і захищені змінні environment.
localhostNode.js-сервер у контейнері має слухати всі інтерфейси. Виклик app.listen(port) без окремої host-адреси дозволяє Express слухати 0.0.0.0. Якщо сервер прив'язати лише до 127.0.0.1, порт контейнера може бути недоступним ззовні.
health endpointБез endpoint для перевірки складніше швидко визначити, чи працює новий контейнер. Простий /health може бути використаний для базової перевірки після deployment.
CI автоматично запускає встановлення залежностей, перевірки та тести.
Для CI використовуйте npm ci, а не npm install.
Dockerfile пакує Node.js-застосунок у відтворюваний образ.
Образ можна публікувати в GitHub Container Registry.
needs визначає порядок виконання job і не дає розгортати неперевірлений код.
Для production краще використовувати тег Docker-образу на основі SHA коміту.
SSH-ключі та інші чутливі дані потрібно зберігати в GitHub Secrets.
Автоматичний deployment може оновлювати .env на сервері, завантажувати новий образ і запускати його через Docker Compose.