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>
14 KiB
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.irSTAGING_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 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)
New-Item -ItemType Directory -Force -Path "C:\dyolink\secrets"
Copy examples and edit:
infrastructure/database.staging.env.example→C:\dyolink\secrets\database.staging.envinfrastructure/backend.staging.env.example→C:\dyolink\secrets\backend.staging.env
Rules:
DATABASE_URLpassword must matchPOSTGRES_PASSWORD.FRONTEND_URLmust matchPUBLIC_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 daemonrunning- 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)
- Gitea → repo → Actions
- Registry — build, push, deploy → Run workflow → branch
master - Watch jobs: build-and-push → deploy (first run ~15–30 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/:
- Create
deploy.registry.envfromdeploy.registry.env.example - Set
REGISTRY_PREFIX,IMAGE_TAG,STAGING_HTTP_PORT,DEPLOY_SECRETS_DIR docker login host.docker.internal:3000 -u <user>(on Windows Docker Desktop; Linux prod will usewixur.ir:3000)docker compose -f docker-compose.registry.yml --env-file deploy.registry.env pull backend frontenddocker 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.