Files
flatrender/docker-compose.v2.yml
T
soroush.asadi 18cdf507f0
CI/CD / CI · Web (tsc) (push) Successful in 1m6s
CI/CD / Deploy · full stack (push) Failing after 6s
ci(deploy): pull infra images (postgres/minio/caddy) via Nexus mirror
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>
2026-06-12 15:23:54 +03:30

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: