name: ${COMPOSE_PROJECT_NAME} services: # --- Infrastructure --- postgres: image: postgres:17-alpine container_name: ${COMPOSE_PROJECT_NAME}_db environment: - POSTGRES_PASSWORD=${DB_PASSWORD} ports: - "${DB_PORT}:5432" volumes: - db_data:/var/lib/postgresql/data networks: [app_net] healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s redis: image: redis:7-alpine container_name: ${COMPOSE_PROJECT_NAME}_redis ports: - "${REDIS_PORT}:6379" networks: [app_net] clickhouse: image: clickhouse/clickhouse-server:latest container_name: ${COMPOSE_PROJECT_NAME}_clickhouse environment: - CLICKHOUSE_USER=baron - CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD} ports: - "${CLICKHOUSE_PORT_HTTP}:8123" - "${CLICKHOUSE_PORT_NATIVE}:9000" volumes: - clickhouse_data:/var/lib/clickhouse networks: [app_net] # --- Ory Stack --- postgres_ory: image: postgres:${ORY_POSTGRES_TAG:-17-alpine} container_name: ${COMPOSE_PROJECT_NAME}_ory_db environment: - POSTGRES_USER=${ORY_POSTGRES_USER:-ory} - POSTGRES_PASSWORD=${ORY_POSTGRES_PASSWORD} - POSTGRES_DB=${ORY_POSTGRES_DB:-ory} volumes: - ory_db_data:/var/lib/postgresql/data - ./ory/init-db:/docker-entrypoint-initdb.d:ro networks: [app_net] healthcheck: test: ["CMD-SHELL", "pg_isready -U ${ORY_POSTGRES_USER:-ory} -d ${KRATOS_DB:-ory_kratos}"] interval: 5s kratos-migrate: image: oryd/kratos:${KRATOS_VERSION:-v26.2.0} env_file: .env environment: - DSN=postgres://${ORY_POSTGRES_USER:-ory}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KRATOS_DB:-ory_kratos}?sslmode=disable&max_conns=20 - KRATOS_SERVE_PUBLIC_BASE_URL=${KRATOS_BROWSER_URL} - KRATOS_SERVE_ADMIN_BASE_URL=${KRATOS_ADMIN_URL:-http://kratos:4434} - KRATOS_SELFSERVICE_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL} - KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=${KRATOS_ALLOWED_RETURN_URLS_JSON:-["${KRATOS_UI_URL}","${KRATOS_UI_URL}/","${USERFRONT_URL}","${USERFRONT_URL}/","${USERFRONT_URL}/ko","${USERFRONT_URL}/ko/","${USERFRONT_URL}/en","${USERFRONT_URL}/en/","${USERFRONT_URL}/auth/callback","${USERFRONT_URL}/ko/auth/callback","${USERFRONT_URL}/en/auth/callback","${ADMINFRONT_URL}/auth/callback","${DEVFRONT_URL}/auth/callback","${ORGFRONT_URL}/auth/callback"]} - KRATOS_SELFSERVICE_FLOWS_ERROR_UI_URL=${KRATOS_UI_URL}/error - KRATOS_SELFSERVICE_FLOWS_SETTINGS_UI_URL=${KRATOS_UI_URL}/error?error=settings_disabled - KRATOS_SELFSERVICE_FLOWS_RECOVERY_UI_URL=${KRATOS_UI_URL}/recovery - KRATOS_SELFSERVICE_FLOWS_VERIFICATION_UI_URL=${KRATOS_UI_URL}/verification - KRATOS_SELFSERVICE_FLOWS_LOGIN_UI_URL=${KRATOS_UI_URL}/login - KRATOS_SELFSERVICE_FLOWS_REGISTRATION_UI_URL=${KRATOS_UI_URL}/registration - KRATOS_SELFSERVICE_FLOWS_LOGOUT_AFTER_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL}/login volumes: - ./config/.generated/ory/kratos:/etc/config/kratos:ro command: migrate sql up -e -c /etc/config/kratos/kratos.yml --yes networks: [app_net] depends_on: postgres_ory: { condition: service_healthy } kratos: image: oryd/kratos:${KRATOS_VERSION:-v26.2.0} container_name: ${COMPOSE_PROJECT_NAME}_kratos env_file: .env environment: - DSN=postgres://${ORY_POSTGRES_USER:-ory}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KRATOS_DB:-ory_kratos}?sslmode=disable&max_conns=20 - COOKIE_SECRET=${COOKIE_SECRET} - KRATOS_SERVE_PUBLIC_BASE_URL=${KRATOS_BROWSER_URL} - KRATOS_SERVE_ADMIN_BASE_URL=${KRATOS_ADMIN_URL:-http://kratos:4434} - KRATOS_SELFSERVICE_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL} - KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=${KRATOS_ALLOWED_RETURN_URLS_JSON:-["${KRATOS_UI_URL}","${KRATOS_UI_URL}/","${USERFRONT_URL}","${USERFRONT_URL}/","${USERFRONT_URL}/ko","${USERFRONT_URL}/ko/","${USERFRONT_URL}/en","${USERFRONT_URL}/en/","${USERFRONT_URL}/auth/callback","${USERFRONT_URL}/ko/auth/callback","${USERFRONT_URL}/en/auth/callback","${ADMINFRONT_URL}/auth/callback","${DEVFRONT_URL}/auth/callback","${ORGFRONT_URL}/auth/callback"]} - KRATOS_SELFSERVICE_FLOWS_ERROR_UI_URL=${KRATOS_UI_URL}/error - KRATOS_SELFSERVICE_FLOWS_SETTINGS_UI_URL=${KRATOS_UI_URL}/error?error=settings_disabled - KRATOS_SELFSERVICE_FLOWS_RECOVERY_UI_URL=${KRATOS_UI_URL}/recovery - KRATOS_SELFSERVICE_FLOWS_VERIFICATION_UI_URL=${KRATOS_UI_URL}/verification - KRATOS_SELFSERVICE_FLOWS_LOGIN_UI_URL=${KRATOS_UI_URL}/login - KRATOS_SELFSERVICE_FLOWS_REGISTRATION_UI_URL=${KRATOS_UI_URL}/registration - KRATOS_SELFSERVICE_FLOWS_LOGOUT_AFTER_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL}/login volumes: - ./config/.generated/ory/kratos:/etc/config/kratos:ro command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier networks: [app_net] depends_on: kratos-migrate: { condition: service_completed_successfully } hydra-migrate: image: oryd/hydra:${HYDRA_VERSION:-v26.2.0} env_file: .env environment: - DSN=postgres://${ORY_POSTGRES_USER:-ory}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${HYDRA_DB:-ory_hydra}?sslmode=disable&max_conns=20 command: migrate sql up -e --yes networks: [app_net] depends_on: postgres_ory: { condition: service_healthy } hydra: image: oryd/hydra:${HYDRA_VERSION:-v26.2.0} container_name: ${COMPOSE_PROJECT_NAME}_hydra env_file: .env environment: - DSN=postgres://${ORY_POSTGRES_USER:-ory}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${HYDRA_DB:-ory_hydra}?sslmode=disable&max_conns=20 - URLS_SELF_ISSUER=${HYDRA_PUBLIC_URL} - URLS_LOGIN=${HYDRA_LOGIN_URL:-${USERFRONT_URL}/login} - URLS_CONSENT=${HYDRA_CONSENT_URL:-${USERFRONT_URL}/consent} - URLS_ERROR=${HYDRA_ERROR_URL:-${USERFRONT_URL}/error} - SECRETS_SYSTEM=${ORY_POSTGRES_PASSWORD} volumes: - ./config/.generated/ory/hydra:/etc/config/hydra:ro command: serve -c /etc/config/hydra/hydra.yml all --dev networks: [app_net] depends_on: hydra-migrate: { condition: service_completed_successfully } keto-migrate: image: oryd/keto:${KETO_VERSION:-v26.2.0} env_file: .env environment: - DSN=postgres://${ORY_POSTGRES_USER:-ory}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KETO_DB:-ory_keto}?sslmode=disable&max_conns=20 volumes: - ./config/.generated/ory/keto:/etc/config/keto:ro command: ["migrate", "up", "-c", "/etc/config/keto/keto.yml", "--yes"] networks: [app_net] depends_on: postgres_ory: { condition: service_healthy } keto: image: oryd/keto:${KETO_VERSION:-v26.2.0} container_name: ${COMPOSE_PROJECT_NAME}_keto env_file: .env environment: - DSN=postgres://${ORY_POSTGRES_USER:-ory}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KETO_DB:-ory_keto}?sslmode=disable&max_conns=20 volumes: - ./config/.generated/ory/keto:/etc/config/keto:ro command: serve -c /etc/config/keto/keto.yml networks: [app_net] depends_on: keto-migrate: { condition: service_completed_successfully } oathkeeper_logs_init: image: alpine:latest command: ["sh", "-c", "mkdir -p /var/log/oathkeeper && chown -R ${OATHKEEPER_UID:-1001}:${OATHKEEPER_GID:-1001} /var/log/oathkeeper"] volumes: - oathkeeper_logs:/var/log/oathkeeper networks: [app_net] oathkeeper: image: oryd/oathkeeper:${OATHKEEPER_VERSION:-v26.2.0} container_name: ${COMPOSE_PROJECT_NAME}_oathkeeper env_file: .env user: "${OATHKEEPER_UID:-1001}:${OATHKEEPER_GID:-1001}" ports: - "${OATHKEEPER_PROXY_PORT}:4455" environment: - APP_ENV=${APP_ENV:-production} - LOG_LEVEL=debug - OATHKEEPER_INTROSPECT_CLIENT_ID=${OATHKEEPER_INTROSPECT_CLIENT_ID:-oathkeeper-introspect} - OATHKEEPER_INTROSPECT_CLIENT_SECRET=${OATHKEEPER_INTROSPECT_CLIENT_SECRET:-oathkeeper-secret} volumes: - ./config/.generated/ory/oathkeeper:/etc/config/oathkeeper:ro - oathkeeper_logs:/var/log/oathkeeper entrypoint: ["/etc/config/oathkeeper/entrypoint.sh"] networks: [app_net] depends_on: oathkeeper_logs_init: { condition: service_completed_successfully } kratos: { condition: service_started } hydra: { condition: service_started } ory_stack_check: image: alpine:latest container_name: ${COMPOSE_PROJECT_NAME}_ory_stack_check command: > /bin/sh -c " apk add --no-cache curl; echo 'Wait for Ory services...'; check_ready() { name=\"$$1\"; url=\"$$2\"; max=\"$${ORY_STACK_CHECK_MAX_ATTEMPTS:-60}\"; i=1; while [ \"$$i\" -le \"$$max\" ]; do if curl --connect-timeout 2 --max-time 3 -fsS \"$$url\" >/dev/null; then echo \"Ory service ready: $$name\"; return 0; fi; echo \"Waiting for Ory service: $$name ($$i/$$max)\"; i=$$((i + 1)); sleep 1; done; echo \"ERROR: Ory service not ready: $$name after $$max attempts ($$url)\" >&2; echo \"ERROR: Check service logs: docker logs $${COMPOSE_PROJECT_NAME}_$$name\" >&2; return 1; }; check_ready kratos http://kratos:4433/health/ready || exit 1; check_ready hydra http://hydra:4444/health/ready || exit 1; check_ready keto http://keto:4466/health/ready || exit 1; echo 'Ory stack is ready.';" depends_on: - kratos - hydra - keto networks: [app_net] init-rp: image: oryd/hydra:${HYDRA_CLI_VERSION:-v26.2.0} env_file: .env entrypoint: ["/bin/sh", "-ec"] command: - | upsert_client() { ID=$$1 shift if hydra get oauth2-client --endpoint "$${HYDRA_ADMIN_URL:-http://hydra:4445}" "$$ID" >/dev/null 2>&1; then hydra update oauth2-client --endpoint "$${HYDRA_ADMIN_URL:-http://hydra:4445}" "$$ID" "$$@" else hydra create oauth2-client --endpoint "$${HYDRA_ADMIN_URL:-http://hydra:4445}" --id "$$ID" "$$@" fi } upsert_client "adminfront" \ --name "AdminFront" \ --grant-type authorization_code,refresh_token \ --response-type code \ --scope openid,offline_access,profile,email \ --token-endpoint-auth-method none \ --redirect-uri "$${ADMINFRONT_CALLBACK_URLS:-$${ADMINFRONT_URL}/auth/callback}" upsert_client "devfront" \ --name "DevFront" \ --grant-type authorization_code,refresh_token \ --response-type code \ --scope openid,offline_access,profile,email \ --token-endpoint-auth-method none \ --redirect-uri "$${DEVFRONT_CALLBACK_URLS:-$${DEVFRONT_URL}/auth/callback}" upsert_client "orgfront" \ --name "OrgFront" \ --grant-type authorization_code,refresh_token \ --response-type code \ --scope openid,offline_access,profile,email \ --token-endpoint-auth-method none \ --redirect-uri "$${ORGFRONT_CALLBACK_URLS:-$${ORGFRONT_URL}/auth/callback}" upsert_client "$${OATHKEEPER_INTROSPECT_CLIENT_ID:-oathkeeper-introspect}" \ --secret "$${OATHKEEPER_INTROSPECT_CLIENT_SECRET:-oathkeeper-secret}" \ --grant-type client_credentials \ --response-type token \ --scope openid,offline_access,profile,email depends_on: ory_stack_check: { condition: service_completed_successfully } networks: [app_net] # --- Application Services --- backend: image: ${BACKEND_IMAGE_NAME}:${IMAGE_TAG} container_name: ${COMPOSE_PROJECT_NAME}_backend env_file: .env environment: - PORT=${BACKEND_PORT} - APP_ENV=${APP_ENV:-production} - IDP_PROVIDER=${IDP_PROVIDER:-ory} - USERFRONT_URL=${USERFRONT_URL} - KRATOS_ADMIN_URL=${KRATOS_ADMIN_URL:-http://kratos:4434} - HYDRA_ADMIN_URL=${HYDRA_ADMIN_URL:-http://hydra:4445} - HYDRA_PUBLIC_URL=${HYDRA_PUBLIC_URL} - KETO_READ_URL=${KETO_READ_URL:-http://keto:4466} - KETO_WRITE_URL=${KETO_WRITE_URL:-http://keto:4467} - DB_HOST=postgres - REDIS_ADDR=redis:6379 - CLICKHOUSE_HOST=clickhouse - SEED_TENANT_CSV_PATH=/app/seed-tenant.csv ports: - "${BACKEND_PORT}:${BACKEND_PORT}" volumes: - ./adminfront/seed-tenant.csv:/app/seed-tenant.csv:ro networks: [app_net] depends_on: postgres: { condition: service_healthy } redis: { condition: service_started } oathkeeper: { condition: service_started } gateway: image: ${USERFRONT_IMAGE_NAME}:${IMAGE_TAG} container_name: ${COMPOSE_PROJECT_NAME}_gateway ports: - "${USERFRONT_PORT}:80" volumes: - ./gateway/nginx.conf:/etc/nginx/nginx.conf:ro labels: - "traefik.enable=true" - "traefik.docker.network=${TRAEFIK_PUBLIC_NETWORK:-traefik-public}" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-gateway.rule=Host(`${PUBLIC_HOST}`)" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-gateway.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-gateway.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-myresolver}" - "traefik.http.services.${COMPOSE_PROJECT_NAME}-gateway.loadbalancer.server.port=80" networks: - app_net - traefik_public adminfront: image: ${ADMINFRONT_IMAGE_NAME}:${IMAGE_TAG} container_name: ${COMPOSE_PROJECT_NAME}_adminfront env_file: .env environment: - APP_ENV=${APP_ENV:-production} - API_PROXY_TARGET=http://backend:${BACKEND_PORT} ports: - "${ADMINFRONT_PORT}:5173" labels: - "traefik.enable=true" - "traefik.docker.network=${TRAEFIK_PUBLIC_NETWORK:-traefik-public}" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-adminfront.rule=Host(`${ADMINFRONT_HOST}`)" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-adminfront.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-adminfront.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-myresolver}" - "traefik.http.services.${COMPOSE_PROJECT_NAME}-adminfront.loadbalancer.server.port=5173" networks: - app_net - traefik_public devfront: image: ${DEVFRONT_IMAGE_NAME}:${IMAGE_TAG} container_name: ${COMPOSE_PROJECT_NAME}_devfront env_file: .env environment: - APP_ENV=${APP_ENV:-production} - API_PROXY_TARGET=http://backend:${BACKEND_PORT} ports: - "${DEVFRONT_PORT}:5174" labels: - "traefik.enable=true" - "traefik.docker.network=${TRAEFIK_PUBLIC_NETWORK:-traefik-public}" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-devfront.rule=Host(`${DEVFRONT_HOST}`)" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-devfront.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-devfront.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-myresolver}" - "traefik.http.services.${COMPOSE_PROJECT_NAME}-devfront.loadbalancer.server.port=5174" networks: - app_net - traefik_public orgfront: image: ${ORGFRONT_IMAGE_NAME}:${IMAGE_TAG} container_name: ${COMPOSE_PROJECT_NAME}_orgfront env_file: .env environment: - APP_ENV=${APP_ENV:-production} - API_PROXY_TARGET=http://backend:${BACKEND_PORT} - USERFRONT_URL=${USERFRONT_URL} ports: - "${ORGFRONT_PORT}:5175" labels: - "traefik.enable=true" - "traefik.docker.network=${TRAEFIK_PUBLIC_NETWORK:-traefik-public}" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-orgfront.rule=Host(`${ORGFRONT_HOST}`)" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-orgfront.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-orgfront.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-myresolver}" - "traefik.http.services.${COMPOSE_PROJECT_NAME}-orgfront.loadbalancer.server.port=5175" networks: - app_net - traefik_public networks: app_net: name: ${COMPOSE_PROJECT_NAME}_net traefik_public: external: true name: ${TRAEFIK_PUBLIC_NETWORK:-traefik-public} volumes: db_data: name: db_data_${INSTANCE_NAME} ory_db_data: name: ory_db_data_${INSTANCE_NAME} clickhouse_data: name: clickhouse_data_${INSTANCE_NAME} oathkeeper_logs: name: oathkeeper_logs_${INSTANCE_NAME}