Ctrl K
ComunidadGuías…

docker-compose.yml

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

Archivo original ↗Preguntar a KiBot
# SPDX-License-Identifier: Apache-2.0
# Copyright 2026 Aaron K. Clark
#
# Container mode: `docker compose up` brings up Postgres + the .NET API + the Python
# poller — the whole two-runtime stack.
# Local mode: `docker compose up -d db` then `dotnet run` (or `applytrack poll`)
# against localhost — the same build, just a different connection string.
services:
  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-applytrack}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-JanewayDidNothingWrong}
      POSTGRES_DB: ${POSTGRES_DB:-applytrack}
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-applytrack}"]
      interval: 5s
      timeout: 5s
      retries: 10
    restart: unless-stopped

  api:
    build:
      context: ./api
      dockerfile: ApplyTrack.Api/Dockerfile
    environment:
      ConnectionStrings__Postgres: "Host=db;Port=5432;Database=${POSTGRES_DB:-applytrack};Username=${POSTGRES_USER:-applytrack};Password=${POSTGRES_PASSWORD:-JanewayDidNothingWrong}"
      ASPNETCORE_URLS: "http://+:8080"
      # Cover-letter AI (all optional; see .env.example). Empty defaults leave drafting
      # off until a tenant configures their own endpoint in the SPA's AI panel.
      Llm__BaseUrl: ${Llm__BaseUrl:-}
      Llm__Model: ${Llm__Model:-}
      Llm__ApiKey: ${Llm__ApiKey:-}
      # Daily per-tenant cap on model requests made with the key above (0 = unlimited).
      Llm__DailyCapPerTenant: ${Llm__DailyCapPerTenant:-200}
      # Daily retention sweep (#350); see README · Configuration. 0 days = keep forever.
      Retention__ScreenshotDays: ${Retention__ScreenshotDays:-90}
      Retention__AgentEventDays: ${Retention__AgentEventDays:-180}
      Retention__SeenDays: ${Retention__SeenDays:-0}
      Retention__Enabled: ${Retention__Enabled:-true}
      Retention__IntervalHours: ${Retention__IntervalHours:-24}
      Retention__InitialDelaySeconds: ${Retention__InitialDelaySeconds:-300}
      Retention__ScreenshotsKeptPerApplication: ${Retention__ScreenshotsKeptPerApplication:-3}
      Retention__ExpiredGraceDays: ${Retention__ExpiredGraceDays:-1}
      Retention__PollRunDays: ${Retention__PollRunDays:-30}
      # Encryption at rest is always on. Leave this unset and the api generates a key on
      # first run into the `secrets` volume below (shared with the agent and the poller,
      # which must decrypt the same rows); set it to manage the key yourself. Back the key
      # up with the database — losing it loses the résumé, letters, packets and screenshots.
      APPLYTRACK_SECRETS_KEY: ${APPLYTRACK_SECRETS_KEY:-}
      # Opt-in observability (#359), empty = off: an OTLP collector, and/or a port for a
      # Prometheus /metrics served on that port only (publish it to the scraper, not the
      # internet). See README · Observability.
      OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-}
      Metrics__Port: ${METRICS_PORT:-}
      # Email / magic-link delivery (all optional; see .env.example). With Email__Host
      # unset, login links are logged to the console instead of sent — no email config
      # needed to try auth locally. Set Email__Host to relay through any SMTP provider.
      Email__Host: ${Email__Host:-}
      Email__Port: ${Email__Port:-587}
      Email__Username: ${Email__Username:-}
      Email__Password: ${Email__Password:-}
      Email__From: ${Email__From:-}
      Email__FromName: ${Email__FromName:-OSApplyTrack}
      # Public origin (#337). Sign-in links are built from this, never the request Host
      # header. Unset, links fall back to the request origin only for localhost/127.0.0.1
      # — reaching the api by LAN IP or a hostname then sends no link. Set it (scheme,
      # no trailing slash) for any non-localhost use; it is REQUIRED once Email__Host is
      # set (the api refuses to boot without it). ALLOWED_HOSTS pins the Host headers
      # Kestrel answers at all, e.g. "apply.example.com;localhost".
      App__PublicBaseUrl: ${APP_PUBLIC_BASE_URL:-}
      AllowedHosts: ${ALLOWED_HOSTS:-*}
      # Same-host proxies are trusted by default. Set either value only when the
      # proxy reaches this container from a different address or bridge network.
      ForwardedHeaders__KnownProxies__0: ${FORWARDED_HEADERS_KNOWN_PROXY:-}
      ForwardedHeaders__KnownNetworks__0: ${FORWARDED_HEADERS_KNOWN_NETWORK:-}
    # 8080 is contested (vLLM, llama.cpp, and half of all dev tools default to it).
    # If it's already taken, Docker fails the bind loudly ("address already in use")
    # rather than silently winning — relocate by setting API_PORT in .env.
    ports:
      - "${API_PORT:-8080}:8080"
    volumes:
      - secrets:/var/lib/applytrack
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  # The agent worker: the same API image with Agent__Enabled=true and no published
  # port. It judges each tenant's promising leads unattended (Settings · Agent is
  # off by default per tenant), holding a connection across multi-minute model
  # calls — which is exactly why it is not run inside the request-serving container.
  agent:
    build:
      context: ./api
      dockerfile: ApplyTrack.Api/Dockerfile
    environment:
      ConnectionStrings__Postgres: "Host=db;Port=5432;Database=${POSTGRES_DB:-applytrack};Username=${POSTGRES_USER:-applytrack};Password=${POSTGRES_PASSWORD:-JanewayDidNothingWrong}"
      ASPNETCORE_URLS: "http://+:8080"
      Agent__Enabled: "true"
      Agent__IntervalSeconds: ${AGENT_INTERVAL:-300}
      Agent__LlmTimeoutSeconds: ${AGENT_LLM_TIMEOUT:-300}
      Llm__BaseUrl: ${Llm__BaseUrl:-}
      Llm__Model: ${Llm__Model:-}
      Llm__ApiKey: ${Llm__ApiKey:-}
      # Email notifications (#355) go out from here: the same SMTP relay as the api's.
      Email__Host: ${Email__Host:-}
      Email__Port: ${Email__Port:-587}
      Email__Username: ${Email__Username:-}
      Email__Password: ${Email__Password:-}
      Email__From: ${Email__From:-}
      Email__FromName: ${Email__FromName:-OSApplyTrack}
      APPLYTRACK_SECRETS_KEY: ${APPLYTRACK_SECRETS_KEY:-}
      # Opt-in observability (#359), empty = off: an OTLP collector, and/or a port for a
      # Prometheus /metrics served on that port only (publish it to the scraper, not the
      # internet). See README · Observability.
      OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-}
      Metrics__Port: ${METRICS_PORT:-}
      App__PublicBaseUrl: ${APP_PUBLIC_BASE_URL:-}
      # The browser container (below). Unset both to prepare packets without one.
      Browser__Endpoint: ${BROWSER_ENDPOINT:-ws://browser:3000/}
      Browser__Proxy: ${BROWSER_PROXY:-http://proxy:3128}
    volumes:
      - secrets:/var/lib/applytrack
    networks:
      - default
      - browser
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  # The browser the agent drives: Playwright's own image running `run-server`, on
  # an internal network whose only way out is the proxy. Never in the api image —
  # Chromium cannot run under the hardened profile the other containers keep.
  browser:
    build:
      context: ./docker/browser
    init: true
    security_opt:
      - seccomp:./docker/browser/seccomp_profile.json
    environment:
      HTTPS_PROXY: http://proxy:3128
      HTTP_PROXY: http://proxy:3128
    networks:
      - browser
    restart: unless-stopped

  # The forward proxy that does the browser's DNS and refuses private destinations.
  proxy:
    build:
      context: ./docker/proxy
    networks:
      - browser
      - default
    restart: unless-stopped

  # The Python discovery poller (second runtime). Shares the same Postgres; the .NET
  # API owns the schema (DbUp migrates it on startup), so on a cold boot the poller's
  # first tick may run before migrations land — its loop retries, no action needed.
  poller:
    build:
      context: .
      dockerfile: Dockerfile.poller
    environment:
      DATABASE_URL: "postgresql://${POSTGRES_USER:-applytrack}:${POSTGRES_PASSWORD:-JanewayDidNothingWrong}@db:5432/${POSTGRES_DB:-applytrack}"
      # Optional (#300): lets tenants who opt in score listings with Jev. Blank = keywords.
      TYPESAFE_API_KEY: ${TYPESAFE_API_KEY:-}
      # The board sessions (LinkedIn, Handshake, MyGreenhouse) are sealed by the api; the
      # poller opens them with the same key — this, or the generated key file (#340).
      APPLYTRACK_SECRETS_KEY: ${APPLYTRACK_SECRETS_KEY:-}
    # Read-only: the api generates the key, the poller only reads it. The key file is
    # owner-only, so the poller runs as the api image's uid to read it.
    user: "1654:1654"
    volumes:
      - secrets:/var/lib/applytrack:ro
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

networks:
  default:
  # No gateway: a container on only this network cannot reach the host, the LAN,
  # or the internet — the browser's containment boundary.
  browser:
    internal: true

volumes:
  pgdata:
  # The generated encryption key, when APPLYTRACK_SECRETS_KEY is unset. Back it up.
  secrets: