From 1258ecf89920435cb981f368b2c7c6a046d677db Mon Sep 17 00:00:00 2001 From: rameen Date: Sun, 23 Aug 2026 11:10:28 +0330 Subject: [PATCH 1/2] Enable Gitea Actions staging deploy on master push --- .gitea/workflows/registry-build-deploy.yml | 339 +++++++++------------ AGENTS.md | 2 +- README.md | 99 +----- infrastructure/deploy.registry.env.example | 7 +- 4 files changed, 158 insertions(+), 289 deletions(-) diff --git a/.gitea/workflows/registry-build-deploy.yml b/.gitea/workflows/registry-build-deploy.yml index 1ca249e..1f5fd2e 100644 --- a/.gitea/workflows/registry-build-deploy.yml +++ b/.gitea/workflows/registry-build-deploy.yml @@ -1,30 +1,25 @@ -# Build backend/frontend images, push to Gitea Container Registry, deploy with pull-only compose. +# Staging: build backend/frontend images, push to Gitea Container Registry, deploy on self-hosted runner. # -# Repository Variables (Settings → Actions → Variables) — non-secret: +# Triggers: push to master/main, or manual workflow_dispatch. +# +# Repository Variables (Settings → Actions → Variables): # REGISTRY_HOST e.g. 178.131.50.201:3000 (no http/https) -# REGISTRY_OWNER Gitea user or org that owns the packages (same as image namespace) +# REGISTRY_OWNER Gitea user or org that owns the packages # PUBLIC_BASE_URL URL users open in browser, e.g. http://178.131.50.201:8088 (no trailing slash) +# DEPLOY_SECRETS_DIR absolute path on runner, e.g. C:/dyolink/secrets +# +# Optional: +# STAGING_HTTP_PORT host port for nginx (default 8088) +# CLONE_HOST when runner is on the same host as Gitea and public IP fails locally, +# set to 127.0.0.1:3000 (no http:// — same style as REGISTRY_HOST) # # Repository Secrets (Settings → Actions → Secrets): # REGISTRY_USERNAME Gitea username for docker login # REGISTRY_PASSWORD Gitea access token (packages:read/write) or account password # -# HTTP registry (typical self-hosted Gitea): Docker defaults to HTTPS. If login/push fails with -# "server gave HTTP response to HTTPS client", add REGISTRY_HOST (e.g. 192.168.1.100:3000) to the -# Docker daemon "insecure-registries" on the RUNNER machine, then restart Docker (Docker Desktop -# → Settings → Docker Engine → JSON → "insecure-registries": ["host:port"]). +# Docker on runner: add REGISTRY_HOST to insecure-registries (HTTP registry), then restart Docker. # -# Optional: -# STAGING_HTTP_PORT host port for nginx (default 8088) -# -# Required for deploy job (absolute path on the runner host): -# DEPLOY_SECRETS_DIR folder containing database.staging.env + backend.staging.env -# -# Runner: self-hosted with Docker. Default shell is powershell (Windows act_runner often has no WSL bash). -# For a Linux runner, change defaults.run.shell to bash and restore bash syntax if needed. -# -# We do NOT use gitea.com/actions/checkout — many restricted networks cannot reach gitea.com. -# Checkout is a plain git clone from the same Gitea host. +# Runner: self-hosted with Docker + git. Default shell is powershell (Windows act_runner). name: Registry — build, push, deploy @@ -38,189 +33,149 @@ defaults: shell: powershell jobs: - temp-success: + build-and-push: runs-on: self-hosted + outputs: + image_tag: ${{ steps.meta.outputs.image_tag }} steps: - - name: Temporary placeholder (always success) + - name: Checkout (clone from this Gitea — no gitea.com) run: | $ErrorActionPreference = 'Stop' - Write-Host "Temporary workflow is active." - Write-Host "Trigger: ${{ github.event_name }}" - Write-Host "Branch: ${{ github.ref_name }}" - Write-Host "Commit: ${{ github.sha }}" - Write-Host "Production build/push/deploy steps are intentionally commented." - exit 0 + $cloneHost = '${{ vars.CLONE_HOST }}'.Trim() + if ([string]::IsNullOrWhiteSpace($cloneHost)) { + $Server = "${{ github.server_url }}".TrimEnd('/') + } elseif ($cloneHost -match '^https?://') { + $Server = $cloneHost.TrimEnd('/') + } else { + $Server = 'http://' + $cloneHost + } + $Repo = "${{ github.repository }}" + $Branch = "${{ github.ref_name }}" + $Token = "${{ github.token }}" + $Actor = "${{ github.actor }}" + $hp = $Server -replace '^https?://', '' + if ($Server.StartsWith('https')) { + $cloneUrl = 'https://' + $Actor + ':' + $Token + '@' + $hp + '/' + $Repo + '.git' + } else { + $cloneUrl = 'http://' + $Actor + ':' + $Token + '@' + $hp + '/' + $Repo + '.git' + } + $env:GIT_TERMINAL_PROMPT = '0' + git clone --depth 1 --branch $Branch $cloneUrl . - # --------------------------------------------------------------------------- - # Production pipeline is temporarily disabled. - # Uncomment these jobs after split-DNS / registry reachability is fixed. - # --------------------------------------------------------------------------- - # - # build-and-push: - # runs-on: self-hosted - # outputs: - # image_tag: ${{ steps.meta.outputs.image_tag }} - # steps: - # - name: Checkout (clone from this Gitea — no gitea.com) - # - name: Image tag and registry prefix - # - name: Log in to container registry - # - name: Build and push backend - # - name: Build and push frontend - # - # deploy: - # needs: build-and-push - # runs-on: self-hosted - # steps: - # - name: Checkout (shallow clone from this Gitea — no gitea.com) - # - name: Write deploy.registry.env and validate secrets path - # - name: Log in to container registry (for pull) - # - name: Pull and start stack + - name: Image tag and registry prefix + id: meta + run: | + $ErrorActionPreference = 'Stop' + $short = (git rev-parse --short HEAD).Trim() + $utf8 = New-Object System.Text.UTF8Encoding $false + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "image_tag=$short`n", $utf8) + $prefix = "${{ vars.REGISTRY_HOST }}/${{ vars.REGISTRY_OWNER }}" + [System.IO.File]::AppendAllText($env:GITHUB_ENV, "REGISTRY_PREFIX=$prefix`n", $utf8) + - name: Log in to container registry + run: | + $ErrorActionPreference = 'Stop' + $pass = @' + ${{ secrets.REGISTRY_PASSWORD }} + '@ + $pass.Trim() | docker login "${{ vars.REGISTRY_HOST }}" -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin + - name: Build and push backend + run: | + $ErrorActionPreference = 'Stop' + $tag = "${{ steps.meta.outputs.image_tag }}" + docker build ` + -t "$env:REGISTRY_PREFIX/dyolink-backend:$tag" ` + -t "$env:REGISTRY_PREFIX/dyolink-backend:latest" ` + ./backend + docker push "$env:REGISTRY_PREFIX/dyolink-backend:$tag" + docker push "$env:REGISTRY_PREFIX/dyolink-backend:latest" - ####SAMPLE - # name: Registry — build, push, deploy + - name: Build and push frontend + env: + PUBLIC_BASE_URL: ${{ vars.PUBLIC_BASE_URL }} + run: | + $ErrorActionPreference = 'Stop' + $tag = "${{ steps.meta.outputs.image_tag }}" + $base = $env:PUBLIC_BASE_URL + docker build ` + --build-arg "NEXT_PUBLIC_API_URL=$base/api" ` + --build-arg "NEXT_PUBLIC_APP_URL=$base" ` + --build-arg "NEXT_PUBLIC_APP_NAME=Dyolink" ` + -t "$env:REGISTRY_PREFIX/dyolink-frontend:$tag" ` + -t "$env:REGISTRY_PREFIX/dyolink-frontend:latest" ` + ./frontend + docker push "$env:REGISTRY_PREFIX/dyolink-frontend:$tag" + docker push "$env:REGISTRY_PREFIX/dyolink-frontend:latest" - # on: - # push: - # branches: [main, master] - # workflow_dispatch: + deploy: + needs: build-and-push + runs-on: self-hosted + steps: + - name: Checkout (shallow clone from this Gitea — no gitea.com) + run: | + $ErrorActionPreference = 'Stop' + $cloneHost = '${{ vars.CLONE_HOST }}'.Trim() + if ([string]::IsNullOrWhiteSpace($cloneHost)) { + $Server = "${{ github.server_url }}".TrimEnd('/') + } elseif ($cloneHost -match '^https?://') { + $Server = $cloneHost.TrimEnd('/') + } else { + $Server = 'http://' + $cloneHost + } + $Repo = "${{ github.repository }}" + $Branch = "${{ github.ref_name }}" + $Token = "${{ github.token }}" + $Actor = "${{ github.actor }}" + $hp = $Server -replace '^https?://', '' + if ($Server.StartsWith('https')) { + $cloneUrl = 'https://' + $Actor + ':' + $Token + '@' + $hp + '/' + $Repo + '.git' + } else { + $cloneUrl = 'http://' + $Actor + ':' + $Token + '@' + $hp + '/' + $Repo + '.git' + } + $env:GIT_TERMINAL_PROMPT = '0' + git clone --depth 1 --branch $Branch $cloneUrl . - # defaults: - # run: - # shell: powershell + - name: Write deploy.registry.env and validate secrets path + run: | + $ErrorActionPreference = 'Stop' + $SD = '${{ vars.DEPLOY_SECRETS_DIR }}'.Trim() + if ([string]::IsNullOrWhiteSpace($SD)) { + Write-Host "Set repository variable DEPLOY_SECRETS_DIR to the absolute path on this runner" + Write-Host "where database.staging.env and backend.staging.env live (not in git)." + exit 1 + } + if (-not (Test-Path (Join-Path $SD "database.staging.env"))) { + Write-Host "Missing $(Join-Path $SD 'database.staging.env')" + exit 1 + } + if (-not (Test-Path (Join-Path $SD "backend.staging.env"))) { + Write-Host "Missing $(Join-Path $SD 'backend.staging.env')" + exit 1 + } + $stagingPort = '${{ vars.STAGING_HTTP_PORT }}'.Trim() + if ([string]::IsNullOrWhiteSpace($stagingPort)) { $stagingPort = '8088' } + $imageTag = "${{ needs.build-and-push.outputs.image_tag }}" + $lines = @( + "REGISTRY_PREFIX=${{ vars.REGISTRY_HOST }}/${{ vars.REGISTRY_OWNER }}", + "IMAGE_TAG=$imageTag", + "STAGING_HTTP_PORT=$stagingPort", + "DEPLOY_SECRETS_DIR=$SD" + ) + Set-Location infrastructure + $lines | Set-Content -Path deploy.registry.env -Encoding utf8 - # jobs: - # build-and-push: - # runs-on: self-hosted - # outputs: - # image_tag: ${{ steps.meta.outputs.image_tag }} - # steps: - # - name: Checkout (clone from this Gitea — no gitea.com) - # run: | - # $ErrorActionPreference = 'Stop' - # $Server = "${{ github.server_url }}".TrimEnd('/') - # $Repo = "${{ github.repository }}" - # $Branch = "${{ github.ref_name }}" - # $Token = "${{ github.token }}" - # $Actor = "${{ github.actor }}" - # $hp = $Server -replace '^https?://', '' - # if ($Server.StartsWith('https')) { - # $cloneUrl = 'https://' + $Actor + ':' + $Token + '@' + $hp + '/' + $Repo + '.git' - # } else { - # $cloneUrl = 'http://' + $Actor + ':' + $Token + '@' + $hp + '/' + $Repo + '.git' - # } - # $env:GIT_TERMINAL_PROMPT = '0' - # git clone --depth 1 --branch $Branch $cloneUrl . - - # - name: Image tag and registry prefix - # id: meta - # run: | - # $ErrorActionPreference = 'Stop' - # $short = (git rev-parse --short HEAD).Trim() - # $utf8 = New-Object System.Text.UTF8Encoding $false - # [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "image_tag=$short`n", $utf8) - # $prefix = "${{ vars.REGISTRY_HOST }}/${{ vars.REGISTRY_OWNER }}" - # [System.IO.File]::AppendAllText($env:GITHUB_ENV, "REGISTRY_PREFIX=$prefix`n", $utf8) - - # - name: Log in to container registry - # run: | - # $ErrorActionPreference = 'Stop' - # $pass = @' - # ${{ secrets.REGISTRY_PASSWORD }} - # '@ - # $pass.Trim() | docker login "${{ vars.REGISTRY_HOST }}" -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin - - # - name: Build and push backend - # run: | - # $ErrorActionPreference = 'Stop' - # $tag = "${{ steps.meta.outputs.image_tag }}" - # docker build ` - # -t "$env:REGISTRY_PREFIX/dyolink-backend:$tag" ` - # -t "$env:REGISTRY_PREFIX/dyolink-backend:latest" ` - # ./backend - # docker push "$env:REGISTRY_PREFIX/dyolink-backend:$tag" - # docker push "$env:REGISTRY_PREFIX/dyolink-backend:latest" - - # - name: Build and push frontend - # env: - # PUBLIC_BASE_URL: ${{ vars.PUBLIC_BASE_URL }} - # run: | - # $ErrorActionPreference = 'Stop' - # $tag = "${{ steps.meta.outputs.image_tag }}" - # $base = $env:PUBLIC_BASE_URL - # docker build ` - # --build-arg "NEXT_PUBLIC_API_URL=$base/api" ` - # --build-arg "NEXT_PUBLIC_APP_URL=$base" ` - # --build-arg "NEXT_PUBLIC_APP_NAME=Dyolink" ` - # -t "$env:REGISTRY_PREFIX/dyolink-frontend:$tag" ` - # -t "$env:REGISTRY_PREFIX/dyolink-frontend:latest" ` - # ./frontend - # docker push "$env:REGISTRY_PREFIX/dyolink-frontend:$tag" - # docker push "$env:REGISTRY_PREFIX/dyolink-frontend:latest" - - # deploy: - # needs: build-and-push - # runs-on: self-hosted - # steps: - # - name: Checkout (shallow clone from this Gitea — no gitea.com) - # run: | - # $ErrorActionPreference = 'Stop' - # $Server = "${{ github.server_url }}".TrimEnd('/') - # $Repo = "${{ github.repository }}" - # $Branch = "${{ github.ref_name }}" - # $Token = "${{ github.token }}" - # $Actor = "${{ github.actor }}" - # $hp = $Server -replace '^https?://', '' - # if ($Server.StartsWith('https')) { - # $cloneUrl = 'https://' + $Actor + ':' + $Token + '@' + $hp + '/' + $Repo + '.git' - # } else { - # $cloneUrl = 'http://' + $Actor + ':' + $Token + '@' + $hp + '/' + $Repo + '.git' - # } - # $env:GIT_TERMINAL_PROMPT = '0' - # git clone --depth 1 --branch $Branch $cloneUrl . - - # - name: Write deploy.registry.env and validate secrets path - # run: | - # $ErrorActionPreference = 'Stop' - # $SD = '${{ vars.DEPLOY_SECRETS_DIR }}'.Trim() - # if ([string]::IsNullOrWhiteSpace($SD)) { - # Write-Host "Set repository variable DEPLOY_SECRETS_DIR to the absolute path on this runner" - # Write-Host "where database.staging.env and backend.staging.env live (not in git)." - # exit 1 - # } - # if (-not (Test-Path (Join-Path $SD "database.staging.env"))) { - # Write-Host "Missing $(Join-Path $SD 'database.staging.env')" - # exit 1 - # } - # if (-not (Test-Path (Join-Path $SD "backend.staging.env"))) { - # Write-Host "Missing $(Join-Path $SD 'backend.staging.env')" - # exit 1 - # } - # $stagingPort = '${{ vars.STAGING_HTTP_PORT }}'.Trim() - # if ([string]::IsNullOrWhiteSpace($stagingPort)) { $stagingPort = '8088' } - # $imageTag = "${{ needs.build-and-push.outputs.image_tag }}" - # $lines = @( - # "REGISTRY_PREFIX=${{ vars.REGISTRY_HOST }}/${{ vars.REGISTRY_OWNER }}", - # "IMAGE_TAG=$imageTag", - # "STAGING_HTTP_PORT=$stagingPort", - # "DEPLOY_SECRETS_DIR=$SD" - # ) - # Set-Location infrastructure - # $lines | Set-Content -Path deploy.registry.env -Encoding utf8 - - # - name: Log in to container registry (for pull) - # run: | - # $ErrorActionPreference = 'Stop' - # $pass = @' - # ${{ secrets.REGISTRY_PASSWORD }} - # '@ - # $pass.Trim() | docker login "${{ vars.REGISTRY_HOST }}" -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin - - # - name: Pull and start stack - # run: | - # $ErrorActionPreference = 'Stop' - # Set-Location infrastructure - # docker compose -f docker-compose.registry.yml --env-file deploy.registry.env pull backend frontend - # docker compose -f docker-compose.registry.yml --env-file deploy.registry.env up -d + - name: Log in to container registry (for pull) + run: | + $ErrorActionPreference = 'Stop' + $pass = @' + ${{ secrets.REGISTRY_PASSWORD }} + '@ + $pass.Trim() | docker login "${{ vars.REGISTRY_HOST }}" -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin + - name: Pull and start stack + run: | + $ErrorActionPreference = 'Stop' + Set-Location infrastructure + docker compose -f docker-compose.registry.yml --env-file deploy.registry.env pull backend frontend + docker compose -f docker-compose.registry.yml --env-file deploy.registry.env up -d diff --git a/AGENTS.md b/AGENTS.md index 8416eaa..89aa04a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ Dental clinic ↔ lab platform (monorepo): |------|--------| | `backend/` | NestJS, Prisma, PostgreSQL | | `frontend/` | Next.js 16, React 19, next-intl, Tailwind | -| `infrastructure/` | Docker, nginx, deploy scripts | +| `infrastructure/` | Docker, nginx, deploy scripts — prod: `DEPLOY.md`, staging: `STAGING-DEPLOY.md` | **Organization types:** `CLINIC` (patients, appointments, treatment) and `LAB` (cases, tasks). Many features are org-type-specific. Permissions use `TAB_*_READ` / `TAB_*_EDIT` codes — see `backend/src/common/permissions.ts`. diff --git a/README.md b/README.md index a412280..2302c2f 100644 --- a/README.md +++ b/README.md @@ -42,103 +42,13 @@ SSL is issued automatically via **Certbot** (`scripts/init-letsencrypt.sh`). Ngi --- -## Deploy on your own server (Docker + Gitea) +## Staging deploy (Gitea Actions + Windows) -High level: **build container images → push to a registry → server pulls images and runs Compose**. Optionally **Gitea Actions** automates that on every merge to `main` / `master`. +**Full guide:** [`infrastructure/STAGING-DEPLOY.md`](infrastructure/STAGING-DEPLOY.md) -### 1. One-time server preparation +Merge or push to **`master`** → Gitea Actions builds images → deploys to `http://:8088`. -1. Install **Docker** and **Docker Compose** on the server. -2. Run **Gitea** with the **container registry** enabled (same host/port you use for `docker login`, e.g. `178.131.50.201:3000`). -3. Copy the repo (or deploy only `infrastructure/` + secrets). You need at least: - - - `infrastructure/docker-compose.registry.yml` - - `infrastructure/nginx/` configs referenced by that compose file - - `infrastructure/database/init.sql` if used by your Postgres service - -4. **Secrets on the server** (never commit real values): - - - Copy `infrastructure/database.staging.env.example` → **`database.staging.env`** (Postgres user/password/db). - - Copy `infrastructure/backend.staging.env.example` → **`backend.staging.env`** (e.g. `DATABASE_URL`, JWT, pointing at the compose Postgres service name). - - Put both files in one directory on the server, e.g. `/opt/dyolink/secrets/`. - -5. **Registry login from the server** (same credentials you use for `docker push`): - - ```bash - docker login : -u - ``` - - For HTTP registries, Docker may require **`insecure-registries`** on the daemon. - -### 2. Manual deploy (build images elsewhere, run on server) - -On your **dev machine** (after successful local builds): - -```powershell -$REG = ":" -$OWN = "" -$TAG = "manual" - -docker build -t "${REG}/${OWN}/dyolink-backend:${TAG}" -t "${REG}/${OWN}/dyolink-backend:latest" ./backend - -docker build ` - --build-arg NEXT_PUBLIC_API_URL="http://:/api" ` - --build-arg NEXT_PUBLIC_APP_URL="http://:" ` - --build-arg NEXT_PUBLIC_APP_NAME="Dyolink" ` - -t "${REG}/${OWN}/dyolink-frontend:${TAG}" ` - -t "${REG}/${OWN}/dyolink-frontend:latest" ` - ./frontend - -docker push "${REG}/${OWN}/dyolink-backend:${TAG}" -docker push "${REG}/${OWN}/dyolink-backend:latest" -docker push "${REG}/${OWN}/dyolink-frontend:${TAG}" -docker push "${REG}/${OWN}/dyolink-frontend:latest" -``` - -On the **server**, from `infrastructure/`: - -1. Create **`deploy.registry.env`** (see `infrastructure/deploy.registry.env.example`): - - - `REGISTRY_PREFIX=:/` (no `http://`, no trailing slash) - - `IMAGE_TAG=latest` or the tag you pushed - - `STAGING_HTTP_PORT=` (e.g. `8088` — browser uses `http://:8088`) - -2. Set **`DEPLOY_SECRETS_DIR`** to the absolute path of the folder containing `database.staging.env` and `backend.staging.env` (you can export it in the shell or add it to `deploy.registry.env` if your Compose setup expects it). - -3. Pull and start: - - ```bash - docker compose -f docker-compose.registry.yml --env-file deploy.registry.env pull backend frontend - docker compose -f docker-compose.registry.yml --env-file deploy.registry.env up -d - ``` - -The backend container runs **`prisma migrate deploy`** on startup (via entrypoint) when `NODE_ENV=production`, so schema updates apply after you deploy a new image that includes new migrations. - -### 3. Automatic deploy (Gitea Actions) - -Workflow file: **`.gitea/workflows/registry-build-deploy.yml`**. - -**Requirements:** - -- **Gitea Actions** enabled for the repository. -- A **self-hosted runner** (with Docker) registered to Gitea — the workflow uses `runs-on: self-hosted`. -- **Windows runners:** the workflow uses **PowerShell** (not Bash). Gitea’s runner was failing with `execvpe(/bin/bash) failed` when Bash was routed through WSL without a real `/bin/bash`. If your runner is **Linux**, switch `.gitea/workflows/registry-build-deploy.yml` to `defaults.run.shell: bash` and use Bash syntax instead. -- **Repository → Actions → Variables** (examples): - - - `REGISTRY_HOST` — e.g. `178.131.50.201:3000` - - `REGISTRY_OWNER` — image namespace (same as Docker image path after the host), e.g. `admin` - - `PUBLIC_BASE_URL` — URL users open in the browser, e.g. `http://178.131.50.201:8088` (no trailing slash) - - `DEPLOY_SECRETS_DIR` — **absolute path on the runner machine** to the folder containing `database.staging.env` and `backend.staging.env` - - Optional: `STAGING_HTTP_PORT` (defaults to `8088`) - -- **Repository → Actions → Secrets:** - - - `REGISTRY_USERNAME` - - `REGISTRY_PASSWORD` — access token with package read/write (or equivalent) - -**Trigger:** push to **`main`** or **`master`**, or run the workflow manually (**workflow_dispatch**). - -The pipeline clones from your Gitea instance, builds and pushes backend/frontend images, then on the runner runs **`docker compose pull`** and **`up -d`** using `infrastructure/docker-compose.registry.yml`. +Workflow: [`.gitea/workflows/registry-build-deploy.yml`](.gitea/workflows/registry-build-deploy.yml) --- @@ -148,5 +58,6 @@ The pipeline clones from your Gitea instance, builds and pushes backend/frontend |------|------| | `backend/Dockerfile` | API image | | `frontend/Dockerfile` | Web image | +| `infrastructure/STAGING-DEPLOY.md` | Staging setup, CI variables, testing | | `infrastructure/docker-compose.registry.yml` | Pull-only staging stack (registry images + nginx + postgres) | | `infrastructure/deploy.registry.env.example` | Template for `deploy.registry.env` | diff --git a/infrastructure/deploy.registry.env.example b/infrastructure/deploy.registry.env.example index 4e8eb3b..87f1add 100644 --- a/infrastructure/deploy.registry.env.example +++ b/infrastructure/deploy.registry.env.example @@ -15,5 +15,8 @@ IMAGE_TAG=latest STAGING_HTTP_PORT=8088 # Absolute path on the server where database.staging.env and backend.staging.env live. -# Use forward slashes on Windows. Same variable as Gitea Actions → DEPLOY_SECRETS_DIR. -# DEPLOY_SECRETS_DIR=D:/dyolink/secrets +# Use forward slashes on Windows. Same path as Gitea Actions variable DEPLOY_SECRETS_DIR. +# DEPLOY_SECRETS_DIR=C:/dyolink/secrets +# +# Gitea Actions also needs CLONE_HOST=127.0.0.1:3000 when runner and Gitea share one Windows host. +# See STAGING-DEPLOY.md (do not use GITEA_* variable names — Gitea rejects them). From ebd2de39cfb05d2f67251e34341a3ba928f5ca8f Mon Sep 17 00:00:00 2001 From: rameen Date: Sun, 23 Aug 2026 11:11:07 +0330 Subject: [PATCH 2/2] staging deploy md doc. --- infrastructure/STAGING-DEPLOY.md | 251 +++++++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 infrastructure/STAGING-DEPLOY.md diff --git a/infrastructure/STAGING-DEPLOY.md b/infrastructure/STAGING-DEPLOY.md new file mode 100644 index 0000000..c100080 --- /dev/null +++ b/infrastructure/STAGING-DEPLOY.md @@ -0,0 +1,251 @@ +# Dyolink — Staging deploy (Gitea + Windows) + +Automatic staging on a **self-hosted Gitea** machine: merge (or push) to **`master`** → build Docker images → push to Gitea Container Registry → deploy on the same host. + +**Example:** Gitea at `http://178.131.50.201:3000`, staging app at `http://178.131.50.201:8088`. + +Production (`nudentic.ir` on Linux, tag-based releases) is documented in [`DEPLOY.md`](DEPLOY.md). + +--- + +## Architecture + +``` +Push / merge to master + ↓ +Gitea Actions (self-hosted act_runner on Windows) + ↓ +Build backend + frontend → push to Gitea registry + ↓ +docker compose pull + up -d (docker-compose.registry.yml) + ↓ +http://:8088 → nginx → frontend / backend → postgres +``` + +| Service | Image source | +|----------|---------------------------------------------------| +| postgres | `postgres:15-alpine` (pulled from Docker Hub) | +| backend | `//dyolink-backend:` | +| frontend | `//dyolink-frontend:` | +| nginx | `nginx:alpine` | + +Frontend public URLs are **baked in at build time** via `PUBLIC_BASE_URL`. + +--- + +## Triggers + +| Event | Staging deploy? | +|-------|-----------------| +| Push to `master` | Yes | +| PR merged into `master` | Yes (merge = push to `master`) | +| Push to other branches only | No | +| Git tag (e.g. `v1.0.0`) | No — reserved for production later | +| Manual | Gitea → Actions → **Registry — build, push, deploy** → Run workflow | + +Workflow file: [`.gitea/workflows/registry-build-deploy.yml`](../.gitea/workflows/registry-build-deploy.yml) + +--- + +## One-time setup (Windows host) + +### 1. Docker Desktop + +- Install and keep **Docker Desktop running** during builds/deploys. +- **Settings → Docker Engine** — allow HTTP registry: + +```json +{ + "insecure-registries": ["178.131.50.201:3000", "127.0.0.1:3000"] +} +``` + +Apply & restart Docker. + +### 2. Gitea Actions runner + +Download [act_runner](https://gitea.com/gitea/act_runner/releases) → e.g. `C:\gitea-runner\act_runner.exe`. + +Register (use **localhost** when Gitea runs on the same PC — public IP often fails locally): + +```powershell +cd C:\gitea-runner +.\act_runner.exe register ` + --instance "http://127.0.0.1:3000" ` + --token "" ` + --name "windows-staging" ` + --labels "self-hosted,windows" +``` + +Start (leave running, or install as a Windows service later): + +```powershell +.\act_runner.exe daemon +``` + +Gitea → repo → **Settings → Actions → Runners** should show **Online**. + +**Requires:** `git` on PATH (for workflow checkout step). + +### 3. Secret env files (not in git) + +```powershell +New-Item -ItemType Directory -Force -Path "C:\dyolink\secrets" +``` + +Copy examples and edit: + +- `infrastructure/database.staging.env.example` → `C:\dyolink\secrets\database.staging.env` +- `infrastructure/backend.staging.env.example` → `C:\dyolink\secrets\backend.staging.env` + +Rules: + +- `DATABASE_URL` password must match `POSTGRES_PASSWORD`. +- `FRONTEND_URL` must match `PUBLIC_BASE_URL` (e.g. `http://178.131.50.201:8088`). +- Replace JWT secrets with long random values (not `CHANGE_ME`). + +### 4. Gitea repository Variables + +**Settings → Actions → Variables** + +| Name | Example | Notes | +|------|---------|--------| +| `REGISTRY_HOST` | `178.131.50.201:3000` | No `http://` | +| `REGISTRY_OWNER` | `admin` | Gitea user/org owning packages | +| `PUBLIC_BASE_URL` | `http://178.131.50.201:8088` | No trailing `/` | +| `DEPLOY_SECRETS_DIR` | `C:/dyolink/secrets` | Forward slashes OK on Windows | +| `CLONE_HOST` | `127.0.0.1:3000` | When runner and Gitea are same machine | +| `STAGING_HTTP_PORT` | `8088` | Optional (8088 is default) | + +**Naming note:** Gitea rejects variable names starting with `GITEA_` or `GITHUB_`. Use `CLONE_HOST`, not `GITEA_CLONE_URL`. + +### 5. Gitea repository Secrets + +**Settings → Actions → Secrets** + +| Name | Value | +|------|--------| +| `REGISTRY_USERNAME` | Gitea username | +| `REGISTRY_PASSWORD` | Gitea access token with **package read/write** | + +Create token: profile → **Settings → Applications → Generate New Token**. + +### 6. Firewall (once) + +```powershell +New-NetFirewallRule -DisplayName "Dyolink Staging 8088" -Direction Inbound -Protocol TCP -LocalPort 8088 -Action Allow +``` + +--- + +## Test the pipeline + +### Before first run + +- [ ] Docker Desktop running +- [ ] `act_runner.exe daemon` running +- [ ] All Variables + Secrets set (including `CLONE_HOST`) +- [ ] Secret env files exist under `DEPLOY_SECRETS_DIR` +- [ ] Workflow enabled on `master` (see repo) + +### Option A — Manual workflow (safest first test) + +1. Gitea → repo → **Actions** +2. **Registry — build, push, deploy** → **Run workflow** → branch `master` +3. Watch jobs: **build-and-push** → **deploy** (first run ~15–30 min) + +### Option B — Push to master + +```bash +git push origin master +``` + +Or merge a PR into `master` — same result. + +### Verify success + +**On Windows (PowerShell):** + +```powershell +docker ps +``` + +Expect: `dyolink_nginx_staging`, `dyolink_backend_staging`, `dyolink_frontend_staging`, `dyolink_postgres_staging`. + +**From browser or another machine:** + +```text +http://178.131.50.201:8088 +``` + +**Health check:** + +```powershell +curl http://178.131.50.201:8088/api/health +``` + +Expected: `{"status":"ok",...}` + +**Gitea packages:** profile/org → **Packages** — should list `dyolink-backend` and `dyolink-frontend` after first build. + +--- + +## Manual deploy (without CI) + +Useful when debugging registry/compose without re-running the full workflow. + +On the Windows host, from repo `infrastructure/`: + +1. Create `deploy.registry.env` from [`deploy.registry.env.example`](deploy.registry.env.example) +2. Set `REGISTRY_PREFIX`, `IMAGE_TAG`, `STAGING_HTTP_PORT`, `DEPLOY_SECRETS_DIR` +3. `docker login 178.131.50.201:3000 -u ` +4. `docker compose -f docker-compose.registry.yml --env-file deploy.registry.env pull backend frontend` +5. `docker compose -f docker-compose.registry.yml --env-file deploy.registry.env up -d` + +--- + +## Troubleshooting + +| Symptom | Fix | +|---------|-----| +| Runner can't register on public IP | Use `http://127.0.0.1:3000` for `--instance` | +| Variable name rejected in Gitea | No `GITEA_*` / `GITHUB_*` prefixes; use `CLONE_HOST` | +| `server gave HTTP response to HTTPS client` | Add registry to Docker **insecure-registries**, restart Docker | +| `Missing database.staging.env` | Check `DEPLOY_SECRETS_DIR` path and file names | +| `docker login` denied | Token needs package permissions; check username/secret | +| Git clone fails in workflow | Set `CLONE_HOST=127.0.0.1:3000` | +| Port 8088 unreachable | Windows firewall rule; confirm nginx container is up | +| Backend restart loop | JWT secrets still placeholder; fix `backend.staging.env` | +| Backend DB auth error | `DATABASE_URL` password ≠ `POSTGRES_PASSWORD` | + +**Logs:** + +```powershell +docker logs dyolink_backend_staging --tail 50 +docker logs dyolink_nginx_staging --tail 50 +docker logs dyolink_frontend_staging --tail 50 +``` + +--- + +## File reference + +| Path | Role | +|------|------| +| `.gitea/workflows/registry-build-deploy.yml` | CI: build, push, deploy | +| `infrastructure/docker-compose.registry.yml` | Staging stack (pull-only images) | +| `infrastructure/deploy.registry.env.example` | Manual deploy env template | +| `infrastructure/database.staging.env.example` | Postgres secrets template | +| `infrastructure/backend.staging.env.example` | API secrets template | +| `infrastructure/nginx/http-only.conf` | HTTP reverse proxy for staging | + +--- + +## Production (later) + +| Environment | Trigger | Host | +|-------------|---------|------| +| Staging | Push/merge to `master` | Windows + Gitea | +| Production | Git tag `v*.*.*` | Linux + `nudentic.ir` | + +Production flow will use Docker Hub (or registry) + [`DEPLOY.md`](DEPLOY.md) — not yet wired to the same workflow.