Ctrl K
ComunidadGuías…

compose.yaml

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        # NEXT_PUBLIC_* values are inlined into the bundle, so they are build args: changing one
        # needs `docker compose build web`.
        NEXT_PUBLIC_BACKEND_URL: ${NEXT_PUBLIC_BACKEND_URL:-http://localhost:8080/api/v1}
        NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-https://veriworkly.com}
        NEXT_PUBLIC_VITALS_URL: ${NEXT_PUBLIC_VITALS_URL:-}
        NODE_ENV: ${NODE_ENV:-production}
    environment:
      NODE_ENV: ${NODE_ENV:-production}
      PORT: 3000
      NEXT_PUBLIC_BACKEND_URL: ${NEXT_PUBLIC_BACKEND_URL:-http://localhost:8080/api/v1}
      BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL:-http://api:8080/api/v1}
      # Read at runtime. Production defaults to off; keep them equal to the API's flags.
      AFFILIATE_PROGRAM_ENABLED: ${AFFILIATE_PROGRAM_ENABLED:-false}
      AMBASSADOR_PROGRAM_ENABLED: ${AMBASSADOR_PROGRAM_ENABLED:-false}
      ERROR_REPORT_URL: ${ERROR_REPORT_URL:-}
    depends_on:
      api:
        condition: service_healthy
    ports:
      # Loopback only: production is VPS + nginx, and nginx on the host is the only client.
      - "127.0.0.1:3000:3000"
    healthcheck:
      test:
        [
          "CMD",
          "node",
          "-e",
          "fetch('http://127.0.0.1:3000').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
        ]
      interval: 1m
      timeout: 10s
      retries: 3
      start_period: 40s
    # Server errors go to stderr when ERROR_REPORT_URL is unset; cap what an outage can write.
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    restart: unless-stopped

  api:
    build:
      # apps/server/Dockerfile expects the repository root as its build context so it can
      # install the npm workspace from the root package-lock.
      context: .
      dockerfile: apps/server/Dockerfile
    # Every variable the server refuses to boot without in production is a `${VAR:?}` here, so
    # `docker compose up` names the missing ones before the container starts. The server's own
    # boot validation (src/config/environmentSchema.ts) applies the format rules on top: https
    # and no localhost for public URLs, TRUST_PROXY as a hop count or CIDR list, non-example
    # secrets. NOTE: do not pass a boolean as a bare `${VAR}`: an empty string parses as false,
    # which silently overrides the production default.
    environment:
      NODE_ENV: ${NODE_ENV:-production}
      PORT: 8080
      DATABASE_URL: ${DATABASE_URL:?DATABASE_URL is required}
      DB_POOL_MAX_TOTAL: ${DB_POOL_MAX_TOTAL:-80}
      # DB_IDLE_IN_TRANSACTION_TIMEOUT_MS is opt-in and deliberately absent: it is sent as a
      # startup parameter, which a pooler such as Neon's PgBouncer refuses. Add it here only for
      # a direct Postgres connection; behind a pooler, set it on the role (server README).
      ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:?ALLOWED_ORIGINS is required (comma-separated https origins)}

      # Proxy / process model. The API port is bound to 127.0.0.1 below, so the host's nginx is
      # the only way in: TRUST_PROXY=1 for nginx alone, 2 with Cloudflare in front of nginx.
      TRUST_PROXY: ${TRUST_PROXY:?TRUST_PROXY is required (proxy hop count, e.g. 1 behind nginx, or a CIDR list)}
      DIRECT_EXPOSURE: ${DIRECT_EXPOSURE:-}
      WEB_CONCURRENCY: ${WEB_CONCURRENCY:?WEB_CONCURRENCY is required (number of cluster workers)}

      # Redis. REDIS_PASSWORD goes verbatim into REDIS_URL, --requirepass and REDISCLI_AUTH, so use
      # only [A-Za-z0-9] characters (e.g. `openssl rand -hex 32`); URL-encoding would break auth.
      REDIS_URL: ${REDIS_URL:-redis://:${REDIS_PASSWORD:?REDIS_PASSWORD is required}@redis:6379}

      # Better Auth Core
      AUTH_SECRET: ${AUTH_SECRET:?AUTH_SECRET is required (at least 32 characters)}
      AUTH_BASE_URL: ${AUTH_BASE_URL:?AUTH_BASE_URL is required (public https URL of this API)}
      # The studio and portfolio reach the API as http://api:8080 (BACKEND_INTERNAL_URL); the auth
      # routes refuse any other Host than AUTH_BASE_URL's and loopback (V-155).
      ALLOWED_HOSTS: ${ALLOWED_HOSTS:-api}
      AUTH_COOKIE_DOMAIN: ${AUTH_COOKIE_DOMAIN}
      STUDIO_URL: ${STUDIO_URL}
      STUDIO_LOGIN_URL: ${STUDIO_LOGIN_URL}
      SITE_URL: ${SITE_URL}

      # Better Auth Sessions
      AUTH_SESSION_TTL_SECONDS: ${AUTH_SESSION_TTL_SECONDS:-2592000}
      AUTH_SESSION_RESET_TTL_ON_USE: ${AUTH_SESSION_RESET_TTL_ON_USE:-86400}
      AUTH_SESSION_CACHE_MAX_AGE_SECONDS: ${AUTH_SESSION_CACHE_MAX_AGE_SECONDS:-900}

      # Auth OTP & Admin
      AUTH_OTP_TTL_SECONDS: ${AUTH_OTP_TTL_SECONDS:-300}
      AUTH_OTP_ALLOWED_ATTEMPTS: ${AUTH_OTP_ALLOWED_ATTEMPTS:-3}
      ADMIN_EMAIL: ${ADMIN_EMAIL:?ADMIN_EMAIL is required (the operator account email)}

      # Email / SMTP. "console" writes OTP codes into the logs and is refused in production.
      AUTH_EMAIL_PROVIDER: ${AUTH_EMAIL_PROVIDER:-resend}
      AUTH_EMAIL_FROM: ${AUTH_EMAIL_FROM}
      RESEND_API_KEY: ${RESEND_API_KEY}
      AUTH_SMTP_HOST: ${AUTH_SMTP_HOST}
      AUTH_SMTP_PORT: ${AUTH_SMTP_PORT:-587}
      AUTH_SMTP_SECURE: ${AUTH_SMTP_SECURE:-false}
      AUTH_SMTP_USER: ${AUTH_SMTP_USER}
      AUTH_SMTP_PASS: ${AUTH_SMTP_PASS}

      # Secrets that must not be the example values
      API_KEY_HASH_SECRET: ${API_KEY_HASH_SECRET:?API_KEY_HASH_SECRET is required (distinct from AUTH_SECRET)}
      ANALYTICS_HASH_PEPPER: ${ANALYTICS_HASH_PEPPER:?ANALYTICS_HASH_PEPPER is required}

      # Rate Limiting
      RATE_LIMIT_WINDOW_MS: ${RATE_LIMIT_WINDOW_MS:-900000}
      RATE_LIMIT_MAX_REQUESTS: ${RATE_LIMIT_MAX_REQUESTS:-100}

      # AI
      AI_API_KEY: ${AI_API_KEY}
      AI_BASE_URL: ${AI_BASE_URL}
      AI_ACTIONS_POLICY_PATH: ${AI_ACTIONS_POLICY_PATH}
      AI_ACTIONS_POLICY_JSON: ${AI_ACTIONS_POLICY_JSON}
      ATS_AI_POLICY_PATH: ${ATS_AI_POLICY_PATH}
      ATS_AI_POLICY_JSON: ${ATS_AI_POLICY_JSON}
      ATS_ENGINE_POLICY_PATH: ${ATS_ENGINE_POLICY_PATH}
      ATS_ENGINE_POLICY_JSON: ${ATS_ENGINE_POLICY_JSON}

      # Portfolio
      PORTFOLIO_URL: ${PORTFOLIO_URL:?PORTFOLIO_URL is required (public https URL)}
      PORTFOLIO_REVALIDATE_SECRET: ${PORTFOLIO_REVALIDATE_SECRET:?PORTFOLIO_REVALIDATE_SECRET is required}
      PORTFOLIO_GRACE_DAYS: ${PORTFOLIO_GRACE_DAYS:-7}

      # Billing
      DODO_PAYMENTS_API_KEY: ${DODO_PAYMENTS_API_KEY:?DODO_PAYMENTS_API_KEY is required}
      DODO_PAYMENTS_WEBHOOK_SECRET: ${DODO_PAYMENTS_WEBHOOK_SECRET:?DODO_PAYMENTS_WEBHOOK_SECRET is required}
      DODO_PAYMENTS_ENVIRONMENT: ${DODO_PAYMENTS_ENVIRONMENT:-test_mode}
      DODO_PAYMENTS_CHECKOUT_RETURN_URL: ${DODO_PAYMENTS_CHECKOUT_RETURN_URL:?DODO_PAYMENTS_CHECKOUT_RETURN_URL is required (public https URL)}
      DODO_PAYMENTS_CHECKOUT_CANCEL_URL: ${DODO_PAYMENTS_CHECKOUT_CANCEL_URL:?DODO_PAYMENTS_CHECKOUT_CANCEL_URL is required (public https URL)}
      DODO_PAYMENTS_PORTAL_RETURN_URL: ${DODO_PAYMENTS_PORTAL_RETURN_URL:?DODO_PAYMENTS_PORTAL_RETURN_URL is required (public https URL)}
      DODO_PAYMENTS_PORTFOLIO_PRO_MONTHLY_PRODUCT_ID: ${DODO_PAYMENTS_PORTFOLIO_PRO_MONTHLY_PRODUCT_ID}
      DODO_PAYMENTS_PORTFOLIO_PRO_ANNUAL_PRODUCT_ID: ${DODO_PAYMENTS_PORTFOLIO_PRO_ANNUAL_PRODUCT_ID}
      DODO_PAYMENTS_BUNDLE_ONE_DAY_PRODUCT_ID: ${DODO_PAYMENTS_BUNDLE_ONE_DAY_PRODUCT_ID}
      DODO_PAYMENTS_BUNDLE_SEVEN_DAY_PRODUCT_ID: ${DODO_PAYMENTS_BUNDLE_SEVEN_DAY_PRODUCT_ID}
      DODO_PAYMENTS_AI_CREDITS_MONTHLY_PRODUCT_ID: ${DODO_PAYMENTS_AI_CREDITS_MONTHLY_PRODUCT_ID}
      DODO_PAYMENTS_AI_CREDITS_ANNUAL_PRODUCT_ID: ${DODO_PAYMENTS_AI_CREDITS_ANNUAL_PRODUCT_ID}
      DODO_PAYMENTS_BUNDLE_MONTHLY_PRODUCT_ID: ${DODO_PAYMENTS_BUNDLE_MONTHLY_PRODUCT_ID}
      DODO_PAYMENTS_BUNDLE_ANNUAL_PRODUCT_ID: ${DODO_PAYMENTS_BUNDLE_ANNUAL_PRODUCT_ID}
      DODO_PAYMENTS_CREDIT_PACK_250_PRODUCT_ID: ${DODO_PAYMENTS_CREDIT_PACK_250_PRODUCT_ID}
      DODO_PAYMENTS_CREDIT_PACK_500_PRODUCT_ID: ${DODO_PAYMENTS_CREDIT_PACK_500_PRODUCT_ID}
      DODO_PAYMENTS_CREDIT_PACK_1200_PRODUCT_ID: ${DODO_PAYMENTS_CREDIT_PACK_1200_PRODUCT_ID}

      # Portfolio media (Cloudflare R2)
      R2_ENDPOINT: ${R2_ENDPOINT:?R2_ENDPOINT is required}
      R2_BUCKET: ${R2_BUCKET:?R2_BUCKET is required}
      R2_ACCESS_KEY_ID: ${R2_ACCESS_KEY_ID:?R2_ACCESS_KEY_ID is required}
      R2_SECRET_ACCESS_KEY: ${R2_SECRET_ACCESS_KEY:?R2_SECRET_ACCESS_KEY is required}
      R2_PUBLIC_BASE_URL: ${R2_PUBLIC_BASE_URL:?R2_PUBLIC_BASE_URL is required}

      # GitHub Sync
      GITHUB_SYNC_ENABLED: ${GITHUB_SYNC_ENABLED:-false}
      GITHUB_TOKEN: ${GITHUB_TOKEN}
      GITHUB_OWNER: ${GITHUB_OWNER}
      GITHUB_REPO: ${GITHUB_REPO}
      GITHUB_PROJECT_URL: ${GITHUB_PROJECT_URL}
      GITHUB_SYNC_CRON: ${GITHUB_SYNC_CRON}
      INTERNAL_SYNC_API_KEY: ${INTERNAL_SYNC_API_KEY}

      # Metrics
      USAGE_METRICS_FLUSH_CRON: ${USAGE_METRICS_FLUSH_CRON}
      USAGE_METRICS_FLUSH_TIMEZONE: ${USAGE_METRICS_FLUSH_TIMEZONE:-UTC}

      # Internal server-to-server keys (x-internal-key); optional, 32+ chars in production
      STATS_INTERNAL_KEY: ${STATS_INTERNAL_KEY}
      INTERNAL_API_KEY: ${INTERNAL_API_KEY}

      # Everything else the server reads (V-347). A bare key passes the variable from the shell
      # or the root .env when it is set and leaves it unset otherwise, so the server default
      # applies. tests/config/compose-api-env.test.ts keeps this list complete.
      # Sign-in providers: without these, Google, GitHub and LinkedIn sign-in are off.
      GOOGLE_CLIENT_ID:
      GOOGLE_CLIENT_SECRET:
      GITHUB_CLIENT_ID:
      GITHUB_CLIENT_SECRET:
      LINKEDIN_CLIENT_ID:
      LINKEDIN_CLIENT_SECRET:
      # Programs: keep equal to the web service flags above.
      AFFILIATE_PROGRAM_ENABLED:
      AMBASSADOR_PROGRAM_ENABLED:
      AFFILIATE_ATTRIBUTION_DAYS:
      # Site cache revalidation (changelog and roadmap pages).
      SITE_REVALIDATE_SECRET:
      # Process model, logging and Redis/Postgres tuning.
      CLUSTERING_ENABLED:
      CRON_ROLE:
      LOG_LEVEL:
      MAX_MEMORY_ENTRIES:
      AUTH_IP_ADDRESS_HEADERS:
      REDIS_BOOT_TIMEOUT_MS:
      REDIS_COMMAND_TIMEOUT_MS:
      DB_POOL_MAX:
      DB_POOL_IDLE_TIMEOUT_MS:
      DB_POOL_CONNECTION_TIMEOUT_MS:
      DB_POOL_STATEMENT_TIMEOUT_MS:
      # Rate limits.
      GLOBAL_RATE_LIMIT_WINDOW_MS:
      GLOBAL_RATE_LIMIT_MAX_REQUESTS:
      AUTH_RATE_LIMIT_WINDOW_MS:
      AUTH_RATE_LIMIT_MAX_REQUESTS:
      AI_RATE_LIMIT_WINDOW_MS:
      AI_RATE_LIMIT_MAX_REQUESTS:
      RATE_LIMIT_REDIS_TIMEOUT_MS:
      # AI and ATS.
      AI_TIMEOUT_MS:
      ATS_EXTRACT_MAX_RSS_MB:
      # API keys.
      API_KEY_AUTH_CACHE_TTL_SECONDS:
      API_KEY_DEFAULT_LIFETIME_DAYS:
      API_KEY_DEFAULT_RATE_LIMIT:
      API_KEY_MAX_RATE_LIMIT:
      API_KEY_DEFAULT_SCOPES:
      API_KEY_LAST_USED_TOUCH_INTERVAL_SECONDS:
      # Credit pack price overrides (cents).
      CREDIT_PACK_250_PRICE_CENTS:
      CREDIT_PACK_500_PRICE_CENTS:
      CREDIT_PACK_1200_PRICE_CENTS:
      # Changelog, roadmap and GitHub caches and sync.
      CHANGELOG_RELEASE_SYNC_ENABLED:
      CHANGELOG_RELEASE_SYNC_CRON:
      CHANGELOG_RELEASE_SYNC_TIMEZONE:
      CHANGELOG_RELEASE_SYNC_MIN_INTERVAL_SECONDS:
      CHANGELOG_CACHE_TTL_SECONDS:
      ROADMAP_CACHE_TTL_SECONDS:
      ROADMAP_STATS_CACHE_TTL_SECONDS:
      GITHUB_STATS_CACHE_TTL_SECONDS:
      GITHUB_SYNC_TIMEZONE:
      USAGE_METRICS_REDIS_RETENTION_DAYS:
      # Data retention windows in days (0 turns a rule off); defaults are the owner decisions.
      RETENTION_EXPIRED_SESSION_DAYS:
      RETENTION_EXPIRED_VERIFICATION_DAYS:
      RETENTION_WEBHOOK_PAYLOAD_DAYS:
      RETENTION_UNPROCESSED_WEBHOOK_PAYLOAD_DAYS:
      RETENTION_AUDIT_LOG_DAYS:
      RETENTION_ADMIN_AUDIT_DAYS:
      RETENTION_ADMIN_MONEY_AUDIT_DAYS:
      RETENTION_CREDIT_RESERVATION_DAYS:
      RETENTION_CREDIT_REPLAY_DAYS:
      RETENTION_FLUSH_BATCH_DAYS:
      RETENTION_AFFILIATE_CLICK_DAYS:
      RETENTION_PORTFOLIO_VIEW_DAYS:
      RETENTION_DELETED_DOCUMENT_DAYS:
    depends_on:
      redis:
        condition: service_healthy
      migrate:
        condition: service_completed_successfully
    ports:
      # Loopback only: production is VPS + nginx, and nginx on the host is the only client.
      - "127.0.0.1:8080:8080"
    healthcheck:
      test:
        [
          "CMD",
          "node",
          "-e",
          "fetch('http://127.0.0.1:8080/api/v1/health').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
        ]
      interval: 1m
      timeout: 10s
      retries: 3
      start_period: 40s
    # Graceful shutdown drains HTTP for up to 25 s and running jobs for up to 20 s, and the
    # cluster master force-kills workers at 35 s; Compose's default 10 s would SIGKILL a run
    # mid-flush (SHUTDOWN_TIMINGS in apps/server/src/lib/shutdown.ts).
    stop_grace_period: 40s
    restart: unless-stopped

  # One-shot: applies pending migrations before `api` starts (V-106). Runs from the `build`
  # stage, which still has the Prisma CLI. Migrations need a direct connection: point
  # DIRECT_DATABASE_URL at the non-pooled endpoint when DATABASE_URL goes through a pooler.
  migrate:
    build:
      context: .
      dockerfile: apps/server/Dockerfile
      target: build
    command: ["npm", "exec", "-w", "@veriworkly/server", "--", "prisma", "migrate", "deploy"]
    environment:
      NODE_ENV: ${NODE_ENV:-production}
      DATABASE_URL: ${DATABASE_URL:?DATABASE_URL is required}
      DIRECT_DATABASE_URL: ${DIRECT_DATABASE_URL:-}
    restart: "no"

  redis:
    image: redis:7-alpine
    # V-113: bounded memory. volatile-lru evicts only keys with a TTL (caches, but also view and
    # usage buffers, rate-limit buckets and locks). Once nothing with a TTL is left, writes still
    # fail with OOM, so size REDIS_MAXMEMORY for the working set (see apps/server/README.md).
    command:
      [
        "redis-server",
        "--appendonly",
        "yes",
        "--requirepass",
        "${REDIS_PASSWORD:?REDIS_PASSWORD is required}",
        "--maxmemory",
        "${REDIS_MAXMEMORY:-256mb}",
        "--maxmemory-policy",
        "volatile-lru",
      ]
    environment:
      # redis-cli reads REDISCLI_AUTH, so the healthcheck authenticates without putting the
      # password on its command line.
      REDISCLI_AUTH: ${REDIS_PASSWORD:?REDIS_PASSWORD is required}
    volumes:
      - redis-data:/data
    healthcheck:
      # redis-cli exits 0 even on a NOAUTH error reply, so check for PONG explicitly.
      test: ["CMD-SHELL", "redis-cli ping | grep -q PONG"]
      interval: 20s
      timeout: 5s
      retries: 3
    restart: unless-stopped

  # Development only (`docker compose --profile dev up`). It has unauthenticated read/write access
  # to the Redis that holds sessions, rate-limit counters and job locks, so it is never started in
  # production and only listens on the host's loopback (reach it over an SSH tunnel).
  redisinsight:
    image: redis/redisinsight:2.70.1
    profiles: ["dev"]
    ports:
      - "127.0.0.1:5540:5540"
    depends_on:
      - redis
    restart: unless-stopped

volumes:
  redis-data: