Add production deployment guide and harden compose for it
Some checks failed
CI/CD / build-test-deploy (push) Has been cancelled
Some checks failed
CI/CD / build-test-deploy (push) Has been cancelled
- docker-compose.yml: bind client/server ports to 127.0.0.1 only — the client container is the sole intended public entry point (it proxies /api/* to the server itself), a host-level nginx sits in front of it in production. Client__Origin now configurable via CLIENT_ORIGIN env var. - docs/DEPLOY.md: step-by-step for a fresh Linux target (Docker install, clone, .env secrets, nginx + certbot). - docs/CI-CD.md: runner registration for that same target machine, in "host" mode so the deploy step's `docker compose up -d` acts on the actual running stack.
This commit is contained in:
@@ -6,3 +6,10 @@ JWT_SECRET=dev-secret-change-me-please-32-chars-min
|
|||||||
|
|
||||||
# Optional — defaults to "devpassword" if unset.
|
# Optional — defaults to "devpassword" if unset.
|
||||||
POSTGRES_PASSWORD=devpassword
|
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
|
||||||
|
|||||||
@@ -21,23 +21,27 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
ConnectionStrings__Postgres: "Host=postgres;Port=5432;Database=quizsystem;Username=quizsystem;Password=${POSTGRES_PASSWORD:-devpassword}"
|
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}
|
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
|
ASPNETCORE_ENVIRONMENT: Production
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
# Mapped to the same host port the client's hardcoded
|
# Bound to loopback only: the client container is the sole public entry
|
||||||
# Client/Shared/JsonWire.fs `serverUrl` already expects — the browser
|
# point (it proxies /api/* to this service itself — see
|
||||||
# (not any container) is what resolves "localhost:5144", so this keeps
|
# src/Client/nginx.conf), so nothing outside this host needs to reach
|
||||||
# that working unmodified for local/dev use of the compose stack.
|
# the API directly. Still published on localhost for local debugging.
|
||||||
ports: ["5144:8080"]
|
ports: ["127.0.0.1:5144:8080"]
|
||||||
networks: [quizsystem]
|
networks: [quizsystem]
|
||||||
|
|
||||||
client:
|
client:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: src/Client/Dockerfile
|
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]
|
networks: [quizsystem]
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
51
docs/CI-CD.md
Normal file
51
docs/CI-CD.md
Normal file
@@ -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` уже
|
||||||
|
обслуживает обновлённую версию.
|
||||||
118
docs/DEPLOY.md
Normal file
118
docs/DEPLOY.md
Normal file
@@ -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 стека).
|
||||||
Reference in New Issue
Block a user