Files
jobtrackingapp/docker-compose.yml
T
cesnimda ce76046a29 feat: complete release readiness work
- consolidate API ownership and remove dead vendor code

- add Stripe billing, learning paths, and public CV hardening

- add migration, recovery, security, audit, and browser gates
2026-07-31 16:54:16 +02:00

217 lines
8.9 KiB
YAML

services:
backend:
build:
context: .
dockerfile: JobTrackerApi/Dockerfile
volumes:
- jobtracker_data:/data
environment:
- ASPNETCORE_URLS=http://+:8080
- Data__Root=/data
- Exports__DailyFolder=/data/exports
- Database__Provider=${DATABASE_PROVIDER:-sqlite}
- ConnectionStrings__JobTracker=${JOBTRACKER_CONNECTION_STRING}
# If you enable HTTPS at a reverse proxy (recommended), handle redirects there.
- HttpsRedirection__Enabled=false
# Backend is internal-only here; nginx is the sole trusted ingress.
- Proxy__TrustForwardedHeaders=true
# Authentication (recommended for any non-local deployment)
- Auth__Require=true
- Auth__JwtKey=${AUTH_JWT_KEY}
- Auth__AdminEmail=${AUTH_ADMIN_EMAIL}
- Auth__AdminPassword=${AUTH_ADMIN_PASSWORD}
- Auth__AllowRegistration=${AUTH_ALLOW_REGISTRATION:-false}
- Turnstile__SiteKey=${TURNSTILE_SITE_KEY}
- Turnstile__SecretKey=${TURNSTILE_SECRET_KEY}
- Stripe__SecretKey=${STRIPE_SECRET_KEY}
- Stripe__PricePremium=${STRIPE_PRICE_PREMIUM}
- Stripe__WebhookSecret=${STRIPE_WEBHOOK_SECRET}
# Optional: allow Google / Microsoft ID-token bearer auth (sign-in, not mail access)
- Auth__GoogleClientId=${AUTH_GOOGLE_CLIENT_ID}
- Auth__MicrosoftClientId=${AUTH_MICROSOFT_CLIENT_ID}
- Google__GmailClientSecret=${GOOGLE_GMAIL_CLIENT_SECRET}
- Google__GmailRedirectUri=${GOOGLE_GMAIL_REDIRECT_URI}
# Optional: Outlook / Microsoft 365 mail linking via Microsoft Graph
- Microsoft__ClientId=${MICROSOFT_CLIENT_ID}
- Microsoft__ClientSecret=${MICROSOFT_CLIENT_SECRET}
- Microsoft__TenantId=${MICROSOFT_TENANT_ID}
- Microsoft__RedirectUri=${MICROSOFT_REDIRECT_URI}
- Ai__BaseUrl=${AI_SERVICE_BASE_URL:-http://ai-service:8001}
- Summarizer__BaseUrl=${SUMMARIZER_BASE_URL:-http://ai-service:8001}
# Shared secret for calls to ai-service. Must match AI_SERVICE_TOKEN below.
# Quoted: the `:?` message contains a colon-space, which YAML would otherwise read as a map.
- "Ai__ServiceToken=${AI_SERVICE_TOKEN:?AI_SERVICE_TOKEN must be set - generate one with python -c 'import secrets; print(secrets.token_hex(32))'}"
# Email (SMTP)
# Build metadata should be resolved before deployment. Examples:
# APP_VERSION=1.0.0
# APP_COMMIT_SHA=abc1234
# APP_BUILD_STAMP=2026-03-22 14:00 UTC
# Do not set literal placeholders like $(git rev-parse --short HEAD) in .env.
- App__PublicBaseUrl=${APP_PUBLIC_BASE_URL}
- App__Version=${APP_VERSION}
- App__CommitSha=${APP_COMMIT_SHA}
- App__BuildStamp=${APP_BUILD_STAMP}
- Email__Enabled=${EMAIL_ENABLED}
- Email__SmtpHost=${EMAIL_SMTP_HOST}
- Email__SmtpPort=${EMAIL_SMTP_PORT}
- Email__SmtpEnableSsl=${EMAIL_SMTP_ENABLE_SSL}
- Email__SmtpTimeoutMs=${EMAIL_SMTP_TIMEOUT_MS}
- Email__SmtpUser=${EMAIL_SMTP_USER}
- Email__SmtpPassword=${EMAIL_SMTP_PASSWORD}
- Email__From=${EMAIL_FROM}
- Email__FromName=${EMAIL_FROM_NAME}
expose:
- "8080"
networks:
- default
- shared_services
# The only other member of ai_internal — the backend is the sole permitted caller of
# ai-service.
- ai_internal
restart: unless-stopped
logging:
options:
max-size: "10m"
max-file: "3"
# Liveness only. /health does not touch the database on purpose: a health check that queried
# MariaDB would restart a healthy backend whenever the database blipped.
# start_period covers first-boot schema reconciliation, which can take a while on a new database.
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 90s
frontend:
build:
context: ./job-tracker-ui
# Next's build type-checker needs more than Docker's default 64MB /dev/shm; too little
# causes a SIGSEGV during `npm run build`.
shm_size: '1gb'
args:
- NEXT_PUBLIC_GOOGLE_CLIENT_ID=${AUTH_GOOGLE_CLIENT_ID}
- NEXT_PUBLIC_MICROSOFT_CLIENT_ID=${AUTH_MICROSOFT_CLIENT_ID}
# Optional override; default in production is `/api`
- NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL}
ports:
- "3000:80"
depends_on:
backend:
condition: service_healthy
networks:
- default
- shared_services
restart: unless-stopped
logging:
options:
max-size: "10m"
max-file: "3"
# Cheap liveness: nginx answering on its own port. wget ships with the alpine base.
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:80/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
ai-service:
build:
context: ./tools/summarizer
dockerfile: Dockerfile
environment:
# Point at an existing/shared Ollama by setting OLLAMA_BASE_URL in .env
# (e.g. http://<host-ip>:11435). The in-compose ollama service below is
# opt-in via the "bundled-ollama" profile, so it is NOT started by default
# and no duplicate Ollama container is created.
- OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://ollama:11434}
- OLLAMA_MODEL=${OLLAMA_MODEL:-qwen2.5:7b}
# AI provider for heavy /cv/* calls: ollama (default) | gemini | groq.
# Set AI_PROVIDER=gemini + GEMINI_API_KEY in prod to offload a weak local GPU.
- AI_PROVIDER=${AI_PROVIDER:-ollama}
- GEMINI_API_KEY=${GEMINI_API_KEY:-}
- GEMINI_MODEL=${GEMINI_MODEL:-gemini-2.0-flash}
- GROQ_API_KEY=${GROQ_API_KEY:-}
- GROQ_MODEL=${GROQ_MODEL:-llama-3.3-70b-versatile}
# Shared secret required on every endpoint except /health. Must match Ai__ServiceToken
# on the backend. `:?` so a deploy that forgets it fails loudly instead of booting open.
# Quoted: the `:?` message contains a colon-space, which YAML would otherwise read as a map.
- "AI_SERVICE_TOKEN=${AI_SERVICE_TOKEN:?AI_SERVICE_TOKEN must be set - generate one with python -c 'import secrets; print(secrets.token_hex(32))'}"
# Deliberately NOT published to the host: this service has no user auth and can spend a
# paid provider's API key (AI_PROVIDER=gemini/groq). The backend reaches it in-network at
# http://ai-service:8001. To debug locally, use docker-compose.override.yml rather than
# re-adding a `ports:` here.
expose:
- "8001"
# ai_internal ONLY. Not on `default` (which the frontend shares) and not on
# `shared_services` (which is `external: true`, so any other compose stack on this host can
# join it and would then be able to reach this service). ai_internal carries exactly two
# members: this service and the backend. Nothing else can route to port 8001.
#
# The network is NOT marked `internal: true` — ai-service still needs egress to
# generativelanguage.googleapis.com / api.groq.com when AI_PROVIDER is gemini or groq.
networks:
- ai_internal
restart: unless-stopped
logging:
options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8001/health', timeout=5).read()"]
interval: 30s
timeout: 10s
retries: 3
# Opt-in only: start with `docker compose --profile bundled-ollama up`.
# Left out of the default set so deploys reuse an existing/shared Ollama
# (configured via OLLAMA_BASE_URL) instead of spinning up a duplicate.
ollama:
profiles: ["bundled-ollama"]
image: ollama/ollama:latest
ports:
- "11434:11434"
environment:
- OLLAMA_HOST=0.0.0.0:11434
volumes:
- ollama_data:/root/.ollama
# On ai_internal so the bundled Ollama stays reachable at http://ollama:11434 now that
# ai-service has left the `default`/`shared_services` networks.
#
# NOTE: if you point OLLAMA_BASE_URL at an Ollama running in ANOTHER compose stack, address
# it by host IP (e.g. http://<host-ip>:11435) — ai-service can no longer resolve container
# names on `shared_services`, by design.
networks:
- default
- shared_services
- ai_internal
restart: unless-stopped
logging:
options:
max-size: "10m"
max-file: "3"
gpus: all
healthcheck:
test: ["CMD", "ollama", "list"]
interval: 20s
timeout: 15s
retries: 10
start_period: 20s
volumes:
jobtracker_data:
ollama_data:
networks:
shared_services:
external: true
name: jobtracker_shared
# Private backend <-> ai-service link. Deliberately NOT external: nothing outside this compose
# project can join it, so ai-service is unreachable from the host, from the frontend, and from
# any other stack sharing jobtracker_shared. Egress to cloud AI providers still works because
# this is a normal bridge (not `internal: true`).
ai_internal:
driver: bridge