docker-compose.yml
Rama main · réplica de lectura del repositorio original.
# ─── NovelWorld microservice stack ───────────────────────────────────────────
# Service topology:
# nginx (80) → gateway (8080) → [novel-service, agent-service,
# narrative-service, user-service]
# The default stack uses PostgreSQL 18; the Redis projection uses the explicit redis profile.
x-logging: &bounded-logs
driver: local
networks:
novel-net:
driver: bridge
volumes:
postgres_data:
redis_data:
embedding_model_cache:
services:
# ─── Infrastructure ────────────────────────────────────────────────────────
postgres:
image: ${POSTGRES_IMAGE:-pgvector/pgvector:pg18@sha256:2ba9ca5f2e7daa0f0e7723cba1ee9167bab54efd3640516a44ac1a928dd67e7a}
container_name: ${CONTAINER_PREFIX:-novel}-postgres
restart: unless-stopped
logging: *bounded-logs
environment:
POSTGRES_USER: ${POSTGRES_USER:-novel}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
POSTGRES_DB: ${POSTGRES_DB:-novel_world}
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- postgres_data:/var/lib/postgresql/data
- ./infra/postgres/init.sql:/docker-entrypoint-initdb.d/01_init.sql:ro
# ports:
# - "5432:5432" # uncomment for local dev
networks:
- novel-net
healthcheck:
# pg_isready also succeeds against the temporary server used by the
# official entrypoint while init scripts are still running. PID 1 becomes
# postgres only after initialization completes.
test: ["CMD-SHELL", "test \"$$(cat /proc/1/comm)\" = postgres && pg_isready -U ${POSTGRES_USER:-novel} -d ${POSTGRES_DB:-novel_world}"]
interval: 10s
timeout: 5s
retries: 5
postgres-migrate:
image: ${POSTGRES_IMAGE:-pgvector/pgvector:pg18@sha256:2ba9ca5f2e7daa0f0e7723cba1ee9167bab54efd3640516a44ac1a928dd67e7a}
restart: "no"
logging: *bounded-logs
environment:
PGHOST: postgres
PGUSER: ${POSTGRES_USER:-novel}
PGPASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
PGDATABASE: ${POSTGRES_DB:-novel_world}
volumes:
- ./infra/postgres/migrations:/migrations:ro
depends_on:
postgres:
condition: service_healthy
command:
- /bin/sh
- -ec
- |
for migration in /migrations/*.sql; do
echo "Applying $$migration"
psql -v ON_ERROR_STOP=1 -f "$$migration"
done
networks:
- novel-net
redis:
profiles: ["redis"]
image: ${REDIS_IMAGE:-redis:8.10.2-alpine@sha256:3811787313eba226a2ef38658c6ccb91cd5e110edc89c37767de373120a0e5a0}
container_name: ${CONTAINER_PREFIX:-novel}-redis
restart: unless-stopped
logging: *bounded-logs
environment:
CACHE_MODE: ${CACHE_MODE:-postgres}
REDIS_PASSWORD: ${REDIS_PASSWORD:-}
entrypoint: ["/bin/sh", "-ec"]
command:
- |
mode=$${CACHE_MODE:-}
[ "$$mode" = redis ] \
|| { echo 'Redis profile requires CACHE_MODE=redis' >&2; exit 64; }
password=$${REDIS_PASSWORD:-}
lower=$$(printf '%s' "$$password" | tr '[:upper:]' '[:lower:]')
distinct=$$(printf '%s' "$$password" | fold -w1 | sort -u | wc -l | tr -d ' ')
[ "$${#password}" -ge 16 ] \
&& [ "$$distinct" -ge 8 ] \
&& case "$$password" in *[!A-Za-z0-9._~-]*) false;; *) true;; esac \
&& case "$$lower" in *placeholder*|*change_me*|your_redis_password_here|runtime-redis-only) false;; *) true;; esac \
|| { echo 'REDIS_PASSWORD must be URL-safe, non-placeholder, at least 16 characters, and contain at least 8 distinct characters' >&2; exit 64; }
exec docker-entrypoint.sh redis-server --requirepass "$$password" --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
# ports:
# - "6379:6379" # uncomment for local dev
networks:
- novel-net
healthcheck:
test: ["CMD", "redis-cli", "--no-auth-warning", "-a", "${REDIS_PASSWORD:-}", "ping"]
interval: 10s
timeout: 5s
retries: 5
embedding:
profiles: ["local-embedding"]
image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.9.4@sha256:2538ea1c9640d3763b15af668039d24172d063b42337b0c27796fc2be180c78d
container_name: ${CONTAINER_PREFIX:-novel}-embedding
restart: unless-stopped
logging: *bounded-logs
environment:
RAYON_NUM_THREADS: "4"
MKL_NUM_THREADS: "4"
command:
- --model-id
- Qwen/Qwen3-Embedding-0.6B
- --revision
- 97b0c614be4d77ee51c0cef4e5f07c00f9eb65b3
- --served-model-name
- Qwen/Qwen3-Embedding-0.6B
- --dtype
- float32
- --max-concurrent-requests
- "4"
- --max-batch-tokens
- "4096"
- --max-client-batch-size
- "1"
- --payload-limit
- "8192"
- --json-output
volumes:
- embedding_model_cache:/data
networks:
- novel-net
read_only: true
tmpfs:
- /tmp:size=64m,mode=1777
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
mem_limit: 12g
cpus: 4.0
# ─── Services ──────────────────────────────────────────────────────────────
user-service:
image: ${USER_SERVICE_IMAGE:-novel-world-user-service:local}
build:
context: .
dockerfile: infra/docker/Dockerfile.rust-service
args:
SERVICE: user-service
container_name: ${CONTAINER_PREFIX:-novel}-user-service
restart: unless-stopped
logging: *bounded-logs
environment:
DATABASE_URL: postgres://${POSTGRES_USER:-novel}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}@postgres:5432/${POSTGRES_DB:-novel_world}
JWT_SECRET: ${JWT_SECRET:?JWT_SECRET is required}
RUNTIME_CONFIG_KEY: ${RUNTIME_CONFIG_KEY:?RUNTIME_CONFIG_KEY is required}
INTERNAL_SERVICE_TOKEN: ${INTERNAL_SERVICE_TOKEN:?INTERNAL_SERVICE_TOKEN is required}
AGENT_SERVICE_URL: http://agent-service:8003
USER_SERVICE_URL: http://127.0.0.1:8001
LLM_DIAGNOSTIC_BUDGET_ID: ${LLM_DIAGNOSTIC_BUDGET_ID:-}
LLM_DIAGNOSTIC_BUDGET_LIMITS: ${LLM_DIAGNOSTIC_BUDGET_LIMITS:-}
LLM_DIAGNOSTIC_PROFILE: ${LLM_DIAGNOSTIC_PROFILE:-vision-journey-diagnostic-v1}
LLM_API_URL: ${LLM_API_URL:-https://api.openai.com}
LLM_API_KEY: ${LLM_API_KEY:-}
LLM_MODEL: ${LLM_MODEL:-gpt-4o-mini}
LLM_PRICING_USD_PER_MILLION: '${LLM_PRICING_USD_PER_MILLION:-{}}'
LLM_PRICING_CNY_PER_MILLION: '${LLM_PRICING_CNY_PER_MILLION:-{}}'
USD_CNY_RATE: ${USD_CNY_RATE:-}
PROMETHEUS_URL: ${PROMETHEUS_URL:-http://prometheus:9090}
AUTH_ACCESS_TOKEN_EXPIRY: ${AUTH_ACCESS_TOKEN_EXPIRY:-3600}
AUTH_REFRESH_TOKEN_EXPIRY: ${AUTH_REFRESH_TOKEN_EXPIRY:-604800}
PORT: 8001
RUST_LOG: ${RUST_LOG:-info}
depends_on:
postgres-migrate:
condition: service_completed_successfully
networks:
- novel-net
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8001/ready"]
interval: 15s
timeout: 5s
retries: 3
novel-service:
image: ${NOVEL_SERVICE_IMAGE:-novel-world-novel-service:local}
build:
context: .
dockerfile: infra/docker/Dockerfile.rust-service
args:
SERVICE: novel-service
container_name: ${CONTAINER_PREFIX:-novel}-novel-service
restart: unless-stopped
logging: *bounded-logs
environment:
DATABASE_URL: postgres://${POSTGRES_USER:-novel}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}@postgres:5432/${POSTGRES_DB:-novel_world}
LLM_API_URL: ${LLM_API_URL:-https://api.openai.com}
LLM_API_KEY: ${LLM_API_KEY:-}
LLM_MODEL: ${LLM_MODEL:-gpt-4o-mini}
LLM_CHARACTER_OUTPUT_MODE: ${LLM_CHARACTER_OUTPUT_MODE:-legacy-json}
LAYA_API_URL: ${LAYA_API_URL:-}
LAYA_API_KEY: ${LAYA_API_KEY:-}
USER_SERVICE_URL: http://user-service:8001
LLM_DIAGNOSTIC_BUDGET_ID: ${LLM_DIAGNOSTIC_BUDGET_ID:-}
LLM_DIAGNOSTIC_PROFILE: ${LLM_DIAGNOSTIC_PROFILE:-vision-journey-diagnostic-v1}
INTERNAL_SERVICE_TOKEN: ${INTERNAL_SERVICE_TOKEN:?INTERNAL_SERVICE_TOKEN is required}
AGENT_SERVICE_URL: http://agent-service:8003
IMAGE_GEN_API_URL: ${IMAGE_GEN_API_URL:-https://api.openai.com}
IMAGE_GEN_API_KEY: ${IMAGE_GEN_API_KEY:-}
S3_ENABLED: ${S3_ENABLED:-false}
S3_ENDPOINT: ${S3_ENDPOINT:-}
S3_BUCKET: ${S3_BUCKET:-novel-world-uploads}
S3_REGION: ${S3_REGION:-us-east-1}
S3_FORCE_PATH_STYLE: ${S3_FORCE_PATH_STYLE:-false}
S3_ACCESS_KEY: ${S3_ACCESS_KEY:-}
S3_SECRET_KEY: ${S3_SECRET_KEY:-}
S3_SESSION_TOKEN: ${S3_SESSION_TOKEN:-}
PORT: 8002
RUST_LOG: ${RUST_LOG:-info}
depends_on:
postgres-migrate:
condition: service_completed_successfully
networks:
- novel-net
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8002/ready"]
interval: 15s
timeout: 5s
retries: 3
agent-service:
image: ${AGENT_SERVICE_IMAGE:-novel-world-agent-service:local}
build:
context: .
dockerfile: infra/docker/Dockerfile.rust-service
args:
SERVICE: agent-service
container_name: ${CONTAINER_PREFIX:-novel}-agent-service
restart: unless-stopped
logging: *bounded-logs
environment:
DATABASE_URL: postgres://${POSTGRES_USER:-novel}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}@postgres:5432/${POSTGRES_DB:-novel_world}
CACHE_MODE: ${CACHE_MODE:-postgres}
REDIS_URL: ${REDIS_URL:-memory://}
LLM_API_URL: ${LLM_API_URL:-https://api.openai.com}
LLM_API_KEY: ${LLM_API_KEY:-}
LLM_MODEL: ${LLM_MODEL:-gpt-4o}
USER_SERVICE_URL: http://user-service:8001
LLM_DIAGNOSTIC_BUDGET_ID: ${LLM_DIAGNOSTIC_BUDGET_ID:-}
LLM_DIAGNOSTIC_PROFILE: ${LLM_DIAGNOSTIC_PROFILE:-vision-journey-diagnostic-v1}
INTERNAL_SERVICE_TOKEN: ${INTERNAL_SERVICE_TOKEN:?INTERNAL_SERVICE_TOKEN is required}
EMBEDDING_PROVIDER: ${EMBEDDING_PROVIDER:-}
EMBEDDING_API_URL: ${EMBEDDING_API_URL:-}
EMBEDDING_API_KEY: ${EMBEDDING_API_KEY:-}
EMBEDDING_MODEL: ${EMBEDDING_MODEL:-}
PORT: 8003
NOVEL_SERVICE_URL: http://novel-service:8002
NARRATIVE_SERVICE_URL: http://narrative-service:8004
RUST_LOG: ${RUST_LOG:-info}
depends_on:
postgres-migrate:
condition: service_completed_successfully
novel-service:
condition: service_healthy
narrative-service:
condition: service_healthy
networks:
- novel-net
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8003/ready"]
interval: 15s
timeout: 5s
retries: 3
narrative-service:
image: ${NARRATIVE_SERVICE_IMAGE:-novel-world-narrative-service:local}
build:
context: .
dockerfile: infra/docker/Dockerfile.rust-service
args:
SERVICE: narrative-service
container_name: ${CONTAINER_PREFIX:-novel}-narrative-service
restart: unless-stopped
logging: *bounded-logs
environment:
DATABASE_URL: postgres://${POSTGRES_USER:-novel}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}@postgres:5432/${POSTGRES_DB:-novel_world}
LLM_API_URL: ${LLM_API_URL:-https://api.openai.com}
LLM_API_KEY: ${LLM_API_KEY:-}
LLM_MODEL: ${LLM_MODEL:-gpt-4o}
LAYA_API_URL: ${LAYA_API_URL:-}
LAYA_API_KEY: ${LAYA_API_KEY:-}
USER_SERVICE_URL: http://user-service:8001
LLM_DIAGNOSTIC_BUDGET_ID: ${LLM_DIAGNOSTIC_BUDGET_ID:-}
LLM_DIAGNOSTIC_PROFILE: ${LLM_DIAGNOSTIC_PROFILE:-vision-journey-diagnostic-v1}
INTERNAL_SERVICE_TOKEN: ${INTERNAL_SERVICE_TOKEN:?INTERNAL_SERVICE_TOKEN is required}
PORT: 8004
NOVEL_SERVICE_URL: http://novel-service:8002
AGENT_SERVICE_URL: http://agent-service:8003
RUST_LOG: ${RUST_LOG:-info}
depends_on:
postgres-migrate:
condition: service_completed_successfully
novel-service:
condition: service_healthy
networks:
- novel-net
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8004/ready"]
interval: 15s
timeout: 5s
retries: 3
# ─── API Gateway ───────────────────────────────────────────────────────────
gateway:
image: ${GATEWAY_IMAGE:-novel-world-gateway:local}
build:
context: .
dockerfile: infra/docker/Dockerfile.rust-service
args:
SERVICE: gateway
container_name: ${CONTAINER_PREFIX:-novel}-gateway
restart: unless-stopped
logging: *bounded-logs
environment:
USER_SERVICE_URL: http://user-service:8001
NOVEL_SERVICE_URL: http://novel-service:8002
AGENT_SERVICE_URL: http://agent-service:8003
NARRATIVE_SERVICE_URL: http://narrative-service:8004
JWT_SECRET: ${JWT_SECRET:?JWT_SECRET is required}
INTERNAL_SERVICE_TOKEN: ${INTERNAL_SERVICE_TOKEN:?INTERNAL_SERVICE_TOKEN is required}
PORT: 8080
RATE_LIMIT_RPS: ${RATE_LIMIT_RPS:-500}
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:5173,http://127.0.0.1:5173,http://localhost,http://127.0.0.1}
RUST_LOG: ${RUST_LOG:-info}
# ports:
# - "8080:8080" # uncomment for local dev
depends_on:
user-service:
condition: service_healthy
novel-service:
condition: service_healthy
agent-service:
condition: service_healthy
narrative-service:
condition: service_healthy
networks:
- novel-net
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/ready"]
interval: 15s
timeout: 5s
retries: 3
# ─── Frontend ──────────────────────────────────────────────────────────────
frontend:
image: ${FRONTEND_IMAGE:-novel-world-frontend:local}
build:
context: ./frontend
dockerfile: Dockerfile
args:
VITE_API_URL: ${VITE_API_URL:-/api}
container_name: ${CONTAINER_PREFIX:-novel}-frontend
restart: unless-stopped
logging: *bounded-logs
networks:
- novel-net
# ─── Nginx reverse proxy ───────────────────────────────────────────────────
nginx:
image: ${NGINX_IMAGE:-nginx:alpine@sha256:060849a174666c53439dc7cecbf8f67e225c7cbfb1a44ca97915d9a172a81e6d}
container_name: ${CONTAINER_PREFIX:-novel}-nginx
restart: unless-stopped
logging: *bounded-logs
ports:
- "${NGINX_HTTP_BIND:-127.0.0.1}:${NGINX_HTTP_PORT:-80}:80"
volumes:
- ./infra/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- gateway
- frontend
networks:
- novel-net