name: lehuo-production x-api-environment: &api-environment APP_ENV: production API_PORT: '3000' DATABASE_URL: ${DATABASE_URL:?DATABASE_URL is required} REDIS_URL: ${REDIS_URL:?REDIS_URL is required} SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET is required} SESSION_TOKEN_PEPPER: ${SESSION_TOKEN_PEPPER:?SESSION_TOKEN_PEPPER is required} AUDIT_IP_HASH_KEY: ${AUDIT_IP_HASH_KEY:?AUDIT_IP_HASH_KEY is required} FIELD_ENCRYPTION_KEY: ${FIELD_ENCRYPTION_KEY:?FIELD_ENCRYPTION_KEY is required} ADMIN_WEB_ORIGIN: ${ADMIN_WEB_ORIGIN:?ADMIN_WEB_ORIGIN is required} ADMIN_COOKIE_SECURE: 'true' AUTH_SESSION_TTL_SECONDS: ${AUTH_SESSION_TTL_SECONDS:-28800} WECHAT_SESSION_TTL_SECONDS: ${WECHAT_SESSION_TTL_SECONDS:-7200} WECHAT_APP_ID: ${WECHAT_APP_ID:-} WECHAT_APP_SECRET: ${WECHAT_APP_SECRET:-} WECHAT_TEMPLATE_APPLICATION_SUBMITTED: ${WECHAT_TEMPLATE_APPLICATION_SUBMITTED:-} WECHAT_TEMPLATE_APPLICATION_ACCEPTED: ${WECHAT_TEMPLATE_APPLICATION_ACCEPTED:-} WECHAT_TEMPLATE_SERVICE_ARRANGED: ${WECHAT_TEMPLATE_SERVICE_ARRANGED:-} WECHAT_TEMPLATE_SERVICE_COMPLETED: ${WECHAT_TEMPLATE_SERVICE_COMPLETED:-} WECHAT_TEMPLATE_FOLLOW_UP_PENDING: ${WECHAT_TEMPLATE_FOLLOW_UP_PENDING:-} WECHAT_TEMPLATE_ACTIVITY_REGISTRATION_RESULT: ${WECHAT_TEMPLATE_ACTIVITY_REGISTRATION_RESULT:-} NOTIFICATION_WORKER_ENABLED: ${NOTIFICATION_WORKER_ENABLED:-false} OBJECT_STORAGE_ENDPOINT: ${OBJECT_STORAGE_ENDPOINT:?OBJECT_STORAGE_ENDPOINT is required} OBJECT_STORAGE_REGION: ${OBJECT_STORAGE_REGION:-auto} OBJECT_STORAGE_BUCKET: ${OBJECT_STORAGE_BUCKET:?OBJECT_STORAGE_BUCKET is required} OBJECT_STORAGE_ACCESS_KEY: ${OBJECT_STORAGE_ACCESS_KEY:?OBJECT_STORAGE_ACCESS_KEY is required} OBJECT_STORAGE_SECRET_KEY: ${OBJECT_STORAGE_SECRET_KEY:?OBJECT_STORAGE_SECRET_KEY is required} OBJECT_STORAGE_FORCE_PATH_STYLE: 'true' services: mysql: image: mysql:8.4.8 restart: unless-stopped command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_0900_ai_ci environment: MYSQL_DATABASE: ${MYSQL_DATABASE:?MYSQL_DATABASE is required} MYSQL_USER: ${MYSQL_USER:?MYSQL_USER is required} MYSQL_PASSWORD: ${MYSQL_PASSWORD:?MYSQL_PASSWORD is required} MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?MYSQL_ROOT_PASSWORD is required} TZ: Asia/Shanghai volumes: - mysql-data:/var/lib/mysql networks: - backend healthcheck: test: - CMD-SHELL - MYSQL_PWD=$$MYSQL_ROOT_PASSWORD mysqladmin ping -h 127.0.0.1 -u root --silent interval: 10s timeout: 5s retries: 12 start_period: 40s security_opt: - no-new-privileges:true redis: image: redis:7.4-alpine restart: unless-stopped command: - redis-server - --appendonly - 'yes' - --requirepass - ${REDIS_PASSWORD:?REDIS_PASSWORD is required} volumes: - redis-data:/data networks: - backend healthcheck: test: [CMD-SHELL, 'redis-cli -a "$$REDIS_PASSWORD" ping | grep -q PONG'] interval: 10s timeout: 5s retries: 8 environment: REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD is required} security_opt: - no-new-privileges:true minio: image: minio/minio:RELEASE.2025-09-07T16-13-09Z restart: unless-stopped command: server /data environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER:?MINIO_ROOT_USER is required} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD is required} MINIO_BROWSER: 'off' MINIO_SERVER_URL: https://${FILES_HOST:?FILES_HOST is required} volumes: - minio-data:/data networks: - backend healthcheck: test: [CMD, curl, -f, http://localhost:9000/minio/health/live] interval: 10s timeout: 5s retries: 8 security_opt: - no-new-privileges:true minio-init: image: minio/mc:RELEASE.2025-08-13T08-35-41Z restart: 'no' entrypoint: [/bin/sh, /bootstrap/init-minio.sh] environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER:?MINIO_ROOT_USER is required} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD is required} OBJECT_STORAGE_BUCKET: ${OBJECT_STORAGE_BUCKET:?OBJECT_STORAGE_BUCKET is required} ADMIN_WEB_ORIGIN: ${ADMIN_WEB_ORIGIN:?ADMIN_WEB_ORIGIN is required} volumes: - ./minio/init-minio.sh:/bootstrap/init-minio.sh:ro networks: - backend depends_on: minio: condition: service_healthy security_opt: - no-new-privileges:true api: image: lehuo-api:${IMAGE_TAG:?IMAGE_TAG is required} build: context: ../.. dockerfile: apps/api/Dockerfile restart: unless-stopped environment: *api-environment extra_hosts: - ${FILES_HOST:?FILES_HOST is required}:host-gateway expose: - '3000' networks: - backend depends_on: mysql: condition: service_healthy redis: condition: service_healthy minio-init: condition: service_completed_successfully healthcheck: test: - CMD - node - -e - fetch('http://127.0.0.1:3000/health/live').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1)) interval: 15s timeout: 5s retries: 8 start_period: 30s read_only: true tmpfs: - /tmp:size=64m,noexec,nosuid cap_drop: - ALL security_opt: - no-new-privileges:true migrate: image: lehuo-api:${IMAGE_TAG:?IMAGE_TAG is required} profiles: [tools] command: - ./apps/api/node_modules/.bin/prisma - migrate - deploy - --config - apps/api/prisma.config.ts environment: DATABASE_URL: ${DATABASE_URL:?DATABASE_URL is required} networks: - backend depends_on: mysql: condition: service_healthy read_only: true tmpfs: - /tmp:size=64m,noexec,nosuid cap_drop: - ALL security_opt: - no-new-privileges:true web: image: lehuo-admin:${IMAGE_TAG:?IMAGE_TAG is required} build: context: ../.. dockerfile: apps/admin-web/Dockerfile args: VITE_API_BASE_URL: https://${PUBLIC_HOST:?PUBLIC_HOST is required} restart: unless-stopped environment: PUBLIC_HOST: ${PUBLIC_HOST:?PUBLIC_HOST is required} FILES_HOST: ${FILES_HOST:?FILES_HOST is required} NGINX_ENVSUBST_FILTER: ^(PUBLIC_HOST|FILES_HOST)$$ ports: - '80:80' - '443:443' volumes: - ${TLS_CERT_PATH:?TLS_CERT_PATH is required}:/etc/nginx/tls/fullchain.pem:ro - ${TLS_KEY_PATH:?TLS_KEY_PATH is required}:/etc/nginx/tls/privkey.pem:ro networks: - backend - edge depends_on: api: condition: service_healthy read_only: true tmpfs: - /var/cache/nginx:size=32m,noexec,nosuid - /var/run:size=4m,noexec,nosuid security_opt: - no-new-privileges:true networks: backend: edge: volumes: mysql-data: minio-data: redis-data: