HostiServer
2026-09-30 11:19
Docker + CI/CD: автодеплой з GitHub Actions
📚 Серія «Docker на VPS», частина 4 з 5:
- Docker на VPS: встановлення і перший контейнер
- Docker Compose: кілька сервісів разом
- Docker у production
- Docker + CI/CD: автодеплой ← ви тут
- Gitea / Forgejo у Docker: повний self-hosted стек
Docker + CI/CD: автоматичний деплой
Стек із попередньої статті вже production-ready: Nginx, SSL, автозапуск після reboot. Але деплой нової версії досі виглядає так само, як і в самому першому пункті серії — змінили код, зробили git push, зайшли по SSH на сервер, вручну виконали docker compose build && docker compose up -d. Це монотонно, схильно до помилок і зовсім не масштабується, щойно деплоїти починає не одна людина.
CI/CD-pipeline знімає саме цей ручний крок. Кожен push у гілку main автоматично запускає послідовність дій: збірку образу, публікацію його в registry і оновлення сервісу на сервері — без входу по SSH вручну.
Ця стаття свідомо не прив'язана до конкретної CI-платформи. Концепцію CI/CD, різницю між Continuous Integration, Delivery і Deployment, а також структуру pipeline (тригер, stage, job, artifact) ми вже розбирали в окремій серії — почати варто з «Що таке CI/CD: від ручного deploy до автоматизованих pipelines». docker compose зі статті 2 і Nginx з SSL зі статті 3 цієї серії теж вважаються відомими. Тут — тільки та частина, яка специфічна саме для Docker: як образ потрапляє з CI на сервер і що саме змінюється в docker-compose.yml.
Результат наприкінці статті: git push у main — і за дві-три хвилини нова версія вже працює на сервері.
2. Docker image registry
2.1 Навіщо потрібен registry
CI збирає образ у своєму власному, ефемерному оточенні — цей образ там же і зникає після завершення job. Серверу потрібно звідкись його забрати. Registry — це проміжна ланка: CI публікує образ туди одразу після збірки, сервер пізніше тягне його звідти командою pull.
2.2 Варіанти registry
| Registry | Коли підходить |
|---|---|
| Docker Hub | Безкоштовно, є rate limit на анонімні pull-запити; достатньо для особистих і невеликих проєктів |
| GitHub Container Registry (ghcr.io) | Безкоштовно для публічних репозиторіїв, зручно, якщо CI вже налаштований на GitHub Actions |
| GitLab Registry / Gitea Registry | Вбудовані прямо в платформу — зручно для self-hosted сценаріїв без додаткового сервісу |
Self-hosted (registry:2) |
Повний контроль, без зовнішніх лімітів; потребує окремого сервера чи ресурсів того самого VPS |
2.3 Іменування образів і теги
Типовий формат — registry/namespace/app:tag. Як тег використовують або короткий git SHA (git rev-parse --short HEAD), або latest. SHA дає відтворюваність — завжди відомо, який саме коміт стоїть на сервері; latest зручний для швидкого доступу до найновішої версії. На практиці CI пушить обидва теги одночасно на один і той самий образ.
2.4 Приватний registry і авторизація
Якщо registry приватний, і CI, і сервер повинні авторизуватись перед роботою з ним — docker login з обліковими даними. Ці облікові дані передаються тільки через секрети CI-платформи, ніколи не прописуються прямо в pipeline-файлі чи в коді.
3. Зміни в docker-compose.yml
3.1 Як було раніше
До появи автодеплою сервіс app мав build: . — Compose сам збирав образ прямо на сервері з локального Dockerfile при кожному запуску.
3.2 Як стає з CI/CD
build: . замінюється на image: registry/namespace/app:latest. Compose на сервері більше нічого не збирає — тільки тягне вже готовий образ, який CI зібрав і опублікував.
3.3 Два compose-файли
Практичне рішення — тримати два файли: docker-compose.yml для production з image:, і docker-compose.override.yml (або docker-compose.dev.yml) для локальної розробки з build:. Обидва зберігаються в git — в .gitignore нічого додатково виключати не потрібно, а pipeline на сервері явно вказує використовувати саме production-файл.
3.4 Тег через змінну
Замість жорстко прописаного latest зручніше задати тег змінною: image: registry/namespace/app:${APP_VERSION:-latest}. Це дозволяє деплоїти конкретну версію, просто передавши значення APP_VERSION через .env або напряму з pipeline, не редагуючи сам compose-файл.
4. Pipeline: кроки і логіка
4.1 Тригер
Деплой запускається на push у гілку main (чи окрему production). Pull request-и і feature-гілки збираються і тестуються, як описано в CI/CD-серії, але не деплояться — це відповідає різниці між Continuous Integration і Continuous Deployment.
4.2 Загальна послідовність
- Checkout коду
- Авторизація в registry (
docker login) - Збірка образу:
docker build -t registry/namespace/app:sha . - Публікація образу в registry (тег
shaі тегlatest) - SSH-з'єднання із сервером
docker compose pull app— тягне новий образdocker compose up -d --no-deps app— перезапускає тількиapp
4.3 Чому --no-deps критично важливий
Без прапора --no-deps команда up -d перезапустить не тільки app, а й усі сервіси, від яких він залежить через depends_on — тобто і db, і cache. Це коротка, але зовсім не обов'язкова недоступність бази даних під час звичайного оновлення застосунку. При rolling-деплої нової версії потрібен перезапуск лише самого app.
4.4 SSH-доступ з CI
Для деплою заводиться окрема пара SSH-ключів, а не особистий ключ будь-кого з команди. Публічний ключ додається в ~/.ssh/authorized_keys на сервері, приватний зберігається виключно в секретах CI-платформи.
4.5 Альтернатива SSH
Замість прямого SSH-з'єднання можна підняти невеликий HTTP-сервіс на самому сервері — webhook, який приймає POST-запит від CI і всередині виконує pull та up -d. Такий підхід дає менше прав CI-платформі над сервером (немає прямого shell-доступу), але вимагає окремого налаштування і підтримки самого webhook-сервісу.
5. Секрети і безпека
5.1 Що зберігати в секретах CI
Мінімальний набір: REGISTRY_USER, REGISTRY_PASSWORD, SSH_PRIVATE_KEY, SERVER_HOST, SERVER_USER.
5.2 Чого не повинно бути в коді
Жодного з цих значень не повинно бути ні в самому коді, ні в Dockerfile, ні в docker-compose.yml, ні в git-історії — навіть у .env-файлах усередині репозиторію. У репозиторії має лежати лише .env.example з порожніми чи заглушковими значеннями, як орієнтир для того, які змінні взагалі потрібні.
5.3 Обмеження SSH-ключа деплою
Ключ для деплою варто робити окремим від усіх інших і, за можливості, обмежувати команду через директиву command= у authorized_keys на сервері — тоді навіть якщо ключ скомпрометований, ним не можна виконати довільну команду. Якщо компрометація все ж сталась, такий окремий ключ можна відкликати, не зачіпаючи доступ інших людей чи сервісів.
5.4 Registry credentials на сервері
docker login на сервері виконується один раз вручну, і облікові дані зберігаються локально в ~/.docker/config.json. Автоматичний повторний login у складі деплою потрібен лише тоді, коли облікові дані регулярно ротуються.
6. Версіонування і rollback
6.1 Чому одного latest недостатньо
Якщо деплой нової версії щось зламав, тег latest уже перезаписаний — на нього більше не можна спертись, щоб відкотитись назад. Потрібен інший, незмінний ідентифікатор конкретної версії.
6.2 SHA-тег як рятівний засіб
Кожен push зберігає в registry образ з тегом, який більше ніколи не перевикористовується — коротким git SHA цього коміту. Rollback у такому разі — це передати попередній SHA назад у pipeline або виконати вручну:
docker compose pull
APP_VERSION=abc1234 docker compose up -d
6.3 Зберігання старих образів
Registry не звільняє місце сам — варто налаштувати політику зберігання останніх N образів (через вбудовані retention policies реєстру або окремий cron-скрипт очищення). Це особливо актуально там, де storage registry тарифікується — необмежена історія тегів поступово стає платним баластом.
6.4 Rollback у самому pipeline
Найзручніший варіант — окремий job у CI, який запускається вручну з параметром SHA чи версії і виконує ті самі кроки деплою, тільки зі старим тегом. Без такого job'а rollback перетворюється назад на ручний SSH-вхід — саме те, від чого автодеплой мав позбавити.
7. Zero-downtime деплой
7.1 Реалістичний рівень для більшості проєктів
docker compose up -d --no-deps app має короткий, зазвичай 1-2 секунди, проміжок між зупинкою старого контейнера і стартом нового. Для переважної більшості сайтів і внутрішніх сервісів це прийнятний рівень — користувач у гіршому випадку побачить одну повільну відповідь замість помітного простою.
7.2 Справжній zero-downtime
Повністю прибрати цей проміжок можна схемою blue/green: два паралельні контейнери (app_blue і app_green), і Nginx перемикає upstream на новий тільки після того, як він підтвердив готовність. Це помітно складніша інфраструктура, і виправдана вона там, де SLA вимагає доступності понад 99.9% — для типового self-hosted проєкту на одному VPS це, як правило, надлишково.
7.3 Страховка на випадок невдалого старту
restart: unless-stopped разом із healthcheck на сервісі app — мінімальна, але дієва страховка: якщо новий контейнер не піднявся (краш при старті, OOM через недостатній ліміт пам'яті), Docker перезапустить його сам, а Nginx поверне 502 лише на короткий час рестарту, а не назавжди.
8. Практичний приклад pipeline (псевдокод)
8.1 Абстрактна схема
Незалежно від конкретної платформи логіка pipeline виглядає однаково:
on: push to main
steps:
- checkout
- login to registry
- build image
- push image
- ssh deploy
ℹ️ Реалізація для конкретної платформи: покроковий приклад саме такого pipeline на GitHub Actions, GitLab CI та Gitea Actions розібраний у серії «CI/CD з нуля» — там же пояснено, чим синтаксис платформ відрізняється в деталях.
8.2 Змінні середовища pipeline
Назва образу і тег зазвичай виносяться в змінні: IMAGE_NAME, IMAGE_TAG. Сам SHA-коміту кожна платформа передає під своєю назвою — $GITHUB_SHA у GitHub Actions, $CI_COMMIT_SHORT_SHA у GitLab CI, свій аналог у Gitea Actions. Назви змінних різняться, концепція та сама.
8.3 Сама команда деплою
Останній крок — SSH на сервер і виконання команди оновлення. Це можна оформити або як одну складену команду прямо в pipeline-файлі, або винести в окремий скрипт deploy.sh у репозиторії. Другий варіант зручніший: скрипт можна протестувати локально до того, як він потрапить у production-pipeline, і його зміни видно в історії git так само, як і зміни коду.
9. Висновок
Тепер кожен git push у main означає оновлену версію на сервері за дві-три хвилини, без ручного входу по SSH і без ризику забути один із кроків.
Повна схема виглядає так: код у git → CI збирає образ і публікує його в registry → сервер тягне готовий образ і перезапускає лише сервіс застосунку → Nginx, як і раніше, роздає трафік, навіть не помічаючи, що позаду відбулось оновлення.
Деталі налаштування самого pipeline під конкретну платформу — GitHub Actions, GitLab CI чи Gitea Actions — виходять за межі цієї статті: вони докладно розібрані в окремій серії «CI/CD з нуля». Тут ми пройшли тільки Docker-специфічну частину пазла: registry, зміни в compose-файлі, SSH-деплой, версіонування і rollback.
Повний цикл — від коду до контейнера на сервері — тепер зібраний. Останній крок серії — привести на той самий сервер і сам Git: наступна стаття замикає серію Gitea чи Forgejo, піднятими в Docker поруч із CI-runner'ом, у повністю self-hosted стек.
📚 Навігація серії:
Ви читаєте частину 4 з 5 «Docker + CI/CD: автоматичний деплой».
Попередня: Частина 3. Docker у production ←
Далі: Частина 5. Gitea / Forgejo у Docker: повний self-hosted стек →
🚀 Сервер, готовий приймати деплой з CI щодня
Автодеплой означає регулярні pull і перезапуски контейнерів — і сервер має витримувати це без просідань для тих, хто в цей момент користується застосунком. Hostiserver дає для цього передбачувані ресурси.
🖥️ Виділені Сервери
- Від $90/міс, повний контроль над залізом для частих production-деплоїв
- Без сусідів по ресурсах: короткий момент перезапуску контейнера не залежить від чужого навантаження
- 24/7 підтримка: допоможемо з SSH-доступом для CI та налаштуванням self-hosted registry
💻 Cloud (VPS) Хостинг
- Від $19.95/міс, KVM-ізоляція, виділені vCPU і RAM
- NVMe-диски: швидкий pull нових образів без затримок на диску
- Стабільна мережа: SSH-деплой з CI не залежить від нестабільного каналу
- Легко масштабувати: більше ресурсів, коли деплоїв стає більше і вони частіші
💬 Не впевнені, який варіант потрібен?
💬 Напишіть нам — допоможемо з вибором!
Часті запитання
- Docker Hub чи self-hosted registry — що обрати для невеликого проєкту?
Для одного-двох приватних проєктів Docker Hub чи вбудований registry GitHub/GitLab/Gitea (залежно від того, де вже живе код) — найпростіший старт без додаткової інфраструктури. Self-hosted
registry:2має сенс, коли лімітів безкоштовних варіантів вже не вистачає або є вимога тримати все у власному периметрі.
- Чому не можна деплоїти тільки за тегом latest?
Тег
latestперезаписується кожним новим пушем, тому він не годиться як точка для відкату — попередній стан просто зникає. Паралельний тег з git SHA лишається незмінним назавжди, і саме він дає можливість повернутись до конкретної версії, а не тільки рухатись вперед.
- Навіщо обов'язково --no-deps при оновленні app?
Без цього прапора
docker compose up -dперезапустить усі сервіси, пов'язані черезdepends_on, включно з базою даних і кешем — навіть якщо змінився тільки код застосунку.--no-depsобмежує перезапуск лише тим сервісом, який дійсно оновився.
- Як безпечно зберігати SSH-ключ для CI?
Окремий ключ, який використовується тільки для деплою і ніде більше, з приватною частиною виключно в секретах CI-платформи (ніколи в репозиторії). На сервері варто обмежити, що саме цей ключ може виконати, директивою
command=уauthorized_keys— тоді компрометація ключа не дає зловмиснику довільний shell на сервері.
- SSH-деплой чи webhook — з чого простіше почати?
SSH простіше налаштувати одразу: пара ключів і кілька команд у pipeline. Webhook вимагає підняти і підтримувати окремий невеликий сервіс на сервері, зате дає CI менше прав — без прямого shell-доступу. Для одного проєкту на одному VPS SSH зазвичай достатньо.
- Новий контейнер після деплою одразу впав — pipeline вважається успішним?
Формально так —
docker compose up -dсам по собі лише запускає контейнер і не чекає, поки застосунок усередині дійсно стане готовим. Тому в pipeline варто додавати окрему перевірку після деплою: короткийcurlна health-ендпоінт застосунку з невеликою затримкою, а не покладатись тільки на код завершення команди.
- Чи потрібен zero-downtime (blue/green) для невеликого проєкту?
Здебільшого ні. Стандартний
--no-depsперезапуск дає перерву в одну-дві секунди, що прийнятно для більшості сайтів і внутрішніх сервісів. Blue/green виправданий, коли є формальна вимога SLA понад 99.9% або коли навіть короткий 502 неприпустимий — наприклад, для платіжного шлюзу з високим трафіком.