diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml new file mode 100644 index 0000000..bdb5ba4 --- /dev/null +++ b/.gitea/workflows/ci-cd.yml @@ -0,0 +1,118 @@ +name: CI/CD + +# Pushes to Gitea trigger this. GitHub (origin) stays a backup and does not deploy. +on: + push: + branches: [master] + pull_request: + branches: [master] + +concurrency: + group: flatrender-cicd-${{ github.ref }} + cancel-in-progress: true + +jobs: + # ── CI: fast frontend type-check before the (long) deploy build ─────────────── + web-check: + name: "CI · Web (tsc)" + runs-on: ubuntu-latest + container: + image: mirror.soroushasadi.com/node:20-alpine + options: --add-host=gitea:host-gateway + steps: + - name: Checkout (tarball) + env: + TOKEN: ${{ github.token }} + SHA: ${{ github.sha }} + run: | + wget -q --header "Authorization: Bearer ${TOKEN}" \ + "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/archive/${SHA}.tar.gz" \ + -O /tmp/repo.tar.gz + tar -xzf /tmp/repo.tar.gz --strip-components=1 + + - name: Install deps + run: | + npm ci --no-audit --no-fund \ + --registry https://mirror.soroushasadi.com/repository/npm-group/ \ + --fetch-retries=5 --fetch-retry-maxtimeout=120000 + + - name: TypeScript check + run: npx tsc --noEmit + + # ── Deploy: build + bring up the whole compose stack on the server ──────────── + deploy: + name: "Deploy · full stack" + runs-on: self-hosted + needs: [web-check] + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + timeout-minutes: 50 + env: + # act runner host mode ships a minimal PATH — extend so docker/snap resolve. + PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin + # Lock the compose project name so volumes keep a STABLE prefix across deploys + # regardless of the runner's checkout directory (prevents orphaned-volume data loss). + COMPOSE_PROJECT_NAME: flatrender + COMPOSE_FILE: docker-compose.v2.yml + DOCKER_BUILDKIT: "1" + COMPOSE_DOCKER_CLI_BUILD: "1" + steps: + - name: Checkout + env: + TOKEN: ${{ github.token }} + REF: ${{ github.ref }} + run: | + git init + git remote add origin "${{ github.server_url }}/${{ github.repository }}.git" + git config http.extraheader "Authorization: Bearer ${TOKEN}" + git fetch --depth=1 origin "${REF}" + git checkout -f FETCH_HEAD + + - name: Write .env (from ENV_FILE secret) + run: printf '%s' "$ENV_FILE" > .env + env: + ENV_FILE: ${{ secrets.ENV_FILE }} + + - name: Backup database (if running) + run: | + if docker ps -a --format '{{.Names}}' | grep -q '^fr2-postgres$'; then + mkdir -p /opt/flatrender-backups + set -a; . ./.env 2>/dev/null || true; set +a + STAMP=$(date +%Y%m%d-%H%M%S) + echo "Dumping DB → /opt/flatrender-backups/flatrender-$STAMP.sql" + docker exec fr2-postgres pg_dump -U "${POSTGRES_USER:-postgres}" flatrender \ + > "/opt/flatrender-backups/flatrender-$STAMP.sql" || echo "backup failed (non-fatal)" + # keep only the 14 most recent dumps + ls -1t /opt/flatrender-backups/flatrender-*.sql 2>/dev/null | tail -n +15 | xargs -r rm -f + else + echo "fr2-postgres not running yet — first deploy, no backup needed." + fi + + - name: Build images + run: docker compose build --parallel + + - name: Start stack + run: docker compose up -d --remove-orphans + + - name: Wait for gateway healthy + run: | + for i in $(seq 1 30); do + S=$(docker inspect --format='{{.State.Health.Status}}' fr2-gateway 2>/dev/null || echo missing) + echo " [$i/30] gateway: $S" + [ "$S" = "healthy" ] && echo "OK gateway healthy" && break + [ "$i" = "30" ] && echo "TIMEOUT gateway" && docker compose logs --tail=60 gateway content-svc identity-svc && exit 1 + sleep 6 + done + + - name: Wait for frontend healthy + run: | + for i in $(seq 1 24); do + S=$(docker inspect --format='{{.State.Health.Status}}' fr2-frontend 2>/dev/null || echo missing) + echo " [$i/24] frontend: $S" + [ "$S" = "healthy" ] && echo "OK frontend healthy" && break + [ "$i" = "24" ] && echo "TIMEOUT frontend" && docker compose logs --tail=60 frontend && exit 1 + sleep 5 + done + + - name: Prune dangling images + if: success() + run: docker image prune -f diff --git a/Dockerfile b/Dockerfile index 25fb260..fbceca6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,12 +4,13 @@ RUN apk add --no-cache libc6-compat WORKDIR /app COPY package.json package-lock.json* ./ -# The Nexus npm proxy intermittently returns 500s / corrupted tarballs while it -# back-fills its cache from upstream. Retry the whole install a few times — each -# pass re-requests only what's still missing, so successive runs converge once -# Nexus has cached every package. Bump npm's own retry budget too. +# npm installs through the self-hosted Nexus mirror (override with --build-arg +# NPM_REGISTRY=... for a different mirror). The proxy intermittently returns 500s +# / corrupted tarballs while it back-fills from upstream, so retry the whole +# install a few times — each pass re-requests only what's still missing. +ARG NPM_REGISTRY=https://mirror.soroushasadi.com/repository/npm-group/ RUN for i in 1 2 3 4 5; do \ - npm ci --registry http://171.22.25.73:8081/repository/npm-group/ \ + npm ci --registry "${NPM_REGISTRY}" \ --fetch-retries=5 --fetch-retry-factor=2 \ --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000 && exit 0; \ echo "npm ci attempt $i failed; retrying in 10s..."; sleep 10; \ diff --git a/deploy/ENV_FILE.production.example b/deploy/ENV_FILE.production.example new file mode 100644 index 0000000..dfdc737 --- /dev/null +++ b/deploy/ENV_FILE.production.example @@ -0,0 +1,73 @@ +# ───────────────────────────────────────────────────────────────────────────── +# FlatRender — PRODUCTION ENV_FILE template +# +# This is the content of the Gitea repo secret named ENV_FILE. +# Set it at: https://git.soroushasadi.com/soroushdes/flatrender/settings/secrets +# The deploy job writes this verbatim to `.env`, which docker compose reads. +# +# Fill every . Generate secrets with: openssl rand -hex 32 +# After editing the secret, push any commit to trigger a redeploy. +# Changing a NEXT_PUBLIC_* value requires a redeploy (baked into the frontend at build). +# ───────────────────────────────────────────────────────────────────────────── + +# ── Host port binding ──────────────────────────────────────────────────────── +# 127.0.0.1 keeps Postgres/MinIO/gateway/frontend OFF the public internet — only +# Caddy (80/443) is public. (Docker bypasses ufw, so this binding is the real guard.) +HOST_BIND=127.0.0.1 + +# ── Domains (DNS A-records must point at this server) ──────────────────────── +DOMAIN=flatrender.example.com +API_DOMAIN=api.flatrender.example.com +STORAGE_DOMAIN=storage.flatrender.example.com +ACME_EMAIL=you@example.com + +# ── Browser-facing URLs (baked into the frontend at build time) ────────────── +NEXT_PUBLIC_SITE_URL=https://flatrender.example.com +NEXT_PUBLIC_API_URL=https://api.flatrender.example.com/v1 +NEXT_PUBLIC_MINIO_URL=https://storage.flatrender.example.com +NEXT_PUBLIC_TENANT_SLUG=flatrender +CORS_ORIGIN=https://flatrender.example.com + +# ── Core secrets ───────────────────────────────────────────────────────────── +JWT_SECRET= +SERVICE_TOKEN= +NODE_HMAC_SECRET= +JWT_ACCESS_MINUTES=1440 + +# ── Postgres ───────────────────────────────────────────────────────────────── +POSTGRES_USER=flatrender +POSTGRES_PASSWORD= + +# ── MinIO (object storage) ─────────────────────────────────────────────────── +MINIO_ACCESS_KEY= +MINIO_SECRET_KEY= +MINIO_BUCKET=flatrender-exports +MINIO_TEMPLATES_BUCKET=flatrender-templates +MINIO_UPLOAD_BUCKET=user-uploads +# render-svc signs presigned URLs for the public storage domain (over HTTPS via Caddy): +MINIO_HOST_ENDPOINT=storage.flatrender.example.com +MINIO_HOST_USE_SSL=true + +# ── Render farm ────────────────────────────────────────────────────────────── +# No AE node on the server → keep the dev worker OFF (it would mock-complete jobs). +# Instead disable rendering in Admin → فارم رندر → موتور رندر so users see a notice. +RENDER_DEV_WORKER=false +RENDER_DEV_SNAPSHOTS=false + +# Gateway host port (bound to HOST_BIND above; public access is via API_DOMAIN/Caddy). +GATEWAY_PORT=8080 + +# ── Payments (fill the providers you actually use; leave others blank) ─────── +STRIPE_SECRET_KEY= +STRIPE_WEBHOOK_SECRET= +STRIPE_PUBLISHABLE_KEY= +ZARINPAL_MERCHANT_ID= +ZARINPAL_CALLBACK_URL=https://api.flatrender.example.com/v1/payments/callback/zarinpal +ZARINPAL_SANDBOX=false +SNAPPAY_CLIENT_ID= +SNAPPAY_CLIENT_SECRET= +SNAPPAY_BASE_URL=https://api.snappay.ir +SNAPPAY_CALLBACK_URL=https://api.flatrender.example.com/v1/payments/callback/snappay +TARA_API_KEY= +TARA_BASE_URL=https://api.tara.ir +TARA_CALLBACK_URL=https://api.flatrender.example.com/v1/payments/callback/tara diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..df54cae --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,52 @@ +# Deploying FlatRender (Gitea CI/CD → server) + +Push to **Gitea** triggers `.gitea/workflows/ci-cd.yml`: a frontend `tsc` check, then a +self-hosted `deploy` job that builds the whole compose stack and brings it up behind +Caddy (Let's Encrypt HTTPS). GitHub (`origin`) stays a backup and never deploys. + +Stack: gateway · identity · content · studio (.NET/Go) · file · render · notification +(Go) · Next.js frontend · Postgres · MinIO · Caddy. All package installs route through +`mirror.soroushasadi.com` (Nexus). + +## One-time setup (do these BEFORE the first `git push gitea master`) + +1. **DNS** — point three A-records at the server: + `DOMAIN`, `API_DOMAIN`, `STORAGE_DOMAIN` (e.g. flatrender.ir / api.flatrender.ir / storage.flatrender.ir). +2. **Firewall** — `ufw allow 22,80,443/tcp`. Everything else binds to `127.0.0.1` + (via `HOST_BIND=127.0.0.1` in the env), so only Caddy faces the internet. +3. **Gitea Actions** — enabled for this repo, and an `act_runner` is registered with the + `self-hosted:host` label (the standard server already has this). +4. **ENV_FILE secret** — at `…/soroushdes/flatrender/settings/secrets`, create `ENV_FILE` + with the filled-in contents of [`ENV_FILE.production.example`](./ENV_FILE.production.example) + (generate each secret with `openssl rand -hex 32`). +5. **Server prerequisites** (already true on the Gitea+Nexus box): Docker + compose v2, + `/etc/docker/daemon.json` has `{"registry-mirrors":["https://mirror.soroushasadi.com"]}`. + +## Go live + +```bash +git push gitea master # triggers CI + deploy +``` + +Watch: `https://git.soroushasadi.com/soroushdes/flatrender/actions`. +First run is the slowest (cold Nexus cache + all images build, ~15–25 min). Caddy issues +TLS certs on first boot. Then visit `https://DOMAIN`. + +## First-run notes + +- **Migrations** auto-run once via `scripts/init-db.sh` when the Postgres volume is first + created. Later schema changes are applied manually with `psql` (the data volume persists). +- **Admin seed** — create the first admin per the project's identity seed flow, then log in + at `https://DOMAIN` → admin. +- **Rendering** — there is no After Effects node on the server, so `RENDER_DEV_WORKER=false`. + Disable rendering in **Admin → فارم رندر → موتور رندر** so users see an "unavailable" notice + instead of jobs that never finish. (Point real render nodes at the server later.) +- **MinIO public URLs** — verify an uploaded image and a render download resolve over + `https://STORAGE_DOMAIN`. If not, recheck `MINIO_HOST_ENDPOINT` / `MINIO_HOST_USE_SSL` / + `NEXT_PUBLIC_MINIO_URL` in the secret and redeploy. + +## Redeploy / rotate secrets + +Edit `ENV_FILE` in Gitea (or push any commit) → the deploy job re-runs. It backs up the DB +to `/opt/flatrender-backups/` before each deploy and never runs `docker compose down -v`. +Changing a `NEXT_PUBLIC_*` value only takes effect after the redeploy (baked at build time). diff --git a/docker-compose.v2.yml b/docker-compose.v2.yml index 0b5602a..349fd52 100644 --- a/docker-compose.v2.yml +++ b/docker-compose.v2.yml @@ -27,7 +27,9 @@ services: - ./backend/db/migrations:/migrations:ro - ./scripts/init-db.sh:/docker-entrypoint-initdb.d/00-init.sh:ro ports: - - "5432:5432" + # HOST_BIND=127.0.0.1 in prod keeps these off the public interface (only + # Caddy's 80/443 face the internet). Unset → 0.0.0.0 for local/LAN dev. + - "${HOST_BIND:-0.0.0.0}:5432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d flatrender"] interval: 5s @@ -46,8 +48,8 @@ services: volumes: - miniodata:/data ports: - - "9000:9000" - - "9001:9001" + - "${HOST_BIND:-0.0.0.0}:9000:9000" + - "${HOST_BIND:-0.0.0.0}:9001:9001" healthcheck: test: ["CMD-SHELL", "mc ready local || exit 1"] interval: 10s @@ -182,7 +184,7 @@ services: container_name: fr2-render restart: unless-stopped ports: - - "5010:8080" # exposed so a LOCAL (host) node-agent can reach /v1/internal/* + - "${HOST_BIND:-0.0.0.0}:5010:8080" # exposed so a LOCAL (host) node-agent can reach /v1/internal/* environment: DATABASE_URL: "postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/flatrender?search_path=render,public" JWT_SECRET: "${JWT_SECRET}" @@ -191,7 +193,8 @@ services: MINIO_ENDPOINT: "${MINIO_HOST_ENDPOINT:-172.28.144.1:9000}" MINIO_ACCESS_KEY: "${MINIO_ACCESS_KEY:-minioadmin}" MINIO_SECRET_KEY: "${MINIO_SECRET_KEY:-minioadmin}" - MINIO_USE_SSL: "false" + # SSL on when MINIO_HOST_ENDPOINT is an HTTPS storage domain (prod via Caddy). + MINIO_USE_SSL: "${MINIO_HOST_USE_SSL:-false}" MINIO_BUCKET: "${MINIO_BUCKET:-flatrender-exports}" # Scene snapshots upload to this public-read bucket; PUBLIC_URL is the # browser-reachable base for the stored snapshot_url (defaults to the host @@ -250,7 +253,7 @@ services: container_name: fr2-gateway restart: unless-stopped ports: - - "${GATEWAY_PORT:-8080}:8080" + - "${HOST_BIND:-0.0.0.0}:${GATEWAY_PORT:-8080}:8080" environment: JWT_SECRET: "${JWT_SECRET}" IDENTITY_URL: "http://identity-svc:8080" @@ -297,7 +300,7 @@ services: container_name: fr2-frontend restart: unless-stopped ports: - - "3000:3000" + - "${HOST_BIND:-0.0.0.0}:3000:3000" environment: NODE_ENV: production PORT: "3000" diff --git a/services/content/NuGet.Config b/services/content/NuGet.Config index eb8c162..cb14dd1 100644 --- a/services/content/NuGet.Config +++ b/services/content/NuGet.Config @@ -2,6 +2,6 @@ - + diff --git a/services/identity/NuGet.Config b/services/identity/NuGet.Config index eb8c162..cb14dd1 100644 --- a/services/identity/NuGet.Config +++ b/services/identity/NuGet.Config @@ -2,6 +2,6 @@ - + diff --git a/services/studio/NuGet.Config b/services/studio/NuGet.Config index eb8c162..cb14dd1 100644 --- a/services/studio/NuGet.Config +++ b/services/studio/NuGet.Config @@ -2,6 +2,6 @@ - +