1# =============================================================================
2# AI Software Factory — local development environment
3# =============================================================================
4#
5# QUICK START
6# pnpm docker:up # = docker compose up -d postgres redis minio opensearch
7# # (one-shot init jobs minio-init / opensearch-init run
8# # automatically and exit; they are NOT long-running services)
9# pnpm docker:down # = docker compose down
10#
11# # Start the application stack too (web + api + worker + realtime):
12# # (profile "apps"; see TASK-0003 — the task spec + dev guide name it `apps`)
13# docker compose --profile apps up -d
14#
15# # Logs / status:
16# docker compose logs -f opensearch
17# docker compose ps
18#
19# # Reset ALL data (destroys volumes):
20# docker compose down -v
21#
22# ENVIRONMENT
23# All variables use the ${VAR:-default} form, so this file works even when no
24# .env exists. Defaults mirror ../../.env.example and .env.docker:
25# POSTGRES_USER=factory POSTGRES_PASSWORD=factory POSTGRES_DB=factory
26# MINIO_ROOT_USER=minioadmin MINIO_ROOT_PASSWORD=minioadmin
27# S3_BUCKET=factory-uploads
28# To override, create infrastructure/docker/.env (compose auto-loads it) or
29# export the variables in your shell. Copy .env.docker as a starting point.
30#
31# The app services (profile "apps") load the repo-root .env via env_file;
32# create it from ../../.env.example first.
33# =============================================================================
34
35name: factory
36
37networks:
38 factory-net:
39 driver: bridge
40
41volumes:
42 pgdata:
43 redisdata:
44 miniodata:
45 opensearchdata:
46
47services:
48 # ---------------------------------------------------------------------------
49 # Data layer
50 # ---------------------------------------------------------------------------
51 postgres:
52 image: postgres:16-alpine
53 container_name: factory-postgres
54 environment:
55 POSTGRES_USER: ${POSTGRES_USER:-factory}
56 POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-factory}
57 POSTGRES_DB: ${POSTGRES_DB:-factory}
58 ports:
59 - "5432:5432"
60 volumes:
61 - pgdata:/var/lib/postgresql/data
62 # Extensions (citext, pg_trgm, btree_gist) are installed once on first
63 # bootstrap of an EMPTY volume by the init script below.
64 - ./init/01-extensions.sql:/docker-entrypoint-initdb.d/01-extensions.sql:ro
65 healthcheck:
66 test:
67 [
68 "CMD-SHELL",
69 "pg_isready -U $${POSTGRES_USER:-factory} -d $${POSTGRES_DB:-factory}",
70 ]
71 interval: 5s
72 timeout: 5s
73 retries: 12
74 start_period: 10s
75 networks:
76 - factory-net
77 restart: unless-stopped
78
79 redis:
80 image: redis:7-alpine
81 container_name: factory-redis
82 # Optional auth for local development. UNCOMMENT the line below AND the
83 # healthcheck auth form together if you want a password-protected Redis:
84 # command: ["redis-server", "--requirepass", "${REDIS_PASSWORD:-}"]
85 ports:
86 - "6379:6379"
87 volumes:
88 - redisdata:/data
89 healthcheck:
90 # Plain PING (no auth). If you enable requirepass above, switch to:
91 # redis-cli -a "$REDIS_PASSWORD" ping | grep -q PONG
92 test: ["CMD-SHELL", "redis-cli ping | grep -q PONG"]
93 interval: 5s
94 timeout: 3s
95 retries: 12
96 start_period: 5s
97 networks:
98 - factory-net
99 restart: unless-stopped
100
101 minio:
102 image: minio/minio:latest
103 container_name: factory-minio
104 command: server /data --console-address ":9001"
105 environment:
106 MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
107 MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin}
108 ports:
109 - "9000:9000" # S3 API
110 - "9001:9001" # Web console
111 volumes:
112 - miniodata:/data
113 healthcheck:
114 test: ["CMD-SHELL", "curl -fsS http://localhost:9000/minio/health/live"]
115 interval: 5s
116 timeout: 5s
117 retries: 12
118 start_period: 10s
119 networks:
120 - factory-net
121 restart: unless-stopped
122
123 # One-shot bootstrap: create the files + thumbs buckets (Phase 9 — both
124 # PRIVATE: 'none' = no anonymous read/list/download) and make sure no
125 # anonymous policy is attached. Exits 0 after success; restarts never.
126 minio-init:
127 image: minio/mc:latest
128 container_name: factory-minio-init
129 depends_on:
130 minio:
131 condition: service_healthy
132 restart: "no"
133 entrypoint: >
134 /bin/sh -c "
135 set -e;
136 mc alias set local http:
137 mc mb --ignore-existing local/${S3_BUCKET_FILES:-factory-files};
138 mc mb --ignore-existing local/${S3_BUCKET_THUMBS:-factory-thumbs};
139 # 'none' = fully private (no anonymous read/list/download)
140 mc anonymous set none local/${S3_BUCKET_FILES:-factory-files};
141 mc anonymous set none local/${S3_BUCKET_THUMBS:-factory-thumbs};
142 echo 'minio-init: buckets ${S3_BUCKET_FILES:-factory-files} + ${S3_BUCKET_THUMBS:-factory-thumbs} ready (private)';
143 "
144 environment:
145 MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
146 MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin}
147 networks:
148 - factory-net
149
150 # ClamAV anti-virus daemon (Phase 9 file pipeline). The official image
151 # runs `clamd` with the TCP stream socket on :3310 (the INIC/INOK/INOC
152 # protocol the worker's `scanBuffer` client speaks) and downloads the
153 # daily signature set on boot. The worker connects over TCP
154 # (CLAMAV_HOST=clamav inside the compose network / localhost on the host).
155 # Production deploys MUST run this (the worker refuses to boot without a
156 # reachable scanner in NODE_ENV=production — fail closed).
157 clamav:
158 image: clamav/clamav:latest
159 container_name: factory-clamav
160 ports:
161 - "3310:3310"
162 networks:
163 - factory-net
164 restart: unless-stopped
165
166 opensearch:
167 image: opensearchproject/opensearch:2.15.0
168 container_name: factory-opensearch
169 environment:
170 - discovery.type=single-node
171 # !!! DEVELOPMENT ONLY !!!
172 # Security plugin is DISABLED (unauthenticated HTTP). NEVER run this
173 # configuration in production — production deploys use managed OpenSearch
174 # with security enabled, TLS and per-user credentials (SEARCH_USERNAME /
175 # SEARCH_PASSWORD from .env.example).
176 - plugins.security.disabled=true
177 # OpenSearch >= 2.12 requires an initial admin password to bootstrap the
178 # security plugin even when `plugins.security.disabled=true` (the demo
179 # installer still runs on first boot of a fresh volume). Dev-only
180 # default, matching the postgres/minio credential convention above —
181 # never used in production (managed OpenSearch + real credentials).
182 - OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD:-FactoryDev!2026}
183 - OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m
184 - bootstrap.memory_lock=true
185 ulimits:
186 memlock:
187 soft: -1
188 hard: -1
189 nofile:
190 soft: 65536
191 hard: 65536
192 ports:
193 - "9200:9200"
194 volumes:
195 - opensearchdata:/usr/share/opensearch/data
196 healthcheck:
197 # HTTP (security disabled in dev), not HTTPS.
198 test:
199 [
200 "CMD-SHELL",
201 "curl -sf http://localhost:9200/_cluster/health | grep -qE '\"status\":\"(green|yellow)\"'",
202 ]
203 interval: 10s
204 timeout: 10s
205 retries: 30
206 start_period: 40s
207 networks:
208 - factory-net
209 restart: unless-stopped
210
211 # One-shot bootstrap: create the index template + factory-docs index.
212 # Reads the mapping from ./opensearch/init-indices.json (mounted read-only).
213 opensearch-init:
214 image: opensearchproject/opensearch:2.15.0
215 container_name: factory-opensearch-init
216 depends_on:
217 opensearch:
218 condition: service_healthy
219 restart: "no"
220 volumes:
221 - ./opensearch-init.sh:/usr/local/bin/opensearch-init.sh:ro
222 - ./opensearch/init-indices.json:/usr/local/share/opensearch/init-indices.json:ro
223 entrypoint: ["/usr/local/bin/opensearch-init.sh"]
224 environment:
225 OPENSEARCH_URL: ${SEARCH_URL:-http:
226 networks:
227 - factory-net
228
229 # ---------------------------------------------------------------------------
230 # Application services — started only with: docker compose --profile apps up -d
231 # (They are NOT started by the default `docker compose up` / `pnpm docker:up`.)
232 # All of them expect the repo-root .env (see env_file). Build contexts are the
233 # repo root so multi-stage Dockerfiles can install the whole workspace.
234 # ---------------------------------------------------------------------------
235 api:
236 image: ai-software-factory/api:local
237 build:
238 context: ../..
239 dockerfile: apps/api/Dockerfile
240 container_name: factory-api
241 profiles: ["apps"]
242 env_file:
243 - path: ../../.env
244 required: false
245 environment:
246 NODE_ENV: production
247 PORT: "4000"
248 # Inside the compose network services are reachable by name.
249 DATABASE_URL: postgresql:
250 REDIS_URL: redis:
251 S3_ENDPOINT: http:
252 SEARCH_URL: http:
253 ports:
254 - "4000:4000"
255 depends_on:
256 postgres:
257 condition: service_healthy
258 redis:
259 condition: service_healthy
260 minio:
261 condition: service_healthy
262 networks:
263 - factory-net
264 restart: unless-stopped
265
266 web:
267 image: ai-software-factory/web:local
268 build:
269 context: ../..
270 dockerfile: apps/web/Dockerfile
271 container_name: factory-web
272 profiles: ["apps"]
273 env_file:
274 - path: ../../.env
275 required: false
276 environment:
277 NODE_ENV: production
278 # NEXT_PUBLIC_* values are baked in at BUILD time by Next.js; they are set
279 # here so the image build of a full-stack compose run sees them too.
280 NEXT_PUBLIC_API_URL: ${API_URL:-http:
281 NEXT_PUBLIC_REALTIME_URL: ${REALTIME_URL:-ws:
282 ports:
283 - "3000:3000"
284 depends_on:
285 - api
286 networks:
287 - factory-net
288 restart: unless-stopped
289
290 worker:
291 image: ai-software-factory/worker:local
292 build:
293 context: ../..
294 dockerfile: apps/worker/Dockerfile
295 container_name: factory-worker
296 profiles: ["apps"]
297 env_file:
298 - path: ../../.env
299 required: false
300 environment:
301 NODE_ENV: production
302 DATABASE_URL: postgresql:
303 REDIS_URL: redis:
304 S3_ENDPOINT: http:
305 # Health server (TASK-0009 item 4): /health + /health/ready on 4003.
306 HEALTH_PORT: "4003"
307 ports:
308 - "4003:4003" # health only — the worker has no business HTTP surface
309 depends_on:
310 postgres:
311 condition: service_healthy
312 redis:
313 condition: service_healthy
314 networks:
315 - factory-net
316 restart: unless-stopped
317
318 realtime:
319 image: ai-software-factory/realtime:local
320 build:
321 context: ../..
322 dockerfile: apps/realtime/Dockerfile
323 container_name: factory-realtime
324 profiles: ["apps"]
325 env_file:
326 - path: ../../.env
327 required: false
328 environment:
329 NODE_ENV: production
330 REALTIME_PORT: "4001"
331 DATABASE_URL: postgresql:
332 REDIS_URL: redis:
333 ports:
334 - "4001:4001" # ws
335 - "4002:4002" # health (/health, /health/ready, /metrics)
336 depends_on:
337 postgres:
338 condition: service_healthy
339 redis:
340 condition: service_healthy
341 networks:
342 - factory-net
343 restart: unless-stopped
344