Repository navigation
Expand file tree
/
Copy pathdocker-compose.prod.yml
More file actions
240 lines (231 loc) · 12.3 KB
/
Copy pathdocker-compose.prod.yml
File metadata and controls
240 lines (231 loc) · 12.3 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
# ─── Identity Atlas — Production Docker Compose ─────────────────────────────
#
# For end users: no git clone needed, pulls pre-built images from ghcr.io.
#
# Quick start (PowerShell version: docs/quickstart.md):
# curl -O https://raw.githubusercontent.com/Fortigi/IdentityAtlas/main/docker-compose.prod.yml
# [ -f .env ] || echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)" > .env
# docker compose -f docker-compose.prod.yml up -d --pull always
#
# Then open http://localhost:3001 and click "Load Demo Data" to get started.
#
# Configuration:
# POSTGRES_PASSWORD is the only required setting; put it in a .env file next
# to this file (the second command above) or pass it inline. Everything else
# has a default — setup/config/.env.example lists every variable.
#
# Image channel (IMAGE_TAG):
# latest — stable customer release (default, omit IMAGE_TAG or leave blank)
# edge — latest merged commit on main; may be unstable (set IMAGE_TAG=edge)
# 5.2.1.0 — pin to a specific release version. When you pin an OLDER release,
# use the docker-compose.prod.yml from that release too: this file
# relies on features of the current images (see COMPOSE_FILE_VERSION).
#
# SECURITY:
# - POSTGRES_PASSWORD is REQUIRED — this file refuses to start without it (no
# default password is shipped). Set a strong, unique value. It reaches the
# containers as a Compose secret (a file under /run/secrets), not as a plain
# container environment variable, so it does not show up in `docker inspect`.
# - Postgres is bound to loopback (127.0.0.1) only and is NOT reachable from
# other hosts; the web container reaches it over the internal compose network.
# Set POSTGRES_BIND_HOST=0.0.0.0 only if you deliberately need off-host access.
# - The secrets-vault master key is auto-generated on first start into the
# web-only `web_keys` volume — the worker container cannot read it. Back that
# volume up, or set IDENTITY_ATLAS_MASTER_KEY explicitly (see below).
# - Every container runs with no-new-privileges, a dropped capability set and
# process / memory limits. Raise the limits for very large tenants with
# POSTGRES_MEM_LIMIT / WEB_MEM_LIMIT / WORKER_MEM_LIMIT (e.g. 16g).
# ─────────────────────────────────────────────────────────────────────────────
# Fail fast when POSTGRES_PASSWORD is unset or empty. Kept in an extension field
# so the value is only used for this check; the containers get it as a secret.
x-required-settings:
postgres-password: "${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set — add a line POSTGRES_PASSWORD=<strong value> to the .env file next to this compose file (see docs/quickstart.md)}"
services:
# ── PostgreSQL 16 ───────────────────────────────────────────────────────────
# Replaced SQL Server in v5. The migrations runner inside the web container
# creates all tables on first startup, so there is no separate init step.
postgres:
image: postgres:16-alpine
# Parallel workers exchange data through /dev/shm. Docker's 64 MB default is
# too small at scale: a parallel VACUUM failed with "could not resize shared
# memory segment ... No space left on device" on a 41M-assignment database, and
# a parallel hash join hits the same limit. A ceiling, not a reservation:
# tmpfs only uses memory for what is actually written.
shm_size: 1gb
environment:
POSTGRES_DB: "${POSTGRES_DB:-identity_atlas}"
POSTGRES_USER: "${POSTGRES_USER:-identity_atlas}"
# Read by the official image's entrypoint when it initialises a new data
# directory. An existing database keeps the password it was created with.
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
secrets:
- postgres_password
# Bind to loopback by default — the web container reaches Postgres over the
# internal compose network (hostname `postgres`), so it never needs to be on
# the host's external interfaces. Override POSTGRES_BIND_HOST=0.0.0.0 only if
# you deliberately need off-host access (and firewall it).
ports:
- "${POSTGRES_BIND_HOST:-127.0.0.1}:${POSTGRES_PORT:-5432}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-identity_atlas} -d ${POSTGRES_DB:-identity_atlas}"]
interval: 10s
timeout: 5s
retries: 30
start_period: 30s
# Container hardening. The official image starts as root to fix ownership of
# the data directory and then drops to the `postgres` user, which needs
# exactly these capabilities; everything else is dropped.
security_opt:
- no-new-privileges:true
cap_drop: [ALL]
cap_add: [CHOWN, DAC_OVERRIDE, FOWNER, SETGID, SETUID]
pids_limit: 2048
mem_limit: "${POSTGRES_MEM_LIMIT:-8g}"
restart: unless-stopped
# ── Web (serves built frontend + API) ────────────────────────────────────────
web:
image: ghcr.io/fortigi/identity-atlas:${IMAGE_TAG:-latest}
environment:
NODE_ENV: production
USE_SQL: "true" # legacy flag — still used by routes to enable DB-backed mode
# Database connection. The password is read from the Compose secret file
# (POSTGRES_PASSWORD_FILE); it is not part of the container environment.
POSTGRES_HOST: postgres
POSTGRES_PORT: "5432"
POSTGRES_DB: "${POSTGRES_DB:-identity_atlas}"
POSTGRES_USER: "${POSTGRES_USER:-identity_atlas}"
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
AUTH_ENABLED: "${AUTH_ENABLED:-false}"
PORT: "3001"
# Release channel this deployment tracks (edge|beta|latest, or a pinned
# version). The auto-update check uses it to compare against the right
# "newest version". Mirrors the IMAGE_TAG that selects the image above;
# if unset, the channel is inferred from the running version.
IMAGE_TAG: "${IMAGE_TAG:-latest}"
# Compose file version — the API checks this to warn users when their
# local docker-compose.prod.yml is outdated. Bump this number whenever
# the compose file changes in a way that affects runtime behavior
# (new env vars, volume mounts, group_add, etc.).
COMPOSE_FILE_VERSION: "4"
# Where the web container keeps the secrets-vault master key when it
# auto-generates one: the web-only `web_keys` volume below, which the
# worker does not mount. An existing key in the shared job_data volume
# (/data/uploads/.master-key) is moved here on first start.
IDENTITY_ATLAS_KEY_DIR: /data/keys
# Optional explicit master key for the secrets vault (LLM API keys,
# scraper credentials). Takes precedence over the key file. Set it so the
# key can be backed up alongside other root secrets. To keep it out of the
# container environment as well, mount it as a secret file and set
# IDENTITY_ATLAS_MASTER_KEY_FILE instead.
IDENTITY_ATLAS_MASTER_KEY: "${IDENTITY_ATLAS_MASTER_KEY:-}"
# Optional Entra ID auth for the UI. AUTH_ENABLED=true alone is NOT
# enough — the SPA also needs the tenant + client IDs. Both vars
# default to empty, which the API treats as "auth enabled but not
# configured" and renders the setup-required page until they're set.
AUTH_TENANT_ID: "${AUTH_TENANT_ID:-}"
AUTH_CLIENT_ID: "${AUTH_CLIENT_ID:-}"
# How many distinct values a filter/attribute dropdown preloads per field
# (default 500, max 5000). Values past the page stay reachable through the
# dropdown's search box. Lower it — e.g. MATRIX_VALUE_PAGE_SIZE=5 — to
# exercise that path on a deployment with only a few hundred objects.
# See docs/architecture/matrix.md → "Attribute values".
MATRIX_VALUE_PAGE_SIZE: "${MATRIX_VALUE_PAGE_SIZE:-}"
# Report generator (experimental). Only reachable when the
# report-generator profile is started; the feature is off until enabled.
FEATURE_CUSTOM_REPORTS: "${FEATURE_CUSTOM_REPORTS:-}"
NL_REPORTS_LLM_URL: "http://report-generator:8080"
secrets:
- postgres_password
ports:
- "${PORT:-3001}:3001"
volumes:
- job_data:/data/uploads
- web_keys:/data/keys
networks: [default, report-generator]
depends_on:
postgres:
condition: service_healthy
# Runs as the unprivileged `node` user and needs no capabilities.
security_opt:
- no-new-privileges:true
cap_drop: [ALL]
pids_limit: 2048
mem_limit: "${WEB_MEM_LIMIT:-8g}"
restart: unless-stopped
# ── Worker (PowerShell — crawlers, risk scoring, account correlation) ───────
# In v5 the worker container has NO database driver. It talks to the API
# for everything (job pickup, ingest, progress reporting). It reads its own
# API key from the shared job_data volume; it never sees the database
# password or the vault master key. Crawler credentials reach it per job.
worker:
image: ghcr.io/fortigi/identity-atlas-worker:${IMAGE_TAG:-latest}
environment:
WEB_API_URL: "http://web:3001/api"
# Optional fixed crawler API key. Leave empty (the default) and the worker
# uses the key the web container writes to the shared volume.
CRAWLER_API_KEY: "${CRAWLER_API_KEY:-}"
volumes:
- job_data:/data/uploads
depends_on:
web:
condition: service_started
# Runs as uid 1000 and needs no capabilities. ForEach-Object -Parallel
# crawlers use many threads, which count towards pids_limit.
security_opt:
- no-new-privileges:true
cap_drop: [ALL]
pids_limit: 4096
mem_limit: "${WORKER_MEM_LIMIT:-8g}"
restart: unless-stopped
# ── Report generator (EXPERIMENTAL, opt-in) ─────────────────────────────────
# Turns a report described in plain language into a report definition. One small
# model, baked into this image, running on CPU. Enable with:
#
# COMPOSE_PROFILES=report-generator (in .env)
# FEATURE_CUSTOM_REPORTS=true (or switch it on in Admin → Experimental)
#
# Questions never leave the deployment: the container has no published port, sits
# on an internal network with the web container only, and cannot reach the
# internet, the database or the worker. See docs/reference/report-generator.md.
report-generator:
image: ghcr.io/fortigi/identity-atlas-report-generator:${IMAGE_TAG:-latest}
profiles: ["report-generator"]
environment:
# Threads = the CPUs this container may use. Keep the two in step.
LLAMA_ARG_THREADS: "${REPORT_GENERATOR_CPUS:-2}"
# The model is loaded on the first question and unloaded after this many seconds
# unused, returning ~3 GB to the host. 0 keeps it loaded all the time.
REPORT_GENERATOR_IDLE_SECONDS: "${REPORT_GENERATOR_IDLE_SECONDS:-900}"
volumes:
# The processed system prompt (~600 MB): makes a restart instant instead of
# re-reading the prompt for minutes.
- report_generator_slots:/slots
networks: [report-generator]
# Runs as uid 1000 on an unprivileged port and needs no capabilities.
security_opt:
- no-new-privileges:true
cap_drop: [ALL]
pids_limit: 512
mem_limit: "${REPORT_GENERATOR_MEMORY:-4g}"
cpus: "${REPORT_GENERATOR_CPUS:-2}"
restart: unless-stopped
networks:
# Only the web container and the model server are on this one, and it has no
# route to the internet (internal: true).
report-generator:
internal: true
secrets:
# Created from the POSTGRES_PASSWORD variable (.env or shell) and mounted
# read-only at /run/secrets/postgres_password in the containers that list it.
postgres_password:
environment: POSTGRES_PASSWORD
volumes:
postgres_data:
job_data:
# Web-only: holds the auto-generated vault master key. Include it in backups.
web_keys:
# The report generator's processed system prompt. Disposable: it is rebuilt
# (a few minutes, once) if lost. No need to back it up.
report_generator_slots: