From 98b162f2b8f2077e9e2266fabe45c991a0cc0d09 Mon Sep 17 00:00:00 2001 From: "S.Gromov" Date: Mon, 4 May 2026 12:51:28 +0300 Subject: [PATCH] =?UTF-8?q?chore:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20CI/CD=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B4-=D0=B4=D0=B5=D0=BF=D0=BB=D0=BE=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - добавлены Gitea workflows для CI и ручного CD - настроен prod compose для запуска за reverse proxy - добавлена публикация Caddy image в Container Registry - обновлена документация по CI/CD и prod-сети --- .env.example | 6 +- .gitea/workflows/ci.yml | 75 +++++++++++++++++++++ .gitea/workflows/deploy.yml | 64 ++++++++++++++++++ docker-compose.yml | 13 +++- docs/MAP.md | 1 + docs/ci-cd.md | 130 ++++++++++++++++++++++++++++++++++++ docs/dev-guide.md | 15 ++++- docs/index.md | 2 + 8 files changed, 301 insertions(+), 5 deletions(-) create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitea/workflows/deploy.yml create mode 100644 docs/ci-cd.md diff --git a/.env.example b/.env.example index 7d89125..3504446 100644 --- a/.env.example +++ b/.env.example @@ -22,5 +22,9 @@ IMGPROXY_DOWNLOAD_TIMEOUT=30 # === Caddy === # Домен для HTTPS (пустое значение = localhost без HTTPS) DOMAIN= -# Порт Caddy для локальной разработки +# Порт Caddy для локальной разработки. В prod за reverse proxy обычно 80. CADDY_PORT=8888 +# Docker image для CD-деплоя. Локально можно не задавать. +# CADDY_IMAGE=registry.example.com/owner/image-gateway:latest +# Docker network внешнего reverse proxy в prod. +# WEB_NETWORK=web diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..0327423 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,75 @@ +name: CI + +on: + push: + branches: [main, master, dev] + pull_request: + branches: [main, master, dev] + workflow_dispatch: + +jobs: + e2e: + runs-on: ubuntu-latest + if: github.event_name != 'push' || !contains(github.event.head_commit.message, '[skip ci]') + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Проверка Docker Compose конфигурации + run: docker compose -f docker-compose.test.yml config + + - name: E2E тесты кеша + run: ./scripts/test-e2e.sh + + docker: + runs-on: ubuntu-latest + needs: e2e + if: > + github.event_name == 'push' && + (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') && + !contains(github.event.head_commit.message, '[skip ci]') + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Setup variables + run: | + DOCKER_REGISTRY=$(echo "${{ gitea.server_url }}" | sed -E 's|^https?://||') + echo "DOCKER_REGISTRY=$DOCKER_REGISTRY" >> "$GITHUB_ENV" + REGISTRY_IMAGE="$DOCKER_REGISTRY/$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')" + echo "REGISTRY_IMAGE=$REGISTRY_IMAGE" >> "$GITHUB_ENV" + + - name: Login to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.CR_USER }} + password: ${{ secrets.CR_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY_IMAGE }} + tags: | + type=ref,event=branch + type=sha,prefix= + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Caddy image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile.caddy + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + provenance: false + sbom: false diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..42bd681 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,64 @@ +name: CD + +on: + workflow_dispatch: + +env: + DEPLOY_HOST: 188.225.47.78 + DEPLOY_PATH: /opt/image-gateway + DEPLOY_USER: root + IMAGE_TAG: latest + WEB_NETWORK: web + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup variables + run: | + DOCKER_REGISTRY=$(echo "${{ gitea.server_url }}" | sed -E 's|^https?://||') + echo "DOCKER_REGISTRY=$DOCKER_REGISTRY" >> "$GITHUB_ENV" + REGISTRY_IMAGE="$DOCKER_REGISTRY/$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')" + echo "REGISTRY_IMAGE=$REGISTRY_IMAGE" >> "$GITHUB_ENV" + + - name: Настройка SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan -H "${{ env.DEPLOY_HOST }}" >> ~/.ssh/known_hosts + + - name: Подготовка директории деплоя + run: | + ssh -i ~/.ssh/deploy_key "${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }}" \ + "mkdir -p '${{ env.DEPLOY_PATH }}'" + + - name: Копирование compose-конфигурации + run: | + scp -i ~/.ssh/deploy_key docker-compose.yml Caddyfile \ + "${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }}:${{ env.DEPLOY_PATH }}/" + + - name: Ручной деплой + run: | + ssh -i ~/.ssh/deploy_key "${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }}" bash -s <<'SCRIPT' + set -euo pipefail + + cd '${{ env.DEPLOY_PATH }}' + + IMAGE='${{ env.REGISTRY_IMAGE }}:${{ env.IMAGE_TAG }}' + WEB_NETWORK='${{ env.WEB_NETWORK }}' + + echo '${{ secrets.CR_TOKEN }}' | docker login '${{ env.DOCKER_REGISTRY }}' \ + -u '${{ secrets.CR_USER }}' --password-stdin + + docker network inspect "$WEB_NETWORK" >/dev/null 2>&1 || docker network create "$WEB_NETWORK" + + WEB_NETWORK="$WEB_NETWORK" CADDY_IMAGE="$IMAGE" docker compose -f docker-compose.yml pull caddy imgproxy + WEB_NETWORK="$WEB_NETWORK" CADDY_IMAGE="$IMAGE" docker compose -f docker-compose.yml up -d --no-build + WEB_NETWORK="$WEB_NETWORK" CADDY_IMAGE="$IMAGE" docker compose -f docker-compose.yml ps + + docker image prune -f + SCRIPT diff --git a/docker-compose.yml b/docker-compose.yml index fc798cf..7fde650 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,12 +15,13 @@ services: - gateway caddy: + image: ${CADDY_IMAGE:-image-gateway-caddy:local} build: context: . dockerfile: Dockerfile.caddy restart: unless-stopped - ports: - - "${CADDY_PORT:-80}:${CADDY_PORT:-80}" + expose: + - "${CADDY_PORT:-80}" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy-data:/data @@ -35,11 +36,17 @@ services: imgproxy: condition: service_started networks: - - gateway + gateway: + web: + aliases: + - image-gateway networks: gateway: driver: bridge + web: + external: true + name: ${WEB_NETWORK:-web} volumes: caddy-data: diff --git a/docs/MAP.md b/docs/MAP.md index d4865b5..b40f346 100644 --- a/docs/MAP.md +++ b/docs/MAP.md @@ -10,5 +10,6 @@ docs/ ├── url-reference.md Формат URL, параметры обработки, примеры ├── cache-api.md Purge кеша, Souin API ├── e2e-tests.md Автоматические Docker Compose e2e-тесты +├── ci-cd.md Gitea Actions CI/CD и ручной деплой └── testing-checklist.md Чек-лист ручного тестирования ``` diff --git a/docs/ci-cd.md b/docs/ci-cd.md new file mode 100644 index 0000000..4afe787 --- /dev/null +++ b/docs/ci-cd.md @@ -0,0 +1,130 @@ +# CI/CD + +Проект использует Gitea Actions workflows в `.gitea/workflows`. + +## Прод Схема + +В проде проект запускается через Docker Compose, а не через один общий Dockerfile. + +Причины: + +- `caddy` и `imgproxy` — разные runtime-сервисы; +- для Caddy нужен кастомный image с Souin, Otter и NutsDB modules; +- `imgproxy` берется как готовый upstream image `darthsim/imgproxy`; +- кешу нужны persistent volumes `caddy-data` и `caddy-cache`; +- Compose проще обновляет только Caddy image без пересборки на сервере; +- Image Gateway должен быть доступен только из сети внешнего reverse proxy. + +В registry публикуется только кастомный Caddy image из `Dockerfile.caddy`. На сервере `docker-compose.yml` подставляет его через переменную `CADDY_IMAGE`. + +Prod compose не публикует host-порты. Caddy подключается к external network `web` с alias `image-gateway`, поэтому внешний reverse proxy должен проксировать на `http://image-gateway:80` при `CADDY_PORT=80`. + +## CI + +Файл: `.gitea/workflows/ci.yml`. + +Запускается на: + +- push в `main`, `master`, `dev`; +- pull request в `main`, `master`, `dev`; +- ручной запуск через `workflow_dispatch`. + +Что делает: + +- проверяет `docker-compose.test.yml` через `docker compose config`; +- запускает e2e-тесты кеша через `./scripts/test-e2e.sh`; +- на основной ветке собирает и публикует Caddy image в Gitea Container Registry. + +Публикуемые теги: + +- branch tag, например `master` или `main`; +- commit SHA; +- `latest` для default branch. + +## CD + +Файл: `.gitea/workflows/deploy.yml`. + +Запускается вручную через `workflow_dispatch`. + +Что делает: + +- вычисляет registry image текущего репозитория; +- подключается к серверу по SSH; +- копирует `docker-compose.yml` и `Caddyfile` в директорию деплоя; +- логинится в Gitea Container Registry; +- тянет `CADDY_IMAGE=/:latest`; +- создает Docker network внешнего reverse proxy, если ее еще нет; +- запускает `docker compose up -d --no-build`. + +Сборка на прод-сервере не выполняется. + +## Secrets + +В Gitea repository secrets должны быть заданы: + +| Secret | Назначение | +|---|---| +| `CR_USER` | пользователь Container Registry | +| `CR_TOKEN` | токен Container Registry | +| `SSH_PRIVATE_KEY` | приватный ключ для деплоя | + +CD workflow использует те же registry и SSH secrets, что и другие проекты. + +Настройки целевого сервера заданы в `env` файла `.gitea/workflows/deploy.yml`: + +| Переменная | Значение по умолчанию | Назначение | +|---|---|---| +| `DEPLOY_HOST` | `188.225.47.78` | host/IP прод-сервера | +| `DEPLOY_USER` | `root` | SSH-пользователь | +| `DEPLOY_PATH` | `/opt/image-gateway` | директория проекта на сервере | +| `IMAGE_TAG` | `latest` | тег Caddy image для деплоя | +| `WEB_NETWORK` | `web` | Docker network внешнего reverse proxy | + +На прод-сервере в `DEPLOY_PATH` должен лежать `.env` с runtime-настройками: + +```env +DOMAIN=images.example.com +CADDY_PORT=80 +WEB_NETWORK=web +IMGPROXY_KEY= +IMGPROXY_SALT= +IMGPROXY_ALLOWED_SOURCES=example.com,cdn.example.com +``` + +CD workflow не копирует `.env`, чтобы не перетирать секреты и runtime-настройки на сервере. + +## Ручной Деплой На Сервере + +Если нужно повторить деплой вручную на сервере: + +```bash +cd /path/to/image-gateway +docker login registry.example.com +CADDY_IMAGE=registry.example.com/owner/image-gateway:latest docker compose -f docker-compose.yml pull caddy imgproxy +CADDY_IMAGE=registry.example.com/owner/image-gateway:latest docker compose -f docker-compose.yml up -d --no-build +``` + +## Reverse Proxy + +Внешний reverse proxy должен быть подключен к той же Docker network, что и Image Gateway (`WEB_NETWORK`, по умолчанию `web`). + +Внутренний upstream при `CADDY_PORT=80`: + +```text +http://image-gateway:80 +``` + +Прокидывайте исходный `Host`, чтобы Souin cache key не дробился по внутренним именам: + +```text +Host: images.example.com +X-Forwarded-Proto: https +X-Forwarded-For: +``` + +Admin API `:2019` не должен публиковаться наружу. + +## Skip CI + +Для пропуска CI добавьте `[skip ci]` в сообщение коммита. diff --git a/docs/dev-guide.md b/docs/dev-guide.md index 101ac92..494d6fc 100644 --- a/docs/dev-guide.md +++ b/docs/dev-guide.md @@ -22,7 +22,18 @@ docker compose -f docker-compose.dev.yml logs -f imgproxy ### Production (`docker-compose.yml`) -Bridge-сеть, порт `80` (или `443` с HTTPS). Порт 2019 **не пробрасывается** — доступен только внутри Docker сети. +Production compose рассчитан на запуск за внешним reverse proxy. + +- host-порты не публикуются; +- `caddy` подключается к external Docker network `web`; +- reverse proxy должен ходить на `http://image-gateway:80` внутри сети `web`; +- порт `2019` не пробрасывается наружу и доступен только внутри Docker networks. + +Перед первым запуском на сервере создайте сеть, если ее еще нет: + +```bash +docker network create web +``` ```bash docker compose up -d --build @@ -49,6 +60,8 @@ docker compose up -d --build |---|---|---| | `CADDY_PORT` | Порт Caddy | `8888` (dev) / `80` (prod) | | `DOMAIN` | Домен для HTTPS (пусто = localhost, prod only) | — | +| `CADDY_IMAGE` | Готовый Caddy image для CD-деплоя | `image-gateway-caddy:local` | +| `WEB_NETWORK` | External Docker network внешнего reverse proxy | `web` | ### Сеть (опционально) diff --git a/docs/index.md b/docs/index.md index 65d119f..44ece3c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,6 +12,7 @@ Self-hosted прокси-сервер для обработки и кеширо | [URL-справочник](url-reference.md) | Формат URL, параметры обработки, примеры | | [API кеша](cache-api.md) | Purge кеша, Souin API | | [E2E тесты](e2e-tests.md) | Автоматические Docker Compose тесты кеша | +| [CI/CD](ci-cd.md) | Gitea Actions, сборка Caddy image и ручной деплой | | [Чек-лист тестирования](testing-checklist.md) | Пошаговая ручная проверка всех функций | ## Быстрая навигация @@ -21,5 +22,6 @@ Self-hosted прокси-сервер для обработки и кеширо - Хочу **узнать формат URL** → [url-reference.md](url-reference.md) - Хочу **сбросить кеш** → [cache-api.md](cache-api.md) - Хочу **запустить автотесты** → [e2e-tests.md](e2e-tests.md) +- Хочу **настроить CI/CD** → [ci-cd.md](ci-cd.md) - Хочу **протестировать вручную** → [testing-checklist.md](testing-checklist.md) - Хочу **понять что это за проект** → [overview.md](overview.md)