Repository navigation
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
182 lines (175 loc) · 8.43 KB
/
Copy pathdocker-compose.yml
File metadata and controls
182 lines (175 loc) · 8.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
services:
db:
# pgvector's build, not stock postgres: migration 0092 runs `CREATE EXTENSION
# vector`, which the stock image doesn't ship. This is Debian-based (bookworm),
# not Alpine — pgvector has no Alpine variant — but pins the same Postgres major
# (18) as the stock image it replaces, so the version-subdirectory volume
# behaviour noted below is unaffected.
image: pgvector/pgvector:pg18
environment:
POSTGRES_USER: hire
POSTGRES_PASSWORD: hire
POSTGRES_DB: hire
ports:
- "${DB_HOST_PORT:-5432}:5432"
volumes:
# postgres:18+ stores data in a version-specific subdirectory and expects
# the volume at /var/lib/postgresql (not the legacy .../data path, which it
# now treats as an unused mount and refuses to start).
- pgdata:/var/lib/postgresql
# Migrations are applied automatically by Postgres on first volume init.
- ./migrations:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U hire -d hire"]
interval: 5s
timeout: 5s
retries: 5
# Search backend for GET /api/v1/jobs/search and the reindex command. The
# huggingFace embedder downloads and runs its model inside this container, so
# the model is cached in the meilisearch_data volume across restarts.
meilisearch:
image: getmeili/meilisearch:v1.49.0
environment:
MEILI_MASTER_KEY: "${MEILI_MASTER_KEY:-dev-insecure-meili-key}"
MEILI_ENV: development
ports:
- "${MEILI_HOST_PORT:-7700}:7700"
volumes:
- meilisearch_data:/meili_data
# Backs the shared rate limiter (internal/ratelimit). A required dependency, not
# an optional one like Meili — no volume, since rate-limit counters are
# ephemeral/TTL'd and losing them on restart is a fail-open no-op, not data loss.
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
# Local S3-compatible storage for résumé upload (internal/blobstore). Without it
# `PUT/POST /me/resume` runs with storage disabled (S3_* unset) and callers degrade to
# the previous per-request extraction — real locally, but no upload persists, so the
# fit analysis never sees a structured résumé. minio-init creates the bucket once and
# exits; the app waits on that, not on minio itself, since the bucket must exist first.
minio:
image: minio/minio:latest
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
command: server /data
ports:
- "${MINIO_HOST_PORT:-9000}:9000"
volumes:
- minio_data:/data
minio-init:
image: minio/mc:latest
depends_on:
- minio
entrypoint: >
/bin/sh -c "
until mc alias set local http://minio:9000 minioadmin minioadmin; do sleep 1; done;
mc mb -p local/hire-resumes;
exit 0;
"
app:
build: .
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
minio-init:
condition: service_completed_successfully
environment:
PORT: "8080"
DATABASE_URL: "postgres://hire:hire@db:5432/hire?sslmode=disable"
# Dev defaults pointing at the local `minio` service above. Prod sets its own
# S3_* in its .env, pointing at the real bucket — these values never leave Docker.
S3_ENDPOINT: "${S3_ENDPOINT:-http://minio:9000}"
S3_BUCKET: "${S3_BUCKET:-hire-resumes}"
S3_ACCESS_KEY: "${S3_ACCESS_KEY:-minioadmin}"
S3_SECRET_KEY: "${S3_SECRET_KEY:-minioadmin}"
AWS_REGION: "${AWS_REGION:-us-east-1}"
# PII_FILTER_URL intentionally has no local default: it fronts a real span-detection
# model (services/pii-filter) with no lightweight local stand-in, so unset here is the
# correct out-of-the-box state — résumé structuring/import degrade gracefully without
# it (internal/resumeextract's Enabled() check), same as an unconfigured LLM.
PII_FILTER_URL: "${PII_FILTER_URL:-}"
# LLM-backed features (enrich, fit analysis, résumé structuring, the assistant, …)
# were silently unreachable locally: these were never passed through from .env at
# all, so every model call degraded as "unconfigured" with no error. Empty is a
# valid, working default (the same degrade-gracefully behaviour, just intentional
# now) — set real values in .env to actually exercise a model-backed surface.
LLM_BASE_URL: "${LLM_BASE_URL:-}"
LLM_API_KEY: "${LLM_API_KEY:-}"
LLM_MODEL: "${LLM_MODEL:-}"
# Optional: the assistant's own model, chosen for tool calling rather than
# cheap one-shot extraction. Falls back to LLM_MODEL.
ASSISTANT_MODEL: "${ASSISTANT_MODEL:-}"
# Bullets allowed per experience or project. Unset (or below 1) means 20.
# The agent reads the live value into its cv_edit tool description, so
# raising this needs no prompt change.
CV_MAX_BULLETS: "${CV_MAX_BULLETS:-}"
# Fit-analysis sanitize ceilings for untrusted model output (see .env.example).
# Empty/unset falls back to internal/matchanalysis's own DefaultBounds — these
# were documented in .env.example but never reached the container, so setting
# them in .env silently had no effect.
MATCH_ANALYSIS_MAX_COMMENT_RUNES: "${MATCH_ANALYSIS_MAX_COMMENT_RUNES:-}"
MATCH_ANALYSIS_MAX_LIST_ITEM_RUNES: "${MATCH_ANALYSIS_MAX_LIST_ITEM_RUNES:-}"
MATCH_ANALYSIS_MAX_RECOMMEND_RUNES: "${MATCH_ANALYSIS_MAX_RECOMMEND_RUNES:-}"
MATCH_ANALYSIS_MAX_REQ_TEXT_RUNES: "${MATCH_ANALYSIS_MAX_REQ_TEXT_RUNES:-}"
MATCH_ANALYSIS_MAX_REQ_EVIDENCE_RUNES: "${MATCH_ANALYSIS_MAX_REQ_EVIDENCE_RUNES:-}"
MATCH_ANALYSIS_MAX_STRENGTHS: "${MATCH_ANALYSIS_MAX_STRENGTHS:-}"
MATCH_ANALYSIS_MAX_GAPS: "${MATCH_ANALYSIS_MAX_GAPS:-}"
MATCH_ANALYSIS_MAX_REQUIREMENTS: "${MATCH_ANALYSIS_MAX_REQUIREMENTS:-}"
MATCH_ANALYSIS_MAX_SIGNALS: "${MATCH_ANALYSIS_MAX_SIGNALS:-}"
MATCH_ANALYSIS_MAX_SIGNAL_QUOTE_RUNES: "${MATCH_ANALYSIS_MAX_SIGNAL_QUOTE_RUNES:-}"
MATCH_ANALYSIS_MAX_SIGNAL_INSIGHT_RUNES: "${MATCH_ANALYSIS_MAX_SIGNAL_INSIGHT_RUNES:-}"
LANGFUSE_BASE_URL: "${LANGFUSE_BASE_URL:-}"
LANGFUSE_PUBLIC_KEY: "${LANGFUSE_PUBLIC_KEY:-}"
LANGFUSE_SECRET_KEY: "${LANGFUSE_SECRET_KEY:-}"
# Required by the auth surface (server fails fast if unset or under 32 bytes).
# Dev default — set a real, random JWT_SECRET in any non-local deployment.
JWT_SECRET: "${JWT_SECRET:-dev-insecure-secret-change-me-0123456789}"
# OAuth callbacks redirect the browser back to this origin; it also
# derives the per-provider callback URL registered at each provider.
FRONTEND_ORIGIN: "${FRONTEND_ORIGIN:-http://localhost:5173}"
# OAuth sign-in is optional: a provider with empty credentials is simply
# disabled (no button in the SPA, its routes 404). Set the values in .env.
OAUTH_GOOGLE_CLIENT_ID: "${OAUTH_GOOGLE_CLIENT_ID:-}"
OAUTH_GOOGLE_CLIENT_SECRET: "${OAUTH_GOOGLE_CLIENT_SECRET:-}"
OAUTH_GITHUB_CLIENT_ID: "${OAUTH_GITHUB_CLIENT_ID:-}"
OAUTH_GITHUB_CLIENT_SECRET: "${OAUTH_GITHUB_CLIENT_SECRET:-}"
OAUTH_LINKEDIN_CLIENT_ID: "${OAUTH_LINKEDIN_CLIENT_ID:-}"
OAUTH_LINKEDIN_CLIENT_SECRET: "${OAUTH_LINKEDIN_CLIENT_SECRET:-}"
OAUTH_APPLE_CLIENT_ID: "${OAUTH_APPLE_CLIENT_ID:-}"
OAUTH_APPLE_TEAM_ID: "${OAUTH_APPLE_TEAM_ID:-}"
OAUTH_APPLE_KEY_ID: "${OAUTH_APPLE_KEY_ID:-}"
OAUTH_APPLE_PRIVATE_KEY: "${OAUTH_APPLE_PRIVATE_KEY:-}"
# Search is optional: an empty MEILI_MASTER_KEY disables the search
# endpoint without affecting the rest of the API.
MEILI_URL: "http://meilisearch:7700"
MEILI_MASTER_KEY: "${MEILI_MASTER_KEY:-dev-insecure-meili-key}"
REDIS_URL: "redis://redis:6379/0"
ports:
- "${HIRE_HOST_PORT:-8080}:8080"
# SvelteKit SSR frontend. nginx (:80) fronts both the Node SSR server and the
# Go API, keeping them same-origin for the auth cookie. The browser talks only
# to this container.
web:
build:
context: .
dockerfile: web/Dockerfile
environment:
# Public origin for canonical/sitemap/JSON-LD — match the host-facing URL.
ORIGIN: "${WEB_ORIGIN:-http://localhost:8090}"
# The Node SSR server fetches the Go API server-to-server (never relative).
API_INTERNAL_URL: "http://app:8080"
ports:
- "${WEB_HOST_PORT:-8090}:80"
depends_on:
- app
volumes:
pgdata:
meilisearch_data:
minio_data: