diff --git a/.env.example b/.env.example index 5c9fa30..414d10d 100644 --- a/.env.example +++ b/.env.example @@ -6,3 +6,10 @@ JWT_SECRET=dev-secret-change-me-please-32-chars-min # Optional — defaults to "devpassword" if unset. POSTGRES_PASSWORD=devpassword + +# Effectively unused now that the client talks to the API via a same-origin +# /api/* proxy (see src/Client/nginx.conf) rather than a cross-origin +# request — CORS just never triggers. Left configurable as a defensive +# fallback; defaults to http://localhost:8081 if unset. For production set +# it to the real public origin, e.g. https://ruvdstest.danamir.site +CLIENT_ORIGIN=http://localhost:8081 diff --git a/docker-compose.yml b/docker-compose.yml index 0520588..4bd1f44 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,23 +21,27 @@ services: environment: ConnectionStrings__Postgres: "Host=postgres;Port=5432;Database=quizsystem;Username=quizsystem;Password=${POSTGRES_PASSWORD:-devpassword}" Jwt__Secret: ${JWT_SECRET:?Set JWT_SECRET in .env — see .env.example} - Client__Origin: "http://localhost:8081" + Client__Origin: ${CLIENT_ORIGIN:-http://localhost:8081} ASPNETCORE_ENVIRONMENT: Production depends_on: postgres: condition: service_healthy - # Mapped to the same host port the client's hardcoded - # Client/Shared/JsonWire.fs `serverUrl` already expects — the browser - # (not any container) is what resolves "localhost:5144", so this keeps - # that working unmodified for local/dev use of the compose stack. - ports: ["5144:8080"] + # Bound to loopback only: the client container is the sole public entry + # point (it proxies /api/* to this service itself — see + # src/Client/nginx.conf), so nothing outside this host needs to reach + # the API directly. Still published on localhost for local debugging. + ports: ["127.0.0.1:5144:8080"] networks: [quizsystem] client: build: context: . dockerfile: src/Client/Dockerfile - ports: ["8081:80"] + # Loopback-only in production, where a host-level nginx (TLS + the real + # domain) is the actual public entry point and proxies here — see + # docs/DEPLOY.md. For local `docker compose up`, still reachable at + # http://localhost:8081 same as before. + ports: ["127.0.0.1:8081:80"] networks: [quizsystem] volumes: diff --git a/docs/CI-CD.md b/docs/CI-CD.md new file mode 100644 index 0000000..d21064c --- /dev/null +++ b/docs/CI-CD.md @@ -0,0 +1,51 @@ +# CI/CD (Gitea Actions) + +Пайплайн — `.gitea/workflows/ci-cd.yml`. На каждый push/PR: юнит-тесты `Domain.Tests` + +`docker compose build` (проверка, что весь стек собирается). На push в `master` — дополнительно +`docker compose up -d` (реальный редеплой). + +Раннер выполняет джобы **прямо на целевом сервере** (`ruvdstest.danamir.site`, тот же, что описан +в `docs/DEPLOY.md`) — это и есть логика деплоя: шаг "Deploy" в workflow просто выполняет +`docker compose up -d` в том же каталоге `/opt/ruvdstests`, поверх уже поднятого стека. + +## Регистрация раннера + +Выполняется один раз, на целевом сервере (там же, где разворачивали приложение), после того как +шаги 1–6 из `docs/DEPLOY.md` пройдены. + +**1. Убедиться, что Actions включены** на вашем Gitea (Site Administration → пункт "Actions" должен +быть виден; если Gitea развёрнута в докере — переменная `GITEA__actions__ENABLED=true` в её +конфиге/окружении). + +**2. Получить токен регистрации**: Site Administration → Actions → Runners → **Create new runner** +(или в настройках именно этого репозитория: Settings → Actions → Runners). + +**3. Запустить раннер** (заполните токен и запустите сами — секрет никому больше не передаётся): + +```bash +docker volume create gitea-runner-data + +docker run -d --name gitea-ci-runner --restart unless-stopped \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v gitea-runner-data:/data \ + -e GITEA_INSTANCE_URL=https://git.danamir.su \ + -e GITEA_RUNNER_REGISTRATION_TOKEN=ВАШ_ТОКЕН_СЮДА \ + -e GITEA_RUNNER_NAME=ruvdstest-prod \ + -e GITEA_RUNNER_LABELS=host:host \ + gitea/runner:latest +``` + +`host:host` — раннер выполняет джобы напрямую в своём собственном контейнере (никаких вложенных +контейнеров на каждый джоб), у него смонтирован docker.sock хоста — поэтому `docker compose` +внутри джоба управляет реальными контейнерами на этой машине. Джоб сам ставит себе `docker-cli` и +.NET SDK через `apk`/`dotnet-install.sh` (см. workflow) — образ раннера намеренно голый, чтобы не +поддерживать отдельный кастомный образ. + +Проверить, что раннер подключился: Site Administration → Actions → Runners — должен появиться +`ruvdstest-prod` со статусом Idle/Online. + +## Проверка + +Сделайте любой коммит и запушьте в `master` — во вкладке **Actions** репозитория должен появиться +запуск, пройти тесты, сборку и деплой. После завершения `https://ruvdstest.danamir.site` уже +обслуживает обновлённую версию. diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md new file mode 100644 index 0000000..d47ecdf --- /dev/null +++ b/docs/DEPLOY.md @@ -0,0 +1,118 @@ +# Деплой на боевой сервер (Linux VPS) + +Выполняется один раз, вручную, на целевой машине (`ruvdstest.danamir.site`). После этого шага +CI/CD (`.gitea/workflows/ci-cd.yml`) берёт на себя все последующие обновления при push в `master`. + +Все команды ниже выполняются на самой целевой машине (по SSH/консоли хостера) — не на машине +разработки. + +## 0. Предварительные условия + +- DNS-запись `ruvdstest.danamir.site` уже указывает на IP этой машины (проверить: `dig +short ruvdstest.danamir.site`). +- Открыты порты 80 и 443 (для HTTP-01 challenge certbot и самого HTTPS). + +## 1. Установить Docker + +```bash +curl -fsSL https://get.docker.com | sh +sudo systemctl enable --now docker +sudo usermod -aG docker $USER # затем перелогиниться, чтобы применилось +``` + +Проверить: + +```bash +docker --version +docker compose version +``` + +## 2. Склонировать репозиторий + +```bash +sudo mkdir -p /opt/ruvdstests +sudo chown $USER:$USER /opt/ruvdstests +git clone https://git.danamir.su/danamir/RuvdsTest.git /opt/ruvdstests +cd /opt/ruvdstests +``` + +## 3. Настроить `.env` с боевыми секретами + +```bash +cp .env.example .env + +# Сгенерировать длинный случайный JWT-секрет и подставить в .env: +sed -i "s|^JWT_SECRET=.*|JWT_SECRET=$(openssl rand -base64 48 | tr -d '\n')|" .env + +# Сгенерировать пароль для Postgres: +sed -i "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$(openssl rand -base64 24 | tr -d '\n')|" .env + +# Публичный origin — на этом этапе это скорее формальность (клиент теперь +# ходит на /api того же origin, кросс-доменных запросов не будет), но +# выставить правильно не помешает: +echo "CLIENT_ORIGIN=https://ruvdstest.danamir.site" >> .env + +cat .env # свериться, что все три значения на месте +``` + +## 4. Поднять стек + +```bash +docker compose up -d --build +docker compose ps # все три контейнера должны быть Up/healthy +``` + +Проверить изнутри машины, что всё работает (пока без TLS, напрямую на loopback-порт клиента): + +```bash +curl -s http://127.0.0.1:8081/api/login -X POST \ + -H "Content-Type: application/json" \ + -d '{"Email":"teacher@example.com","Password":"teacher123"}' +# ожидается {"Ok":{"Token":"...", ...}} +``` + +## 5. Установить nginx + получить сертификат + +```bash +sudo apt update && sudo apt install -y nginx certbot python3-certbot-nginx +``` + +Создать `/etc/nginx/sites-available/ruvdstest`: + +```nginx +server { + listen 80; + server_name ruvdstest.danamir.site; + + location / { + proxy_pass http://127.0.0.1:8081; + 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; + } +} +``` + +Клиентский контейнер уже сам умеет проксировать `/api/*` на сервер (см. `src/Client/nginx.conf`), +поэтому хостовому nginx достаточно одного `proxy_pass` на весь сайт целиком — отдельный location +для `/api` тут не нужен. + +```bash +sudo ln -s /etc/nginx/sites-available/ruvdstest /etc/nginx/sites-enabled/ +sudo nginx -t && sudo systemctl reload nginx + +sudo certbot --nginx -d ruvdstest.danamir.site +``` + +`certbot --nginx` сам допишет `listen 443 ssl`, сертификат и редирект с 80 на 443 в тот же файл. + +## 6. Проверить снаружи + +Открыть `https://ruvdstest.danamir.site` в браузере — должен появиться экран логина, залогиниться +демо-пользователем, убедиться что запросы в Network идут на `https://ruvdstest.danamir.site/api/...` +(не на `localhost`). + +## 7. Раннер CI/CD + +См. `docs/CI-CD.md` — регистрируется на этой же машине, деплой-шаг пайплайна выполняет +`docker compose up -d` прямо здесь же (поверх уже поднятого в шаге 4 стека).