feat: V2 microservices stack — backend services, gateway, JWT auth
Add full V2 architecture: identity, content, studio (.NET 10) and file, render, notification, gateway (Go) services with vendored deps, plus DB migrations, event/API contracts, and an init-db script. Wire the Next.js frontend to the gateway: server-side JWT auth routes (login/register/refresh/logout/me), gateway fetch helper, and session/ cookie/jwt helpers under src/lib. Containerize the stack via docker-compose.v2.yml and per-service Dockerfiles. Base images resolve through a Nexus mirror (Docker Hub) and MCR directly; npm/NuGet pull from Nexus groups. Self-host fonts via next/font/local to avoid Google Fonts (geo-blocked). Add CI workflow and ignore .env.v2, *.stackdump, and .NET bin/obj. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
# 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"
|
||||
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"
|
||||
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"
|
||||
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
|
||||
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}"
|
||||
MINIO_ENDPOINT: "minio: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}"
|
||||
NOTIFICATION_URL: "http://notification-svc:8080"
|
||||
SERVICE_TOKEN: "${SERVICE_TOKEN:-internal-service-secret}"
|
||||
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
|
||||
|
||||
# ── 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_SUPABASE_URL: "${NEXT_PUBLIC_SUPABASE_URL:-}"
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: "${NEXT_PUBLIC_SUPABASE_ANON_KEY:-}"
|
||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: "${NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY:-}"
|
||||
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:-8088}/v1}"
|
||||
NEXT_PUBLIC_TENANT_SLUG: "${NEXT_PUBLIC_TENANT_SLUG:-flatrender}"
|
||||
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"
|
||||
# Server-side secrets (not baked into image)
|
||||
SUPABASE_SERVICE_ROLE_KEY: "${SUPABASE_SERVICE_ROLE_KEY:-}"
|
||||
STRIPE_SECRET_KEY: "${STRIPE_SECRET_KEY:-}"
|
||||
STRIPE_WEBHOOK_SECRET: "${STRIPE_WEBHOOK_SECRET:-}"
|
||||
depends_on:
|
||||
gateway:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:3000 || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
miniodata:
|
||||
Reference in New Issue
Block a user