419 lines
18 KiB
YAML
419 lines
18 KiB
YAML
# FlatRender V2 — full microservices stack
|
|
# Usage:
|
|
# cp .env.v2.example .env.v2
|
|
# docker compose -f docker-compose.v2.yml --env-file .env.v2 up -d
|
|
#
|
|
# Public port: 8080 → API Gateway
|
|
# MinIO UI: 9001 → http://localhost:9001
|
|
#
|
|
# Per-service ports are intentionally NOT published (internal Docker network).
|
|
# Add `ports: ["5010:8080"]` to a service to expose it for local debugging.
|
|
|
|
services:
|
|
|
|
# ── Shared infrastructure ───────────────────────────────────────────────────
|
|
|
|
postgres:
|
|
# Pull infra images through the Nexus mirror (Docker Hub blocks Iran IPs).
|
|
# Override INFRA_REGISTRY= (empty) to use plain Docker Hub names elsewhere.
|
|
image: ${INFRA_REGISTRY:-mirror.soroushasadi.com/}postgres:16-alpine
|
|
container_name: fr2-postgres
|
|
restart: unless-stopped
|
|
environment:
|
|
POSTGRES_DB: flatrender
|
|
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
|
volumes:
|
|
- pgdata:/var/lib/postgresql/data
|
|
# migrations are run once by init-db.sh when the data volume is first created
|
|
- ./backend/db/migrations:/migrations:ro
|
|
# Directory mount (NOT a single file) — robust against stale CI-workspace dirs.
|
|
- ./deploy/postgres-initdb:/docker-entrypoint-initdb.d:ro
|
|
ports:
|
|
# 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
|
|
timeout: 5s
|
|
retries: 15
|
|
start_period: 10s
|
|
|
|
minio:
|
|
# minio:latest is compiled for x86-64-v2 and crashes on baseline-CPU servers
|
|
# ("Fatal glibc error: CPU does not support x86-64-v2"). Use a `-cpuv1` build
|
|
# (same release, plain x86-64). Pulled from the Liara Docker mirror, which
|
|
# back-fills these tags (the soroushasadi mirror only has cached v2 builds).
|
|
# Dev overrides MINIO_REGISTRY= + MINIO_IMAGE_TAG=latest for plain Docker Hub.
|
|
image: ${MINIO_REGISTRY:-docker-mirror.liara.ir/}minio/minio:${MINIO_IMAGE_TAG:-RELEASE.2025-06-13T11-33-47Z-cpuv1}
|
|
container_name: fr2-minio
|
|
restart: unless-stopped
|
|
command: server /data --console-address ":9001"
|
|
environment:
|
|
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-minioadmin}
|
|
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-minioadmin}
|
|
volumes:
|
|
- miniodata:/data
|
|
ports:
|
|
- "${EDGE_BIND:-0.0.0.0}:${MINIO_PORT:-9000}:9000"
|
|
- "${EDGE_BIND:-0.0.0.0}:${MINIO_CONSOLE_PORT:-9001}:9001"
|
|
healthcheck:
|
|
# Liveness via curl (newer images) with an mc fallback (older images that
|
|
# still bundle the client). Covers minio:latest drift either way.
|
|
test: ["CMD-SHELL", "curl -sf http://localhost:9000/minio/health/live || mc ready local || exit 1"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 8
|
|
start_period: 20s
|
|
|
|
# ── Identity Service (.NET 10) ──────────────────────────────────────────────
|
|
# Config keys: ConnectionStrings:DefaultConnection Jwt:Secret
|
|
# ZarinPal:MerchantId SnapPay:ClientId Tara:ApiKey
|
|
# Stripe:SecretKey Stripe:WebhookSecret
|
|
|
|
identity-svc:
|
|
build:
|
|
context: ./services/identity
|
|
container_name: fr2-identity
|
|
restart: unless-stopped
|
|
environment:
|
|
ASPNETCORE_ENVIRONMENT: Production
|
|
ASPNETCORE_HTTP_PORTS: "8080"
|
|
ConnectionStrings__DefaultConnection: "Host=postgres;Port=5432;Database=flatrender;Username=${POSTGRES_USER:-postgres};Password=${POSTGRES_PASSWORD:-postgres};Search Path=identity,public;Pooling=true"
|
|
Jwt__Secret: "${JWT_SECRET}"
|
|
Jwt__Issuer: "flatrender-identity"
|
|
Jwt__Audience: "flatrender"
|
|
Jwt__AccessTokenMinutes: "${JWT_ACCESS_MINUTES:-1440}"
|
|
ServiceToken: "${SERVICE_TOKEN:-internal-service-secret}"
|
|
ZarinPal__MerchantId: "${ZARINPAL_MERCHANT_ID:-}"
|
|
ZarinPal__CallbackUrl: "${ZARINPAL_CALLBACK_URL:-http://localhost:8080/v1/payments/callback/zarinpal}"
|
|
ZarinPal__Sandbox: "${ZARINPAL_SANDBOX:-true}"
|
|
# FlatRender Pay broker — when ApiKey+Secret are set, plan purchases route
|
|
# through pay.flatrender.ir (the single ZarinPal-verified domain) instead of a
|
|
# direct ZarinPal call. ReturnBase = this identity service's public base.
|
|
FlatPay__BaseUrl: "${FLATPAY_BASE_URL:-https://pay.flatrender.ir}"
|
|
FlatPay__ApiKey: "${FLATPAY_FLATRENDER_API_KEY:-}"
|
|
FlatPay__Secret: "${FLATPAY_FLATRENDER_SECRET:-}"
|
|
FlatPay__ReturnBase: "${FLATPAY_RETURN_BASE:-https://api.flatrender.ir}"
|
|
Stripe__SecretKey: "${STRIPE_SECRET_KEY:-}"
|
|
Stripe__WebhookSecret: "${STRIPE_WEBHOOK_SECRET:-}"
|
|
SnapPay__ClientId: "${SNAPPAY_CLIENT_ID:-}"
|
|
SnapPay__ClientSecret: "${SNAPPAY_CLIENT_SECRET:-}"
|
|
SnapPay__BaseUrl: "${SNAPPAY_BASE_URL:-https://api.snappay.ir}"
|
|
SnapPay__CallbackUrl: "${SNAPPAY_CALLBACK_URL:-http://localhost:8080/v1/payments/callback/snappay}"
|
|
Tara__ApiKey: "${TARA_API_KEY:-}"
|
|
Tara__BaseUrl: "${TARA_BASE_URL:-https://api.tara.ir}"
|
|
Tara__CallbackUrl: "${TARA_CALLBACK_URL:-http://localhost:8080/v1/payments/callback/tara}"
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"]
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 20s
|
|
|
|
# ── Content Service (.NET 10) ───────────────────────────────────────────────
|
|
# Config keys: ConnectionStrings:Postgres Jwt:Secret
|
|
|
|
content-svc:
|
|
build:
|
|
context: ./services/content
|
|
container_name: fr2-content
|
|
restart: unless-stopped
|
|
environment:
|
|
ASPNETCORE_ENVIRONMENT: Production
|
|
ASPNETCORE_HTTP_PORTS: "8080"
|
|
ConnectionStrings__Postgres: "Host=postgres;Port=5432;Database=flatrender;Username=${POSTGRES_USER:-postgres};Password=${POSTGRES_PASSWORD:-postgres};Search Path=content,public;Pooling=true"
|
|
Jwt__Secret: "${JWT_SECRET}"
|
|
Jwt__Issuer: "flatrender-identity"
|
|
Jwt__Audience: "flatrender"
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"]
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 20s
|
|
|
|
# ── File Service (Go) ───────────────────────────────────────────────────────
|
|
|
|
file-svc:
|
|
build:
|
|
context: ./services/file
|
|
container_name: fr2-file
|
|
restart: unless-stopped
|
|
environment:
|
|
DATABASE_URL: "postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/flatrender?search_path=file_mgr,public"
|
|
JWT_SECRET: "${JWT_SECRET}"
|
|
MINIO_ENDPOINT: "minio:9000"
|
|
MINIO_ACCESS_KEY: "${MINIO_ACCESS_KEY:-minioadmin}"
|
|
MINIO_SECRET_KEY: "${MINIO_SECRET_KEY:-minioadmin}"
|
|
MINIO_USE_SSL: "false"
|
|
PORT: "8080"
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
minio:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"]
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 10s
|
|
|
|
# ── Studio Service (.NET 10) ────────────────────────────────────────────────
|
|
# Config keys: ConnectionStrings:Default Jwt:Key
|
|
|
|
studio-svc:
|
|
build:
|
|
context: ./services/studio
|
|
container_name: fr2-studio
|
|
restart: unless-stopped
|
|
environment:
|
|
ASPNETCORE_ENVIRONMENT: Production
|
|
ASPNETCORE_HTTP_PORTS: "8080"
|
|
ConnectionStrings__Default: "Host=postgres;Port=5432;Database=flatrender;Username=${POSTGRES_USER:-postgres};Password=${POSTGRES_PASSWORD:-postgres};Search Path=studio,public;Pooling=true"
|
|
Jwt__Key: "${JWT_SECRET}"
|
|
Jwt__Issuer: "flatrender-identity"
|
|
Jwt__Audience: "flatrender"
|
|
Cors__Origins__0: "${CORS_ORIGIN:-http://localhost:3000}"
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"]
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 20s
|
|
|
|
# ── Render Orchestrator (Go) ────────────────────────────────────────────────
|
|
|
|
render-svc:
|
|
build:
|
|
context: ./services/render
|
|
container_name: fr2-render
|
|
restart: unless-stopped
|
|
ports:
|
|
- "${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}"
|
|
NODE_HMAC_SECRET: "${NODE_HMAC_SECRET:-node-secret-change-me}"
|
|
# Host-reachable so presigned template/output URLs work for a host-run node-agent.
|
|
MINIO_ENDPOINT: "${MINIO_HOST_ENDPOINT:-172.28.144.1:9000}"
|
|
MINIO_ACCESS_KEY: "${MINIO_ACCESS_KEY:-minioadmin}"
|
|
MINIO_SECRET_KEY: "${MINIO_SECRET_KEY:-minioadmin}"
|
|
# 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
|
|
# MinIO endpoint above when unset).
|
|
MINIO_UPLOAD_BUCKET: "${MINIO_UPLOAD_BUCKET:-user-uploads}"
|
|
MINIO_PUBLIC_URL: "${NEXT_PUBLIC_MINIO_URL:-http://172.28.144.1:9000}"
|
|
NOTIFICATION_URL: "http://notification-svc:8080"
|
|
IDENTITY_URL: "http://identity-svc:8080"
|
|
SERVICE_TOKEN: "${SERVICE_TOKEN:-internal-service-secret}"
|
|
PORT: "8080"
|
|
# Dev: process Queued jobs in-process (progress + preview → Done) without a
|
|
# Windows AE node. Set "false" in production where real render nodes claim jobs.
|
|
RENDER_DEV_WORKER: "${RENDER_DEV_WORKER:-true}"
|
|
# Dev: fulfil scene-snapshot jobs with a generated placeholder image (no AE).
|
|
# Keep "false" in production — real nodes render the actual AE frame.
|
|
RENDER_DEV_SNAPSHOTS: "${RENDER_DEV_SNAPSHOTS:-false}"
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
minio:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"]
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 10s
|
|
|
|
# ── Notification Service (Go) ───────────────────────────────────────────────
|
|
|
|
notification-svc:
|
|
build:
|
|
context: ./services/notification
|
|
container_name: fr2-notification
|
|
restart: unless-stopped
|
|
environment:
|
|
DATABASE_URL: "postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/flatrender?search_path=notification,public"
|
|
JWT_SECRET: "${JWT_SECRET}"
|
|
SERVICE_TOKEN: "${SERVICE_TOKEN:-internal-service-secret}"
|
|
PORT: "8080"
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"]
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 10s
|
|
|
|
# ── Payment Broker (Go) — pay.flatrender.ir ─────────────────────────────────
|
|
# Standalone generic ZarinPal gateway. Other sites (meezi, bargevasat) and
|
|
# FlatRender register as client_apps and route payments through it, because
|
|
# ZarinPal only accepts callbacks on the single verified domain pay.flatrender.ir.
|
|
# Exposed on its OWN host port (mirror-nginx → pay.flatrender.ir → here);
|
|
# it does NOT sit behind the API gateway (clients auth with API key + HMAC).
|
|
|
|
payment-svc:
|
|
build:
|
|
context: ./services/payment
|
|
container_name: fr2-payment
|
|
restart: unless-stopped
|
|
ports:
|
|
# Default to the production port 1607 so the bind works without an ENV_FILE
|
|
# edit (8090 collided on the server). Override via PAY_PORT if 1607 is taken.
|
|
- "${EDGE_BIND:-0.0.0.0}:${PAY_PORT:-1607}:8080"
|
|
environment:
|
|
DATABASE_URL: "postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/flatrender?search_path=payment,public"
|
|
JWT_SECRET: "${JWT_SECRET}"
|
|
PORT: "8080"
|
|
# Externally reachable base — ZarinPal callback + user redirect are built from it.
|
|
PUBLIC_BASE_URL: "${PAY_PUBLIC_URL:-http://localhost:1607}"
|
|
# Shared default ZarinPal merchant (a client_app may override per-site).
|
|
ZARINPAL_MERCHANT_ID: "${ZARINPAL_MERCHANT_ID:-}"
|
|
ZARINPAL_SANDBOX: "${ZARINPAL_SANDBOX:-true}"
|
|
# Unit ZarinPal expects in the amount field: "rial" (official v4) or "toman".
|
|
ZARINPAL_AMOUNT_UNIT: "${ZARINPAL_AMOUNT_UNIT:-rial}"
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"]
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 10s
|
|
|
|
# ── API Gateway (Go) ────────────────────────────────────────────────────────
|
|
|
|
gateway:
|
|
build:
|
|
context: ./services/gateway
|
|
container_name: fr2-gateway
|
|
restart: unless-stopped
|
|
ports:
|
|
# EDGE_BIND/port face the reverse proxy (mirror-nginx → 171.22.25.73:PORT).
|
|
- "${EDGE_BIND:-0.0.0.0}:${GATEWAY_PORT:-8080}:8080"
|
|
environment:
|
|
JWT_SECRET: "${JWT_SECRET}"
|
|
IDENTITY_URL: "http://identity-svc:8080"
|
|
CONTENT_URL: "http://content-svc:8080"
|
|
FILE_URL: "http://file-svc:8080"
|
|
STUDIO_URL: "http://studio-svc:8080"
|
|
RENDER_URL: "http://render-svc:8080"
|
|
RENDER_WS_URL: "ws://render-svc:8080"
|
|
NOTIFICATION_URL: "http://notification-svc:8080"
|
|
PORT: "8080"
|
|
depends_on:
|
|
identity-svc:
|
|
condition: service_healthy
|
|
content-svc:
|
|
condition: service_healthy
|
|
file-svc:
|
|
condition: service_healthy
|
|
studio-svc:
|
|
condition: service_healthy
|
|
render-svc:
|
|
condition: service_healthy
|
|
notification-svc:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 5s
|
|
|
|
# ── Frontend (Next.js) ──────────────────────────────────────────────────────
|
|
# NEXT_PUBLIC_* vars are baked in at build time — pass them as build args.
|
|
# Server-side secrets are injected at runtime via environment.
|
|
|
|
frontend:
|
|
build:
|
|
context: .
|
|
args:
|
|
NEXT_PUBLIC_SITE_URL: "${NEXT_PUBLIC_SITE_URL:-http://localhost:3000}"
|
|
# V2 gateway: browser-facing base (host port) baked in at build time.
|
|
NEXT_PUBLIC_API_URL: "${NEXT_PUBLIC_API_URL:-http://localhost:${GATEWAY_PORT:-8080}/v1}"
|
|
NEXT_PUBLIC_TENANT_SLUG: "${NEXT_PUBLIC_TENANT_SLUG:-flatrender}"
|
|
NEXT_PUBLIC_MINIO_URL: "${NEXT_PUBLIC_MINIO_URL:-http://localhost:9000}"
|
|
container_name: fr2-frontend
|
|
restart: unless-stopped
|
|
ports:
|
|
- "${EDGE_BIND:-0.0.0.0}:${FRONTEND_PORT:-3000}:3000"
|
|
environment:
|
|
NODE_ENV: production
|
|
PORT: "3000"
|
|
HOSTNAME: "0.0.0.0"
|
|
# Server-side: Next route handlers reach the gateway over the internal network.
|
|
API_GATEWAY_URL: "http://gateway:8080"
|
|
# Admin proxy reaches the payment broker directly (not via the gateway).
|
|
PAYMENT_SVC_URL: "http://payment-svc:8080"
|
|
depends_on:
|
|
gateway:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000 || exit 1"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 30s
|
|
|
|
# ── Caddy (TLS reverse proxy) ───────────────────────────────────────────────
|
|
# Handles Let's Encrypt certificates and terminates HTTPS for all three
|
|
# public domains: frontend, API gateway, and MinIO storage.
|
|
#
|
|
# Required .env.v2 vars: DOMAIN, API_DOMAIN, STORAGE_DOMAIN
|
|
# Set ACME_EMAIL to a real address so Let's Encrypt can contact you.
|
|
#
|
|
# For local dev (no real domain), comment out this block and access
|
|
# services directly on their host ports (:3000, :8088, :9000).
|
|
caddy:
|
|
image: ${INFRA_REGISTRY:-mirror.soroushasadi.com/}caddy:2-alpine
|
|
container_name: fr2-caddy
|
|
restart: unless-stopped
|
|
# Opt-in only: `docker compose --profile edge up`. NOT started by default —
|
|
# on a server with an existing reverse proxy (mirror-nginx owns 80/443),
|
|
# FlatRender publishes host ports and the proxy routes the domains to them.
|
|
profiles: ["edge"]
|
|
ports:
|
|
- "80:80"
|
|
- "443:443"
|
|
- "443:443/udp" # HTTP/3 QUIC
|
|
volumes:
|
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
|
- caddy_data:/data
|
|
- caddy_config:/config
|
|
environment:
|
|
DOMAIN: "${DOMAIN:-localhost}"
|
|
API_DOMAIN: "${API_DOMAIN:-api.localhost}"
|
|
STORAGE_DOMAIN: "${STORAGE_DOMAIN:-storage.localhost}"
|
|
ACME_EMAIL: "${ACME_EMAIL:-admin@example.com}"
|
|
depends_on:
|
|
- frontend
|
|
- gateway
|
|
- minio
|
|
networks:
|
|
- default
|
|
|
|
volumes:
|
|
pgdata:
|
|
caddy_data:
|
|
caddy_config:
|
|
miniodata:
|