Шаблон актуализирован по чек-листу безопасности. Несколько строк из оригинала убраны — там были советы ради удобства («порт не в секретах, лень каждый раз набирать», «root для homelab допустимо»), которые создают реальные дыры при копировании в prod. Универсальность сохранена: шаблон подходит для любого репозитория.
Что здесь важно до копирования YAML#
GitHub Actions для Docker — это часть цепочки поставки, а не просто удобная автоматизация. Через workflow проходят исходники, registry-токены, образ приложения и иногда доступ к серверу.
Поэтому шаблон нужно читать как инфраструктурный код: какие события запускают сборку, какие secrets доступны, куда публикуется image и что произойдёт, если кто-то изменит workflow в pull request.
Основной workflow#
name: Build and Push Docker Image
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
concurrency:
group: build-${{ github.ref }}
cancel-in-progress: true
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read # минимум для checkout
packages: write # нужен для push в GHCR
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0 # gitleaks сканирует всю историю
- name: Secret scan
uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Login to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker metadata
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest,enable=${{ github.ref_name == 'main' || github.ref_name == 'master' }}
type=ref,event=branch,enable=${{ github.ref_name != 'main' && github.ref_name != 'master' }}
type=sha,format=short
labels: |
org.opencontainers.image.vendor=WebZaytsev
org.opencontainers.image.version={{version}}
org.opencontainers.image.created={{date 'YYYY-MM-DD HH:mm:ss'}}
org.opencontainers.image.revision={{sha}}
- name: Build image (local, no push yet)
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
push: false
load: true
tags: local:scan
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Scan image (Trivy)
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: local:scan
exit-code: '1'
severity: HIGH,CRITICAL
scanners: vuln,misconfig,secret
ignore-unfixed: true
- name: Push to GHCR
if: github.event_name != 'pull_request'
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
provenance: true
Что изменилось по сравнению с прошлой версией — по пунктам.
Триггеры. Было '**' — сборка запускалась на каждый push в любую ветку. Теперь только main/master плюс pull request к ним. pull_request важен: без этого триггера CI не проверяет код перед мёрджем вообще, и утверждение «push не срабатывает на PR» из старой версии было ошибочным — оно не срабатывало, потому что PR просто не триггерил workflow.
Concurrency. Параллельные push-и в одну ветку отменяют предыдущий прогон. Без этого два деплоя могут стартовать одновременно и оставить сервер в непредсказуемом состоянии.
Permissions. Явный минимум прав на уровне job. Не указывать permissions — значит наследовать широкий дефолт репозитория.
SHA-пины. Все actions зафиксированы по SHA коммита, а не по плавающему тегу вроде @v4. Плавающий тег при переназначении — это supply-chain-атака: кто-то обновляет action, и ваш пайплайн выполняет уже другой код. Обновлять SHA автоматически — через Dependabot (сниппет ниже).
Секреты — не в
build-args. Если передать секрет через--build-arg, он осядет в слоях образа и будет виден вdocker history. Секреты нужны только в рантайме: через environment variables или BuildKit--mount=type=secret.
Scan-before-push. Образ сначала собирается локально (push: false, load: true), проходит Trivy (уязвимости, мисконфигурации, секреты), и только после этого пушится. Уязвимые образы в GHCR не попадают. Второй шаг build-push-action использует GHA-кэш и не пересобирает образ — просто переупаковывает с нужными тегами и отправляет.
ignore-unfixed: true — не падаем на CVE, у которых ещё нет патча. Без этого флага alpine/debian-образы нередко блокируют сборку из-за уязвимостей в системных пакетах, для которых апстрим ещё не выпустил фикс.
Автодеплой#
Рекомендуемый способ: pull-based#
CI пушит образ — и всё. Сервер сам подтягивает обновление по расписанию или через Watchtower. Никакого SSH-ключа в GitHub Secrets, никакого вектора «через CI зашли на хост».
# cron на сервере — каждые 5 минут проверяет обновление
*/5 * * * * cd /opt/myapp && docker compose pull && docker compose up -d --remove-orphans
Или Watchtower с webhook-уведомлениями — запускается рядом в том же compose и перезапускает контейнеры при появлении нового тега.
Деплой-пользователь на хосте — без sudo, без прямого доступа к docker.sock. Команды только docker compose pull и up -d.
Если без SSH не обойтись#
Если pull-based не вписывается в вашу инфраструктуру — вот hardened-вариант. Нужно понимать: компрометация workflow или утечка secrets → компрометация сервера. Это осознанный выбор, а не случайность.
deploy:
needs: build-and-push
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
environment: production # опционально: protection rules + ручной approve для прода
permissions:
contents: read
steps:
- name: Deploy
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
port: ${{ secrets.SERVER_PORT }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
fingerprint: ${{ secrets.SSH_HOST_FINGERPRINT }}
script: |
set -e
cd ${{ secrets.SERVER_PATH }}
docker compose pull
docker compose up -d --remove-orphans
fingerprint — SHA256-отпечаток публичного ключа хоста, защита от MITM. Получить: ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key.pub | cut -d' ' -f2. Без него action не проверяет, что подключается именно к вашему серверу.
set -e первой строкой в скрипте: прерывает выполнение при любой ошибке. Опция script_stop убрана в новых версиях action — эквивалент теперь именно set -e в самом скрипте.
Все параметры подключения (host, username, port, path) — только в secrets. Один раз завести и не трогать проще, чем разбираться с инцидентом.
Hardening compose на сервере#
Это уже не CI, но пока о деплое: вот минимальный набор ограничений для каждого сервиса в compose.
services:
myapp:
image: ghcr.io/org/myapp:latest
user: "1000:1000"
read_only: true
tmpfs:
- /tmp:uid=1000,gid=1000
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
mem_limit: 256m
pids_limit: 256
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 30s
retries: 3
У tmpfs при non-root не забудьте uid/gid — иначе каталог создастся от root, и процесс под user: не сможет туда писать.
Dependabot для actions#
Чтобы SHA-пины обновлялись автоматически, добавьте .github/dependabot.yml:
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
Dependabot будет открывать pull request'ы с обновлёнными SHA и тегами — остаётся смотреть changelog и мёрджить.
Что проверить после копипасты#
- Все secrets заведены в настройках репозитория:
SERVER_HOST,SERVER_USER,SERVER_PORT,SSH_PRIVATE_KEY,SSH_HOST_FINGERPRINT,SERVER_PATH. .github/dependabot.ymlдобавлен, Dependabot включён в настройках репозитория.- Trivy не в warn-only:
exit-codeстоит'1', а не'0'. - Deploy-пользователь на сервере работает без
sudoи без прямого доступа кdocker.sock.
Если вам нужен воспроизводимый CI/CD-пайплайн для Docker-сервисов, одной сборки образа мало. Напишите мне в Telegram или на почту — помогу собрать GitHub Actions-процесс с нормальными секретами, deploy-правами, сканированием образов и понятным rollback, чтобы его можно было безопасно использовать в реальном проекте, а не только в демо.



