From 34040503cfbee2dec8d6f8683c72d84fe955eb5c Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Thu, 28 May 2026 19:06:28 +0330 Subject: [PATCH] docs: rewrite DEPLOY.md with self-hosted setup guide Replaces outdated Arvan Cloud instructions with the current self-hosted stack: Gitea CI, Nexus mirror, Docker Compose, IP-based access today and Caddy+domain tomorrow. Co-Authored-By: Claude Sonnet 4.6 --- DEPLOY.md | 321 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 258 insertions(+), 63 deletions(-) diff --git a/DEPLOY.md b/DEPLOY.md index bf61690..23342fb 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -1,83 +1,278 @@ -# Meezi — Production deployment (Arvan Cloud) +# Meezi — Deployment Guide -## Prerequisites - -- Arvan Cloud account (Iran region) -- Domain `meezi.ir` DNS pointed to Arvan load balancer -- ZarinPal merchant ID (production, sandbox off) -- Kavenegar API key -- PostgreSQL 16 + Redis managed or VMs - -## Services - -| Service | Suggested host | -|---------|----------------| -| API | `api.meezi.ir` → ASP.NET container port 8080 | -| Dashboard | `app.meezi.ir` or `meezi.ir` → Next.js standalone | -| QR landing | `meezi.ir/q/*` → same Next.js app (`/q/[code]` route) | -| Postgres | Private network only | -| Redis | Private network only | - -## Environment variables (API) +## Architecture overview ``` -ConnectionStrings__DefaultConnection=Host=...;Database=meezi;... -ConnectionStrings__Redis=... -Jwt__Key=<32+ char secret> -App__PublicBaseUrl=https://api.meezi.ir -App__QrPublicBaseUrl=https://meezi.ir -Billing__DashboardBaseUrl=https://app.meezi.ir -ZarinPal__MerchantId= -ZarinPal__Sandbox=false -Kavenegar__ApiKey= -Snappfood__WebhookSecret= -Taraz__Username= -Taraz__Password= -Taraz__CertificatePath=/secrets/taraz.pfx +Server: 171.22.25.73 +│ +├── Gitea :3000 ← source control + CI runner +├── Nexus :8081 ← package mirror (NuGet, npm, Docker) +│ +├── meezi-api :5080 ← .NET main API +├── meezi-admin-api:5081 ← .NET admin API +├── meezi-web :3101 ← Next.js cafe owner dashboard +├── meezi-admin-web:3102 ← Next.js super-admin panel +├── meezi-website :3010 ← Next.js marketing website +├── meezi-finder :3103 ← Next.js public finder +├── meezi-db :5434 ← PostgreSQL (not internet-facing) +└── meezi-redis :6381 ← Redis (not internet-facing) +``` + +Docker Compose files: + +``` +docker-compose.yml main services (postgres, redis, api, web, website, finder) +docker-compose.admin.yml admin overlay (+admin-api, +admin-web) +docker-compose.mirror.yml Nexus mirror — run once separately, stays running +docker-compose.caddy.yml Caddy HTTPS proxy — add when domain is ready +``` + +--- + +## First-time setup + +### Step 1 — Set the `ENV_FILE` secret in Gitea + +Open in browser: +``` +http://171.22.25.73:3000/soroushdes/meezi/settings/secrets +``` + +Click **Add Secret**, name it exactly **`ENV_FILE`**, paste the block below, click Save. + +```env +ASPNETCORE_ENVIRONMENT=Production + +# ── Database ────────────────────────────────────────────────────────────────── +DB_PASSWORD=YOUR_STRONG_PASSWORD +DB_CONNECTION_STRING=Host=postgres;Port=5432;Database=meezi;Username=meezi;Password=YOUR_STRONG_PASSWORD + +# ── JWT ─────────────────────────────────────────────────────────────────────── +# Generate: openssl rand -hex 32 +JWT_KEY=YOUR_64_CHAR_HEX + +# ── Migrations ──────────────────────────────────────────────────────────────── RUN_MIGRATIONS=true -Cors__Origins__0=https://app.meezi.ir -Cors__Origins__1=https://meezi.ir + +# ── Public URLs ─────────────────────────────────────────────────────────────── +# ⚠️ NEXT_PUBLIC_* are baked into Next.js images at build time. +# Changing them requires a CI re-run to rebuild images. +NEXT_PUBLIC_API_URL=http://171.22.25.73:5080 +NEXT_PUBLIC_ADMIN_API_URL=http://171.22.25.73:5081 +NEXT_PUBLIC_SITE_URL=http://171.22.25.73:3010 +NEXT_PUBLIC_FINDER_URL=http://171.22.25.73:3103 + +APP_QR_BASE_URL=http://171.22.25.73:3101 +BILLING_DASHBOARD_URL=http://171.22.25.73:3101 + +# ── CORS ────────────────────────────────────────────────────────────────────── +CORS_ORIGIN_0=http://171.22.25.73:3101 +CORS_ORIGIN_1=http://171.22.25.73:3010 +CORS_ORIGIN_2=http://171.22.25.73:3103 +CORS_ADMIN_ORIGIN_0=http://171.22.25.73:3102 + +# ── Host ports ──────────────────────────────────────────────────────────────── +API_PORT=5080 +ADMIN_API_PORT=5081 +WEB_PORT=3101 +ADMIN_WEB_PORT=3102 +WEBSITE_PORT=3010 +FINDER_PORT=3103 +POSTGRES_PORT=5434 +REDIS_PORT=6381 + +# ── Payment: ZarinPal ───────────────────────────────────────────────────────── +ZARINPAL_MERCHANT_ID= +ZARINPAL_SANDBOX=true + +# ── SMS: Kavenegar ──────────────────────────────────────────────────────────── +# Empty = OTP printed to API container logs (ok for testing) +KAVENEGAR_API_KEY= + +# ── Snappfood webhook ───────────────────────────────────────────────────────── +SNAPPFOOD_WEBHOOK_SECRET=YOUR_RANDOM_SECRET ``` -### ZarinPal / Taraz validation +> The actual generated values were set directly in Gitea during initial setup. +> To view or rotate them: Gitea → Settings → Secrets → ENV_FILE → Edit. -1. **Sandbox first:** set `ZarinPal__Sandbox=true` and a sandbox merchant; complete subscribe flow; confirm redirect `?billing=success` and JWT refresh on settings. -2. **Production:** set `ZarinPal__Sandbox=false` and production `ZarinPal__MerchantId`; verify callback URL is reachable from ZarinPal. -3. **Taraz:** with real credentials, submit from Settings → تاراز; confirm tracking in API logs (stub logs until full SDK wired). +--- -## Environment variables (Web) +### Step 2 — Trigger the first deployment +On the server: + +```bash +cd ~/meezi +git pull origin main +git push gitea main ``` + +Watch the pipeline: +``` +http://171.22.25.73:3000/soroushdes/meezi/actions +``` + +CI takes ~5–10 minutes: builds 6 Docker images, runs all checks, then deploys. + +--- + +## Service URLs (no domain, IP-based) + +| Service | URL | +|---|---| +| Marketing website | http://171.22.25.73:3010/fa | +| Cafe owner dashboard | http://171.22.25.73:3101/fa/login | +| Public finder | http://171.22.25.73:3103/fa | +| Super-admin panel | http://171.22.25.73:3102/fa/admin/login | +| Main API (Swagger) | http://171.22.25.73:5080/swagger | +| Admin API (Swagger) | http://171.22.25.73:5081/swagger | +| Gitea | http://171.22.25.73:3000 | +| Nexus | http://171.22.25.73:8081 | + +--- + +## Day-to-day: pushing code + +```bash +git add . +git commit -m "your message" +git push origin main # GitHub backup +git push gitea main # ← triggers CI + auto-deploy +``` + +Every push to `main` on Gitea runs all CI jobs. If all pass, deploy runs automatically. + +--- + +## Checking containers on the server + +```bash +# Status of all app containers +docker compose -f docker-compose.yml -f docker-compose.admin.yml ps + +# Live logs for a service +docker compose logs -f api +docker compose logs -f web +docker compose logs -f admin-api + +# All containers on the machine +docker ps +``` + +--- + +## When domain is ready (tomorrow) + +### 1. Point DNS at the server + +Create these A records — all pointing to `171.22.25.73`: + +| Hostname | Service | +|---|---| +| `meezi.ir` | Marketing website | +| `app.meezi.ir` | Cafe dashboard | +| `api.meezi.ir` | Main API | +| `finder.meezi.ir` | Finder | +| `admin.meezi.ir` | Admin panel | +| `admin-api.meezi.ir` | Admin API | + +### 2. Update `ENV_FILE` secret in Gitea + +Remove the IP-based section and replace with: + +```env +# ── Domain ──────────────────────────────────────────────────────────────────── +DOMAIN=meezi.ir +ACME_EMAIL=you@example.com + NEXT_PUBLIC_API_URL=https://api.meezi.ir +NEXT_PUBLIC_ADMIN_API_URL=https://admin-api.meezi.ir +NEXT_PUBLIC_SITE_URL=https://meezi.ir +NEXT_PUBLIC_FINDER_URL=https://finder.meezi.ir + +APP_QR_BASE_URL=https://app.meezi.ir +BILLING_DASHBOARD_URL=https://app.meezi.ir + +CORS_ORIGIN_0=https://app.meezi.ir +CORS_ORIGIN_1=https://meezi.ir +CORS_ORIGIN_2=https://finder.meezi.ir +CORS_ADMIN_ORIGIN_0=https://admin.meezi.ir + +# Remove all PORT= lines — Caddy is the only public endpoint ``` -## Routing +### 3. Update CI deploy steps -- Customer QR codes encode `https://meezi.ir/q/{qrCode}` (see `App:QrPublicBaseUrl`). -- Next.js route [`web/dashboard/src/app/q/[code]/page.tsx`](web/dashboard/src/app/q/[code]/page.tsx) resolves via public `GET /api/q/{code}`. -- Flutter app parses scanned URL and calls the same API. +In `.gitea/workflows/ci-cd.yml` find the deploy job and add `docker-compose.caddy.yml`: -## Arvan checklist +```yaml +- name: Start all services + run: | + docker compose \ + -f docker-compose.yml \ + -f docker-compose.admin.yml \ + -f docker-compose.caddy.yml \ + up -d --remove-orphans +``` -- [ ] Postgres + Redis on private network (no public ports) -- [ ] `api_uploads` persistent volume mounted at `/app/uploads` -- [ ] `RUN_MIGRATIONS=true` on API deploy only -- [ ] Hangfire `/hangfire` behind VPN or basic auth -- [ ] CORS origins: dashboard + marketing domain -- [ ] `App__QrPublicBaseUrl=https://meezi.ir` -- [ ] `Billing__DashboardBaseUrl=https://app.meezi.ir` (locale path added by API) -- [ ] TLS on load balancer for `api.*` and `app.*` -- [ ] Kavenegar + ZarinPal production keys in Arvan secrets (not in git) +Open port 80 and 443 in the server firewall: +```bash +ufw allow 80 +ufw allow 443 +``` -## Deploy steps +### 4. Push to Gitea -1. Build and push Docker images (`docker compose` Dockerfiles in `docker/`). -2. Run EF migrations on API startup (`RUN_MIGRATIONS=true`) once per release. -3. Configure Hangfire dashboard behind auth in production. -4. Smoke test: OTP login, POS terminal register, create order, menu images visible, ZarinPal subscribe (sandbox first). +```bash +git push gitea main +``` -## CI suggestion +CI rebuilds all images with the new domain URLs baked in. Caddy starts and gets +Let's Encrypt certificates automatically — no certbot or manual renewal needed. -- `dotnet build` + `dotnet test` on PR -- `npm run build` in `web/dashboard` -- Deploy on tag to Arvan registry +--- + +## Secrets to rotate before going live + +| Secret | How to generate | +|---|---| +| `JWT_KEY` | `openssl rand -hex 32` | +| `DB_PASSWORD` | Strong random password — update both `DB_PASSWORD=` and inside `DB_CONNECTION_STRING=` | +| `ZARINPAL_MERCHANT_ID` | panel.zarinpal.com → API → MerchantID | +| `ZARINPAL_SANDBOX` | Change to `false` for live payments | +| `KAVENEGAR_API_KEY` | kavenegar.com dashboard | + +After updating any secret in Gitea: push a commit to trigger a redeploy. + +--- + +## Mirror server (Nexus) + +Nexus runs separately and should always be running: + +```bash +# Start (first time or after server reboot) +docker compose -f docker-compose.mirror.yml up -d + +# Health check +curl -s http://localhost:8081/service/rest/v1/status +``` + +Provisioned repos: + +| Repo | Type | Upstream | +|---|---|---| +| `nuget-group` | NuGet group | Liara → Runflare fallback | +| `npm-group` | npm group | Liara → Runflare fallback | +| `docker-hub-proxy` | Docker proxy :5000 | Docker Hub | +| `mcr-proxy` | Docker proxy :5002 | mcr.microsoft.com | +| `pypi-proxy` | PyPI proxy | Liara | +| `ubuntu-proxy` | APT proxy | Liara (jammy) | +| `ubuntu-security-proxy` | APT proxy | Liara (jammy-security) | + +To add new mirrors or switch upstreams: +```bash +./mirrors/nexus/add-liara-mirrors.sh +./mirrors/nexus/update-docker-upstream.sh +```