-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy path.env.prod.dockercompose.example
More file actions
222 lines (210 loc) · 12.4 KB
/
Copy path.env.prod.dockercompose.example
File metadata and controls
222 lines (210 loc) · 12.4 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
# =============================================================================
# Huella Latam — PRODUCTION docker-compose environment (on-premise)
# =============================================================================
# Quick start:
# cp .env.prod.dockercompose.example .env.prod.dockercompose
# chmod 600 .env.prod.dockercompose # it will hold real secrets
# # fill in every empty / <placeholder> value below, then:
# docker compose -f docker-compose.prod.yml --env-file .env.prod.dockercompose up -d --build
#
# Unlike the local-dev template, this file has NO working defaults — every
# placeholder must be filled for the deployment. `.env.prod.dockercompose` is
# gitignored; this template (`.env.prod.dockercompose.example`) is committed.
#
# Full guide: docs/operations/production-deployment.md
#
# ⚠️ Variable precedence: docker-compose interpolation uses
# shell env > --env-file > defaults
# If your shell (e.g. direnv / .envrc) exports any of these vars, those
# exports SILENTLY OVERRIDE this file. See
# `docs/operations/docker-compose.md` → Troubleshooting → "Compose uses the
# wrong value for a variable" for diagnosis and fixes.
# =============================================================================
# -----------------------------------------------------------------------------
# Database (EXTERNAL PostgreSQL — not part of this compose stack)
# -----------------------------------------------------------------------------
# Full connection string to the external PostgreSQL server (version >= 15;
# project standard is 18). Use the restricted APPLICATION user created by your
# DBA, NOT the superuser. Migrations and seeds are applied separately — see the
# deployment guide.
# ⚠️ The password MUST be URL-encoded if it contains special characters
# (@ : / ? # & % $ ...). Encode it with:
# node -e "console.log(encodeURIComponent(process.argv[1]))" 'p@ss#word'
DATABASE_URL=postgresql://<app-user>:<url-encoded-password>@<db-host>:5432/<db-name>?schema=public
# -----------------------------------------------------------------------------
# Migration DB credential — used ONLY by the operator-invoked `migrate` / `seed`
# one-shot services (docker-compose.prod.yml). NOT read by the API.
# -----------------------------------------------------------------------------
# Full connection string for the MIGRATION user — a DIFFERENT, higher-privileged
# role than the application user in DATABASE_URL above. Per the DBA contract it
# needs CONNECT + CREATE on the schema (Prisma creates _prisma_migrations and
# runs DDL). URL-encode the password exactly as for DATABASE_URL.
#
# Least privilege: this only needs a value on the host that RUNS migrations. The
# deploy host (which only runs `up`) does not need it — leaving it empty here
# does NOT break `up`; it only fails a migrate/seed run (fail-fast, via
# validate:version).
#
# ⚠️ Tables end up OWNED by this migration user. If it differs from the app
# user, re-apply the DBA-contract GRANTs after every migrate run.
MIGRATION_DATABASE_URL=postgresql://<migration-user>:<url-encoded-password>@<db-host>:5432/<db-name>?schema=public
# -----------------------------------------------------------------------------
# API (core)
# -----------------------------------------------------------------------------
# Host port to reach the API (container always listens on 8080)
API_PORT=8080
LOG_LEVEL=info
# Deployed version identifier — use the git tag being deployed
APP_VERSION=
# CORS: must match the web app's browser origin EXACTLY (scheme + host + port,
# no trailing slash). Same value as VITE_FRONT_BASE_URL below.
ALLOWED_ORIGIN=http://<server-host-or-dns>
# Whether X-Forwarded-For may set request.ip. SET THIS if a reverse proxy or
# load balancer fronts the stack (the usual case here — see
# docs/operations/production-deployment.md, where TLS termination is expected
# upstream). request.ip is the rate limiter's bucket key: left unset behind a
# proxy, every caller resolves to the proxy's address and the 100 req/min limit
# is shared by ALL of them instead of applied per client. Nothing fails visibly
# when that happens.
# <ip-or-cidr>[,<ip-or-cidr>...] trust only these senders — preferred
# <n> trust n proxy hops in front of the API
# (0-10; out of range refuses to boot, because
# more hops than really exist == `true`)
# loopback|linklocal|uniquelocal Fastify's named ranges
# true trust the whole chain — last resort; on a
# directly-exposed API it lets a caller forge
# the header and evade the limit entirely
# false trust nothing
# If the API really is reached directly, write `false` rather than leaving this
# empty: both trust nothing, but an empty value reads as "nobody decided" and
# the API warns about it on every production boot.
TRUST_PROXY=
# Browser-reachable public origin of THIS API (scheme + host[:port], no path, no
# trailing slash) — e.g. https://api.example.cl. This is the origin part of
# VITE_API_BASE_URL below (which adds the `/api` route prefix). Required only
# when MINIO_RELAY_ENABLED=true: the storage relay rewrites presigned
# URLs to <API_ORIGIN>/api/storage.
API_ORIGIN=
# OPTIONAL — leave empty. With AUTH_PROVIDER=jwks (required here) tokens are
# verified against the IdP's public keys, not this secret, and production
# refuses to boot without the full JWKS block — so the static HMAC fallback
# cannot be reached from this file. An incomplete jwks config fails closed
# instead of downgrading to a shared secret. A blank value reads as unset.
JWT_SECRET=
# -----------------------------------------------------------------------------
# Authentication
# AUTH_PROVIDER: jwks (production)
# jwks → validates OIDC access tokens (Entra, Keycloak, …; set the JWKS block below)
# -----------------------------------------------------------------------------
AUTH_PROVIDER=jwks
# Email promoted to superadmin by the promote-superadmin script (optional)
SUPERADMIN_EMAIL=
# JWKS — how the API validates access tokens (AUTH_PROVIDER=jwks). The API reads
# these directly; there are no AZURE_* auth vars. A .env file can't do the
# conditional derivation Entra needs (tenant-type branching, plus CIAM's
# GUID-vs-subdomain host split), so for Azure Entra derive these from your tenant (see
# docs/infrastructure/AzureAuthenticationSetup.md):
# external (CIAM): JWKS_ISSUER=https://<TENANT_ID>.ciamlogin.com/<TENANT_ID>/v2.0
# JWKS_URI=https://<SUBDOMAIN>.ciamlogin.com/<TENANT_ID>/discovery/v2.0/keys
# organizational : JWKS_ISSUER=https://login.microsoftonline.com/<TENANT_ID>/v2.0
# JWKS_URI=https://login.microsoftonline.com/<TENANT_ID>/discovery/v2.0/keys
# JWKS_AUDIENCE=<API_CLIENT_ID> (bare GUID)
JWKS_URI=
JWKS_ISSUER=
JWKS_AUDIENCE=
JWKS_REQUIRED_SCOPE=
JWKS_SKIP_SCOPE_CHECK=
# -----------------------------------------------------------------------------
# Object storage provider: azure_blob_storage | minio
# Selects the backend for file upload/download and the badge/terms seeds.
# Fill ONLY the matching block below; the API refuses to boot if the selected
# provider's required vars are missing. REQUIRED — there is no default; the API
# refuses to boot if STORAGE_PROVIDER is unset
# (allowed values: azure_blob_storage | minio).
# -----------------------------------------------------------------------------
STORAGE_PROVIDER=azure_blob_storage
# -----------------------------------------------------------------------------
# Azure Blob Storage — required when STORAGE_PROVIDER=azure_blob_storage
# On-premise hosts have NO Azure Managed Identity, so the dedicated storage
# Service Principal is required: set ALL THREE SP vars together (production-grade,
# vault-managed values). SP creation walkthrough:
# docs/operations/docker-compose.md → "Azure Blob Storage".
# Leaving AZURE_STORAGE_ACCOUNT_NAME empty disables file upload/download AND
# the badge/terms seeds — see the seed warning in the deployment guide.
# -----------------------------------------------------------------------------
AZURE_STORAGE_ACCOUNT_NAME=
AZURE_STORAGE_CONTAINER_NAME=files
# Dedicated SP for storage (Directory tenant where the storage account lives —
# intentionally separate from AZURE_TENANT_ID above, which is the auth tenant).
AZURE_STORAGE_TENANT_ID=
AZURE_STORAGE_CLIENT_ID=
AZURE_STORAGE_CLIENT_SECRET=
# -----------------------------------------------------------------------------
# MinIO / S3-compatible — required when STORAGE_PROVIDER=minio
# The MinIO server is EXTERNAL to this compose stack (like PostgreSQL above).
# Point the API at it via MINIO_ENDPOINT + credentials; the bucket must already
# exist and its CORS must allow the web origin (ALLOWED_ORIGIN) for browser
# presigned uploads/downloads. MINIO_BUCKET/REGION/FORCE_PATH_STYLE have working
# defaults — leave them unless your deployment differs.
# -----------------------------------------------------------------------------
MINIO_ENDPOINT=http://<minio-host>:9000
MINIO_ACCESS_KEY=
MINIO_SECRET_KEY=
MINIO_BUCKET=files
MINIO_REGION=us-east-1
MINIO_FORCE_PATH_STYLE=true
# Optional storage relay. Set to `true` and the API rewrites
# presigned MinIO URLs to <API_ORIGIN>/api/storage and proxies them itself, so
# MinIO stays internal — no public MinIO URL and no MinIO CORS needed. Requires
# API_ORIGIN (in the API core section above). Leave `false` to serve
# browser-direct presigned URLs (requires MinIO reachable by the browser + CORS
# allowing ALLOWED_ORIGIN).
MINIO_RELAY_ENABLED=false
# -----------------------------------------------------------------------------
# Chatbot (optional AI feature — DPG optionality)
# -----------------------------------------------------------------------------
# Off by default: the platform runs fully without AI and needs no cloud LLM.
# To enable, set CHATBOT_ENABLED=true AND provision a real provider:
# LLM_PROVIDER=azure-openai
# AZURE_OPENAI_ENDPOINT=... # your Azure OpenAI resource endpoint
# AZURE_OPENAI_DEPLOYMENT_NAME=... # your chat model deployment
# COOKIE_SECRET=... # long random string (signs the session cookie)
# and set VITE_CHATBOT_ENABLED=true below. Booting with CHATBOT_ENABLED=true and
# LLM_PROVIDER=mock is rejected in production.
CHATBOT_ENABLED=false
# -----------------------------------------------------------------------------
# Web (Vite) — inlined into the bundle at BUILD time (docker build args).
# Changing any of these requires rebuilding the web image.
# -----------------------------------------------------------------------------
# Host port to reach the web app
WEB_PORT=80
# Browser-reachable API URL — uses the host-exposed API_PORT above, NOT the
# container-internal port.
VITE_API_BASE_URL=http://<server-host-or-dns>:8080
# Browser-reachable web URL — must match ALLOWED_ORIGIN above.
VITE_FRONT_BASE_URL=http://<server-host-or-dns>
VITE_APP_VERSION=
VITE_IS_DEMO_APP=false
# Mirror of CHATBOT_ENABLED above — set to true only when enabling the chatbot.
VITE_CHATBOT_ENABLED=false
# Generic OIDC config for the frontend (build-time; rebuild the web image on change).
# ISSUER + CLIENT_ID + SCOPES are required — the app fails loud at boot if any is
# empty. VITE_OIDC_SCOPES baseline: openid (OIDC id_token) + email
# (backend rejects tokens without it) + profile (display name). offline_access is
# intentionally NOT requested: silent renew uses the SSO-session-bound refresh
# token (8h) instead of a long-lived offline one.
# Keycloak → VITE_OIDC_ISSUER=https://<keycloak-host>/realms/huella
# Entra → VITE_OIDC_ISSUER=https://<subdomain>.ciamlogin.com/<tenant-id>/v2.0
# and append " api://<API_CLIENT_ID>/access_as_user" to VITE_OIDC_SCOPES
VITE_OIDC_ISSUER=
VITE_OIDC_CLIENT_ID=huella-web
VITE_OIDC_SCOPES=openid profile email
VITE_OIDC_REDIRECT_URI=${VITE_FRONT_BASE_URL}/auth/callback
VITE_OIDC_POST_LOGOUT_REDIRECT_URI=${VITE_FRONT_BASE_URL}
# Browser-facing object-storage origin, baked into the nginx CSP (connect-src/
# img-src) so the browser may PUT/GET presigned URLs. REQUIRED for browser uploads.
# Use the storage URL the BROWSER reaches:
# MinIO (on-prem) → https://storage.example.com
# Azure Blob → https://<account>.blob.core.windows.net
# Must match MinIO's public presign endpoint and its CORS-allowed origin.
STORAGE_ORIGIN=