# 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: image: 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 - ./scripts/init-db.sh:/docker-entrypoint-initdb.d/00-init.sh:ro ports: - "5432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d flatrender"] interval: 5s timeout: 5s retries: 15 start_period: 10s minio: image: minio/minio:latest 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: - "9000:9000" - "9001:9001" healthcheck: test: ["CMD-SHELL", "mc ready local || exit 1"] interval: 10s timeout: 5s retries: 5 start_period: 10s # ── 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}" 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: - "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}" MINIO_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 # ── API Gateway (Go) ──────────────────────────────────────────────────────── gateway: build: context: ./services/gateway container_name: fr2-gateway restart: unless-stopped ports: - "${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: - "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" 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: caddy:2-alpine container_name: fr2-caddy restart: unless-stopped 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: