Files
dyolink/infrastructure/STAGING-DEPLOY.md
rameen 2e2c910d22 Document staging HTTPS on wixur.ir (Windows nginx + Let's Encrypt).
Mobinnet holds public port 80, so staging is served on 443. Examples and the host nginx template now match FRONTEND_URL=https://wixur.ir.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 14:46:05 +03:30

14 KiB
Raw Blame History

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. 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 <REGISTRY_HOST>/<owner>/dyolink-backend:<sha>
frontend <REGISTRY_HOST>/<owner>/dyolink-frontend:<sha>
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). 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


One-time setup (Windows host)

1. Docker Desktop

  • Install and keep Docker Desktop running during builds/deploys.
  • Settings → Docker Engine — allow HTTP registry:
{
  "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:

[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 → 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):

cd C:\gitea-runner
# Stop daemon first (Ctrl+C) if running
.\act_runner.exe register `
  --instance "http://127.0.0.1:3000" `
  --token "<token-from-Site-Admin-OR-repo-Runners-page>" `
  --name "windows-staging" `
  --labels "windows:host"

Start (leave running, or install as a Windows service later):

.\act_runner.exe daemon

Verify: open your repoSettings → Actions → Runnerswindows-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)

New-Item -ItemType Directory -Force -Path "C:\dyolink\secrets"

Copy examples and edit:

  • infrastructure/database.staging.env.exampleC:\dyolink\secrets\database.staging.env
  • infrastructure/backend.staging.env.exampleC:\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)

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.

Do not run portproxy on port 80 at the same time — it will lose to nginx (404 / hang). Keep portproxy 8088 → 18088 as fallback.

# 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):

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, deployRun workflow → branch master
  3. Watch jobs: build-and-pushdeploy (first run ~1530 min)

Option B — Push to master

git push origin master

Or merge a PR into master — same result.

Verify success

On Windows (PowerShell):

docker ps

Expect: dyolink_nginx_staging, dyolink_backend_staging, dyolink_frontend_staging, dyolink_postgres_staging.

From browser or another machine:

http://wixur.ir

Health check:

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
  2. Set REGISTRY_PREFIX, IMAGE_TAG, STAGING_HTTP_PORT, DEPLOY_SECRETS_DIR
  3. docker login host.docker.internal:3000 -u <user> (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:

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.