Files
flatrender/docker-compose.v2.yml
T
soroush.asadi 127f40e1c1
CI/CD / CI · Web (tsc) (push) Successful in 1m8s
CI/CD / Deploy · full stack (push) Failing after 1m41s
ci: Gitea CI/CD pipeline + server deploy (Nexus mirror, Caddy HTTPS)
- .gitea/workflows/ci-cd.yml: frontend tsc check → self-hosted deploy job that
  builds the full compose stack and brings it up behind Caddy. Locks
  COMPOSE_PROJECT_NAME=flatrender (stable volumes), backs up the DB before each
  deploy, health-waits gateway+frontend, no `down -v`.
- Route all package installs through mirror.soroushasadi.com:
  frontend Dockerfile npm registry → NPM_REGISTRY build arg (Nexus default);
  3× NuGet.Config (content/identity/studio) → HTTPS nuget-group (were a bare IP).
- Harden host ports: ${HOST_BIND:-0.0.0.0} prefix on postgres/minio/render/gateway/
  frontend so prod (HOST_BIND=127.0.0.1) keeps them off the public internet — only
  Caddy 80/443 is public. Dev (unset → 0.0.0.0) unchanged.
- render-svc MINIO_USE_SSL now env-driven (MINIO_HOST_USE_SSL) for HTTPS storage domain.
- deploy/ENV_FILE.production.example (the Gitea secret template) + deploy/README.md
  (one-time setup + go-live checklist).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:29:09 +03:30

358 lines
14 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:
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:
# 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:
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:
- "${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
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:
- "${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
# ── API Gateway (Go) ────────────────────────────────────────────────────────
gateway:
build:
context: ./services/gateway
container_name: fr2-gateway
restart: unless-stopped
ports:
- "${HOST_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:
- "${HOST_BIND:-0.0.0.0}: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: