# 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. | Public URL | What | |------------|------| | `https://wixur.ir` | Staging app (port **443** → Windows nginx → Docker). Mobinnet: public **80** is the modem. | | `http://wixur.ir:8088` | HTTP fallback | | `http://wixur.ir:3000` | Gitea + container registry | DNS `wixur.ir` must point at the **Windows** host. Production (`https://nudentic.ir` on Linux) is separate — see [`DEPLOY.md`](DEPLOY.md). Do **not** point `wixur.ir` at the Linux VPS. --- ## 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) ↓ https://wixur.ir → Windows nginx :443 → 127.0.0.1:18088 → Docker nginx → app http://wixur.ir:8088 → portproxy :8088 → 127.0.0.1:18088 (fallback) http://wixur.ir:3000 → Gitea (native, no Docker) ``` | 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`. After changing the public URL, re-run the Gitea workflow (or push to `master`) and set `FRONTEND_URL` in `C:\dyolink\secrets\backend.staging.env` to the same origin. --- ## Cut over from `:8088` to `http://wixur.ir` DNS A record for `wixur.ir` → Windows IP (already done if `http://wixur.ir:3000` and `:8088` work). On Windows: host nginx on **80** proxies to Docker **18088** ([`nginx/windows-edge-http.conf`](nginx/windows-edge-http.conf)). Remove portproxy on **80** (keep **8088** as fallback). Router must forward **80**. See §7. In Gitea → repo → **Settings → Actions → Variables**: - `PUBLIC_BASE_URL` = `https://wixur.ir` - `STAGING_HTTP_PORT` = `80` (optional; workflow default is 80) On disk: `FRONTEND_URL=https://wixur.ir` and `COOKIE_SECURE=true` in `C:\dyolink\secrets\backend.staging.env`. Then run the **Registry — build, push, deploy** workflow so the frontend image is rebuilt without `:8088`. --- --- ## 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": [ "host.docker.internal:3000", "127.0.0.1:3000", "wixur.ir:3000" ] } ``` Apply & restart Docker Desktop. **Why `host.docker.internal`?** Docker Desktop runs in a Linux VM. `docker login` runs from that VM — `127.0.0.1:3000` is the VM, not Gitea on Windows. ### 2b. Gitea `app.ini` — match registry URL Edit `C:\gitea\custom\conf\app.ini`: ```ini [server] ROOT_URL = http://host.docker.internal:3000/ ``` Restart Gitea. Gitea uses `ROOT_URL` for Docker registry auth redirects. ### 2c. Gitea Actions runner Download [act_runner](https://gitea.com/gitea/act_runner/releases) → e.g. `C:\gitea-runner\act_runner.exe`. **Important — runner level:** the registration token decides which repos can use the runner. | Token from | Runner level | Works for `admin/dyolink`? | |------------|--------------|----------------------------| | **Site Administration → Actions → Runners** | Instance (global) | Yes (recommended) | | **Repo → Settings → Actions → Runners** | Repository | Yes | | **User profile → Settings → Actions → Runners** | Individual | Often **no** — jobs stay queued with “no matching online runner” | If your runner shows **Type: Individual** in Gitea but jobs never start, delete it and re-register with an **instance** or **repository** token (table above). Register (use **localhost** when Gitea runs on the same PC — public IP often fails locally): ```powershell cd C:\gitea-runner # Stop daemon first (Ctrl+C) if running .\act_runner.exe register ` --instance "http://127.0.0.1:3000" ` --token "" ` --name "windows-staging" ` --labels "windows:host" ``` Start (leave running, or install as a Windows service later): ```powershell .\act_runner.exe daemon ``` **Verify:** open **your repo** → **Settings → Actions → Runners** — `windows-staging` must appear here as **Idle/Online** (not only under user settings). **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` (`http://wixur.ir`). - Replace JWT secrets with long random values (not `CHANGE_ME`). ### 4. Gitea repository Variables **Settings → Actions → Variables** | Name | Example | Notes | |------|---------|--------| | `REGISTRY_HOST` | `host.docker.internal:3000` | **Windows + Docker Desktop:** Docker runs in a Linux VM — `127.0.0.1` is the VM, not Gitea. Use `host.docker.internal:3000`. Gitea `ROOT_URL` should match this so registry login from CI works. Browsers and the Linux VPS use `http://wixur.ir:3000`. | | `REGISTRY_OWNER` | `admin` | Gitea user/org owning packages | | `PUBLIC_BASE_URL` | `https://wixur.ir` | How **users** open staging (HTTPS on 443). No trailing slash. | | `DEPLOY_SECRETS_DIR` | `C:/dyolink/secrets` | Forward slashes OK on Windows | | `CLONE_HOST` | `127.0.0.1:3000` | Git clone (runs on Windows host, not inside Docker VM) | | `STAGING_HTTP_PORT` | `80` | Public HTTP port (portproxy). Optional; workflow default is 80. | | `STAGING_LOCAL_PORT` | `18088` | Docker bind on `127.0.0.1`. Must not be `80` if portproxy already uses 80. | **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 HTTP 80" -Direction Inbound -Protocol TCP -LocalPort 80 -Action Allow # Optional fallback while cutting over from :8088 New-NetFirewallRule -DisplayName "Dyolink Staging 8088" -Direction Inbound -Protocol TCP -LocalPort 8088 -Action Allow ``` ### 7. Port 80 = host nginx (not portproxy) Gitea stays on **:3000**. Docker staging nginx binds **127.0.0.1:18088**. A **Windows nginx** (the 1.29.x you already have) listens on **80** and proxies to 18088. Config: [`nginx/windows-edge-http.conf`](nginx/windows-edge-http.conf). **Do not** run portproxy on port 80 at the same time — it will lose to nginx (404 / hang). Keep portproxy **8088 → 18088** as fallback. ```powershell # 1) Free port 80 from portproxy (nginx will bind 80) netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=80 # 2) Confirm Docker staging is up curl.exe http://127.0.0.1:18088/health # 3) Install windows-edge-http.conf into host nginx, then: # nginx -t # nginx -s reload # (paths depend on where nginx is installed) # 4) Must print "healthy" (not 404) curl.exe http://127.0.0.1/health ``` Replace any **default_server** / leftover `server { listen 80; }` in the host nginx that returns 404, or this file will never win. From another machine (after **router forward TCP 80** → this PC): ```bash curl http://wixur.ir/health ``` **Mobinnet:** public **80** is the modem. Use **443** instead: router forward **TCP 443 → 192.168.1.100**, then `https://wixur.ir`. Let's Encrypt after mobile-data `/health` works (self-signed is enough for that test). --- ## 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://wixur.ir ``` **Health check:** ```powershell curl http://wixur.ir/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 host.docker.internal:3000 -u ` (on Windows Docker Desktop; Linux prod will use `wixur.ir:3000`) 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 | |---------|-----| | `no matching online runner with label` | Runner **offline** → start `act_runner.exe daemon`. Or wrong **runner level** → re-register with token from **Site Administration → Actions → Runners** or **repo → Settings → Actions → Runners** (not user profile). Confirm runner appears on **repo** Runners page as Online. | | 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` | | `docker login` connection refused on `127.0.0.1:3000` | **Docker Desktop on Windows:** set `REGISTRY_HOST=host.docker.internal:3000`, add it to insecure-registries, set Gitea `ROOT_URL=http://host.docker.internal:3000/`. Keep `CLONE_HOST=127.0.0.1:3000` for git. | | `docker login` / push denied, redirect to public IP | Set Gitea `ROOT_URL` to a host Docker can reach (`host.docker.internal:3000` on Windows Docker Desktop). | | `server gave HTTP response to HTTPS client` | Add registry host 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 80 bind forbidden / access permissions | Portproxy already owns 80. Bind Docker to `18088` and point portproxy **80 → 127.0.0.1:18088**. Free IIS if it holds 80. | | `http://wixur.ir` unreachable / empty reply | Docker Desktop: run **portproxy** (§7). `127.0.0.1:18088/health` must work on Windows first. Router must forward **80**. | | Port 8088 bind forbidden / access permissions | Portproxy already owns 8088. Bind Docker to `18088` and point portproxy **8088 → 127.0.0.1:18088**. | | Port 8088 unreachable from Mac / empty reply | Legacy URL. Prefer `http://wixur.ir`. Same 18088 backend; add portproxy 8088 only as fallback. | | Backend restart loop | JWT secrets still placeholder; fix `backend.staging.env` | | Backend DB auth error | `DATABASE_URL` password ≠ `POSTGRES_PASSWORD` | | `dumb-init docker-entrypoint.sh: No such file or directory` | Windows CRLF in shell scripts — fixed in Dockerfiles (rebuild images). | | `frontend is unhealthy` / deploy waits on frontend | Next.js `/` redirects to `/en` (3xx). Rebuild after healthcheck fix (accepts 2xx/3xx). | **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 **inside Docker** staging | | `infrastructure/nginx/windows-edge-http.conf` | Windows **host** nginx on port 80 → 18088 | --- ## Production (later) | Environment | Trigger | Host | |-------------|---------|------| | Staging | Push/merge to `master` | Windows + Gitea | | Production | Git tag `v*.*.*` | Linux + `nudentic.ir` | Production tag deploy (Gitea registry → Linux `nudentic.ir`) is **not** in this workflow yet. This file is Windows staging only.