docker-compose.yml
Rama main · réplica de lectura del repositorio original.
# 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: