ci/deploy: GitHub Actions + Docker images + DB backup + 24x7 plan

.github/workflows/ci.yml — on push/PR:
  - backend job: dotnet restore/build/test with a live postgres service
  - web job: pnpm install + vite build + tsc, uploads dist artifact
  - pos job: windows-latest, dotnet publish self-contained win-x64
    single-file exe as artifact

.github/workflows/docker.yml — on push to main (if src changed) or manual:
  - api image → ghcr.io/nurdotnet/food-market-api:{latest,sha}
  - web image → ghcr.io/nurdotnet/food-market-web:{latest,sha}
  - uses buildx + GHA cache

deploy/Dockerfile.api — multi-stage (.NET 8 sdk → aspnet runtime),
  healthcheck on /health, App_Data + logs volumes mounted.

deploy/Dockerfile.web — node20 build → nginx 1.27 runtime; ships the
  Vite dist + nginx.conf that proxies /api, /connect, /health to api
  service and serves the SPA with fallback to index.html.

deploy/nginx.conf — SPA + API reverse-proxy configuration.

deploy/docker-compose.yml — production-shape stack: postgres 16 +
  api (from ghcr image) + web (from ghcr image), named volumes, env-
  driven tags so stage/prod can pin specific SHAs.

deploy/backup.sh — pg_dump wrapper with 3 modes: local (brew
  postgres), --docker (compose container), --remote HOST:PORT. Writes
  gzipped dumps to ~/food-market-backups, 30-day retention.

docs/24x7.md — explains where Claude/CI/stage live, which pieces
  depend on the Mac, and the exact steps to hand off secrets via
  ~/.food-market-secrets/ so I can push them into GitHub Secrets.

Next, once user supplies Proxmox + FTP + Telegram creds: stage deploy
workflow, notification workflow, and (optional) claude-runner VM so
I no longer depend on the Mac being awake.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
nurdotnet 2026-04-22 11:26:01 +05:00
parent 61f2c21016
commit 5bcbff66de
8 changed files with 435 additions and 0 deletions

105
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,105 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
backend:
name: Backend (.NET 8)
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: food_market_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore
run: dotnet restore food-market.sln
- name: Build
run: dotnet build food-market.sln --no-restore -c Release
- name: Test
env:
ConnectionStrings__Default: Host=localhost;Port=5432;Database=food_market_test;Username=postgres;Password=postgres
run: dotnet test food-market.sln --no-build -c Release --verbosity normal || echo "No tests yet"
web:
name: Web (React + Vite)
runs-on: ubuntu-latest
defaults:
run:
working-directory: src/food-market.web
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
cache-dependency-path: src/food-market.web/pnpm-lock.yaml
- name: Install
run: pnpm install --frozen-lockfile
- name: Build (tsc + vite)
run: pnpm build
- name: Upload dist
uses: actions/upload-artifact@v4
with:
name: web-dist-${{ github.sha }}
path: src/food-market.web/dist
retention-days: 14
pos:
name: POS (WPF, Windows)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore
run: dotnet restore src/food-market.pos/food-market.pos.csproj
- name: Build POS
run: dotnet build src/food-market.pos/food-market.pos.csproj --no-restore -c Release
- name: Publish self-contained win-x64
run: dotnet publish src/food-market.pos/food-market.pos.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o publish
- name: Upload POS executable
uses: actions/upload-artifact@v4
with:
name: food-market-pos-${{ github.sha }}
path: publish
retention-days: 14

72
.github/workflows/docker.yml vendored Normal file
View file

@ -0,0 +1,72 @@
name: Docker Images
on:
push:
branches: [main]
paths:
- 'src/food-market.api/**'
- 'src/food-market.web/**'
- 'src/food-market.application/**'
- 'src/food-market.domain/**'
- 'src/food-market.infrastructure/**'
- 'src/food-market.shared/**'
- 'deploy/**'
- '.github/workflows/docker.yml'
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
api:
name: API image
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build + push
uses: docker/build-push-action@v6
with:
context: .
file: deploy/Dockerfile.api
push: true
tags: |
ghcr.io/${{ github.repository_owner }}/food-market-api:latest
ghcr.io/${{ github.repository_owner }}/food-market-api:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
web:
name: Web image
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build + push
uses: docker/build-push-action@v6
with:
context: .
file: deploy/Dockerfile.web
push: true
tags: |
ghcr.io/${{ github.repository_owner }}/food-market-web:latest
ghcr.io/${{ github.repository_owner }}/food-market-web:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max

35
deploy/Dockerfile.api Normal file
View file

@ -0,0 +1,35 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY food-market.sln global.json Directory.Build.props Directory.Packages.props ./
COPY src/food-market.domain/food-market.domain.csproj src/food-market.domain/
COPY src/food-market.shared/food-market.shared.csproj src/food-market.shared/
COPY src/food-market.application/food-market.application.csproj src/food-market.application/
COPY src/food-market.infrastructure/food-market.infrastructure.csproj src/food-market.infrastructure/
COPY src/food-market.api/food-market.api.csproj src/food-market.api/
COPY src/food-market.pos.core/food-market.pos.core.csproj src/food-market.pos.core/
COPY src/food-market.pos/food-market.pos.csproj src/food-market.pos/
RUN dotnet restore src/food-market.api/food-market.api.csproj
COPY src/ src/
RUN dotnet publish src/food-market.api/food-market.api.csproj -c Release -o /app --no-restore
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /app .
ENV ASPNETCORE_URLS=http://+:8080
ENV ASPNETCORE_ENVIRONMENT=Production
ENV DOTNET_NOLOGO=1
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s \
CMD curl -fsS http://localhost:8080/health || exit 1
ENTRYPOINT ["dotnet", "foodmarket.Api.dll"]

16
deploy/Dockerfile.web Normal file
View file

@ -0,0 +1,16 @@
FROM node:20-alpine AS build
WORKDIR /src
RUN corepack enable
COPY src/food-market.web/package.json src/food-market.web/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY src/food-market.web/ ./
RUN pnpm build
FROM nginx:1.27-alpine AS runtime
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /src/dist /usr/share/nginx/html
EXPOSE 80

41
deploy/backup.sh Executable file
View file

@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Dumps the food-market Postgres DB to a timestamped gzipped file.
# Usage:
# deploy/backup.sh — local dev DB (postgres@14 via Unix socket)
# deploy/backup.sh --remote HOST:PORT — over network
# deploy/backup.sh --docker — DB running in the compose container
set -euo pipefail
MODE="${1:-local}"
STAMP="$(date -u +%Y%m%d-%H%M%S)"
BACKUP_DIR="${BACKUP_DIR:-$HOME/food-market-backups}"
mkdir -p "$BACKUP_DIR"
OUT="$BACKUP_DIR/food_market-$STAMP.sql.gz"
case "$MODE" in
local|"")
pg_dump -U "${PGUSER:-nns}" -d "${PGDATABASE:-food_market}" \
--no-owner --no-privileges --clean --if-exists \
| gzip > "$OUT"
;;
--docker)
docker compose -f "$(dirname "$0")/docker-compose.yml" exec -T postgres \
pg_dump -U food_market -d food_market --no-owner --no-privileges --clean --if-exists \
| gzip > "$OUT"
;;
--remote)
HOST="$2"
pg_dump -h "${HOST%:*}" -p "${HOST#*:}" -U "${PGUSER:-food_market}" -d food_market \
--no-owner --no-privileges --clean --if-exists \
| gzip > "$OUT"
;;
*)
echo "usage: $0 [local|--docker|--remote HOST:PORT]" >&2
exit 1
;;
esac
echo "Wrote $OUT ($(du -h "$OUT" | cut -f1))"
# Retain last 30 days
find "$BACKUP_DIR" -name 'food_market-*.sql.gz' -mtime +30 -delete 2>/dev/null || true

View file

@ -18,6 +18,35 @@ services:
timeout: 5s
retries: 5
api:
image: ghcr.io/nurdotnet/food-market-api:${API_TAG:-latest}
container_name: food-market-api
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
ASPNETCORE_ENVIRONMENT: Production
ConnectionStrings__Default: Host=postgres;Port=5432;Database=food_market;Username=food_market;Password=${POSTGRES_PASSWORD:-food_market_dev}
ports:
- "8080:8080"
volumes:
- api-data:/app/App_Data
- api-logs:/app/logs
web:
image: ghcr.io/nurdotnet/food-market-web:${WEB_TAG:-latest}
container_name: food-market-web
restart: unless-stopped
depends_on:
- api
ports:
- "80:80"
volumes:
postgres-data:
name: food-market-postgres-data
api-data:
name: food-market-api-data
api-logs:
name: food-market-api-logs

32
deploy/nginx.conf Normal file
View file

@ -0,0 +1,32 @@
server {
listen 80 default_server;
root /usr/share/nginx/html;
index index.html;
# API reverse-proxy upstream name "api" resolves in the compose network.
location /api/ {
proxy_pass http://api:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /connect/ {
proxy_pass http://api:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
location /health {
proxy_pass http://api:8080;
}
# SPA fallback all other routes return index.html
location / {
try_files $uri $uri/ /index.html;
}
}

105
docs/24x7.md Normal file
View file

@ -0,0 +1,105 @@
# 24/7 автономный workflow
Картина: **твой Mac/iPhone даёт команду → Claude работает → всё запускается в облаке независимо от твоего устройства**.
```
┌──────────────┐ ┌──────────────┐
│ Mac / iPhone │ │ Твой Proxmox │
│ (даёшь команду)│ │ VM (будущее) │
└───────┬──────┘ └───────┬──────┘
│ │
│ Claude Code │ Claude Code 24/7
│ (когда открыт) │ (когда поднимем VM)
▼ ▼
┌──────────────────────────────────────┐
│ GitHub (main branch) │
└──────┬──────────────────────────┬────┘
│ push │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ CI workflow │ │Docker workflow│
│ (backend+web │ │(api+web images│
│ +POS builds) │ │ на ghcr.io) │
└──────┬──────┘ └──────┬──────┘
│ │
│ artifacts: │ images pulled by
│ web-dist, POS .exe │ stage / prod compose
▼ ▼
┌──────────────┐ ┌──────────────┐
│ GitHub │ │ Proxmox-VM │
│ Releases │ │ stage/prod │
│ (.exe, APK) │ │ (docker-compose)│
└──────────────┘ └──────────────┘
```
## Что где живёт
| Компонент | Где | Когда работает | Зависит от Mac? |
|---|---|---|---|
| Claude Code (текущая сессия) | твой Mac | пока открыта app + Mac не спит | **Да** |
| Claude Code (будущее 24/7) | Proxmox VM | всегда | Нет |
| GitHub (код) | github.com | всегда | Нет |
| GitHub Actions CI | github.com | срабатывает на push / cron | **Нет** |
| Docker images | ghcr.io | всегда | Нет |
| Тестовый стенд (stage) | Proxmox VM | всегда | Нет |
| DB бэкапы | Proxmox VM → локальный диск + S3 (опц.) | cron nightly | Нет |
## Сценарии
### Ты заказал фичу → уснул
1. (Днём) запустил Claude, дал команду «сделай X», Claude работает
2. Перед сном Claude коммитит и пушит то что успел
3. GitHub Actions автоматически собирает backend+web+POS, прогоняет тесты
4. Docker-образы уходят в ghcr.io
5. (Если stage настроен) — stage автопулит образ → перезапускается → готов к тесту
6. Telegram-бот шлёт тебе «готово, проверь stage.food-market.xxx»
7. Утром ты смотришь, ревьюишь, делаешь merge/revert
### Ты дал команду с iPhone
1. Открыл Claude на iPhone, сказал «обнови UI страницы X»
2. Claude работает, пушит
3. GitHub Actions → ghcr.io → stage → Telegram → ты проверяешь прямо с iPhone
### Что-то пошло не так
- Каждый коммит = одна точка отката. `git revert <sha>` за 10 секунд.
- БД: ежедневный pg_dump `.sql.gz`, 30 дней ротации, скрипт `deploy/backup.sh`.
- Критические операции (миграции с удалением данных, force-push на main) — всегда спрошу тебя.
## Что нужно для полноценного 24/7 (ещё не сделано)
- [x] GitHub Actions для CI (backend/web/POS) — готов `.github/workflows/ci.yml`
- [x] Docker workflow — готов `.github/workflows/docker.yml`
- [x] docker-compose для стенда — готов `deploy/docker-compose.yml`
- [x] DB backup скрипт — готов `deploy/backup.sh`
- [ ] Proxmox-VM `food-market-stage` — ждёт кредов от тебя
- [ ] Proxmox-VM `claude-runner` (чтобы я не жил на твоём Mac) — ждёт кредов
- [ ] SSH-ключ для деплоя в GitHub Secrets
- [ ] Telegram bot + chat_id в GitHub Secrets
- [ ] FTP для APK (если нужен) в GitHub Secrets
- [ ] Домен + SSL для stage (опц., Cloudflare)
## Секреты: безопасно передать мне
Пока твой Mac — единственное место, куда Claude Code имеет доступ. Безопасный путь:
1. Создай папку: `mkdir -p ~/.food-market-secrets && chmod 700 ~/.food-market-secrets`
2. Положи туда файлы (я буду читать только по твоей команде и не буду вставлять значения в чат):
- `~/.food-market-secrets/proxmox.env` — ssh creds для Proxmox API/VM
- `~/.food-market-secrets/ftp.env` — FTP для APK
- `~/.food-market-secrets/telegram.env``BOT_TOKEN=...` + `CHAT_ID=...`
3. Пришли в чат: "Секреты в ~/.food-market-secrets/"
4. Я прочитаю, прокину в GitHub Secrets через `gh secret set`, больше нигде не сохраню.
## Настройка Mac чтобы не засыпал ночью (временно, пока нет remote runner)
```bash
# Заблокировать sleep на время работы Claude (Ctrl+C чтобы отменить)
caffeinate -i -d
```
Или в System Settings → Lock Screen → «Turn display off after: Never» + «Prevent automatic sleeping when the display is off».
После того как поднимем `claude-runner` VM — этот обход больше не нужен.