18cdf507f0
Docker Hub blocks Iran IPs (403), so 'docker compose up' couldn't pull the base
infra images on the server even though all service images built fine through the
mirror. Prefix them with ${INFRA_REGISTRY:-mirror.soroushasadi.com/} so they pull
through Nexus by default; set INFRA_REGISTRY= to use plain Docker Hub names.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
365 lines
15 KiB
YAML
365 lines
15 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
|
|
- ./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: ${INFRA_REGISTRY:-mirror.soroushasadi.com/}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:
|
|
- "${EDGE_BIND:-0.0.0.0}:${MINIO_PORT:-9000}:9000"
|
|
- "${EDGE_BIND:-0.0.0.0}:${MINIO_CONSOLE_PORT:-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:
|
|
# 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"
|
|
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:
|