Repository navigation
Expand file tree
/
Copy pathcompose.yaml
More file actions
337 lines (316 loc) · 14.8 KB
/
Copy pathcompose.yaml
File metadata and controls
337 lines (316 loc) · 14.8 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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
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: