# ============================================================================= # AI Software Factory — local development environment # ============================================================================= # # QUICK START # pnpm docker:up # = docker compose up -d postgres redis minio opensearch # # (one-shot init jobs minio-init / opensearch-init run # # automatically and exit; they are NOT long-running services) # pnpm docker:down # = docker compose down # # # Start the application stack too (web + api + worker + realtime): # # (profile "apps"; see TASK-0003 — the task spec + dev guide name it `apps`) # docker compose --profile apps up -d # # # Logs / status: # docker compose logs -f opensearch # docker compose ps # # # Reset ALL data (destroys volumes): # docker compose down -v # # ENVIRONMENT # All variables use the ${VAR:-default} form, so this file works even when no # .env exists. Defaults mirror ../../.env.example and .env.docker: # POSTGRES_USER=factory POSTGRES_PASSWORD=factory POSTGRES_DB=factory # MINIO_ROOT_USER=minioadmin MINIO_ROOT_PASSWORD=minioadmin # S3_BUCKET=factory-uploads # To override, create infrastructure/docker/.env (compose auto-loads it) or # export the variables in your shell. Copy .env.docker as a starting point. # # The app services (profile "apps") load the repo-root .env via env_file; # create it from ../../.env.example first. # ============================================================================= name: factory networks: factory-net: driver: bridge volumes: pgdata: redisdata: miniodata: opensearchdata: services: # --------------------------------------------------------------------------- # Data layer # --------------------------------------------------------------------------- postgres: image: postgres:16-alpine container_name: factory-postgres environment: POSTGRES_USER: ${POSTGRES_USER:-factory} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-factory} POSTGRES_DB: ${POSTGRES_DB:-factory} ports: - "5432:5432" volumes: - pgdata:/var/lib/postgresql/data # Extensions (citext, pg_trgm, btree_gist) are installed once on first # bootstrap of an EMPTY volume by the init script below. - ./init/01-extensions.sql:/docker-entrypoint-initdb.d/01-extensions.sql:ro healthcheck: test: [ "CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-factory} -d $${POSTGRES_DB:-factory}", ] interval: 5s timeout: 5s retries: 12 start_period: 10s networks: - factory-net restart: unless-stopped redis: image: redis:7-alpine container_name: factory-redis # Optional auth for local development. UNCOMMENT the line below AND the # healthcheck auth form together if you want a password-protected Redis: # command: ["redis-server", "--requirepass", "${REDIS_PASSWORD:-}"] ports: - "6379:6379" volumes: - redisdata:/data healthcheck: # Plain PING (no auth). If you enable requirepass above, switch to: # redis-cli -a "$REDIS_PASSWORD" ping | grep -q PONG test: ["CMD-SHELL", "redis-cli ping | grep -q PONG"] interval: 5s timeout: 3s retries: 12 start_period: 5s networks: - factory-net restart: unless-stopped minio: image: minio/minio:latest container_name: factory-minio command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin} ports: - "9000:9000" # S3 API - "9001:9001" # Web console volumes: - miniodata:/data healthcheck: test: ["CMD-SHELL", "curl -fsS http://localhost:9000/minio/health/live"] interval: 5s timeout: 5s retries: 12 start_period: 10s networks: - factory-net restart: unless-stopped # One-shot bootstrap: create the files + thumbs buckets (Phase 9 — both # PRIVATE: 'none' = no anonymous read/list/download) and make sure no # anonymous policy is attached. Exits 0 after success; restarts never. minio-init: image: minio/mc:latest container_name: factory-minio-init depends_on: minio: condition: service_healthy restart: "no" entrypoint: > /bin/sh -c " set -e; mc alias set local http://minio:9000 $${MINIO_ROOT_USER:-minioadmin} $${MINIO_ROOT_PASSWORD:-minioadmin}; mc mb --ignore-existing local/${S3_BUCKET_FILES:-factory-files}; mc mb --ignore-existing local/${S3_BUCKET_THUMBS:-factory-thumbs}; # 'none' = fully private (no anonymous read/list/download) mc anonymous set none local/${S3_BUCKET_FILES:-factory-files}; mc anonymous set none local/${S3_BUCKET_THUMBS:-factory-thumbs}; echo 'minio-init: buckets ${S3_BUCKET_FILES:-factory-files} + ${S3_BUCKET_THUMBS:-factory-thumbs} ready (private)'; " environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin} networks: - factory-net # ClamAV anti-virus daemon (Phase 9 file pipeline). The official image # runs `clamd` with the TCP stream socket on :3310 (the INIC/INOK/INOC # protocol the worker's `scanBuffer` client speaks) and downloads the # daily signature set on boot. The worker connects over TCP # (CLAMAV_HOST=clamav inside the compose network / localhost on the host). # Production deploys MUST run this (the worker refuses to boot without a # reachable scanner in NODE_ENV=production — fail closed). clamav: image: clamav/clamav:latest container_name: factory-clamav ports: - "3310:3310" networks: - factory-net restart: unless-stopped opensearch: image: opensearchproject/opensearch:2.15.0 container_name: factory-opensearch environment: - discovery.type=single-node # !!! DEVELOPMENT ONLY !!! # Security plugin is DISABLED (unauthenticated HTTP). NEVER run this # configuration in production — production deploys use managed OpenSearch # with security enabled, TLS and per-user credentials (SEARCH_USERNAME / # SEARCH_PASSWORD from .env.example). - plugins.security.disabled=true # OpenSearch >= 2.12 requires an initial admin password to bootstrap the # security plugin even when `plugins.security.disabled=true` (the demo # installer still runs on first boot of a fresh volume). Dev-only # default, matching the postgres/minio credential convention above — # never used in production (managed OpenSearch + real credentials). - OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD:-FactoryDev!2026} - OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m - bootstrap.memory_lock=true ulimits: memlock: soft: -1 hard: -1 nofile: soft: 65536 hard: 65536 ports: - "9200:9200" volumes: - opensearchdata:/usr/share/opensearch/data healthcheck: # HTTP (security disabled in dev), not HTTPS. test: [ "CMD-SHELL", "curl -sf http://localhost:9200/_cluster/health | grep -qE '\"status\":\"(green|yellow)\"'", ] interval: 10s timeout: 10s retries: 30 start_period: 40s networks: - factory-net restart: unless-stopped # One-shot bootstrap: create the index template + factory-docs index. # Reads the mapping from ./opensearch/init-indices.json (mounted read-only). opensearch-init: image: opensearchproject/opensearch:2.15.0 container_name: factory-opensearch-init depends_on: opensearch: condition: service_healthy restart: "no" volumes: - ./opensearch-init.sh:/usr/local/bin/opensearch-init.sh:ro - ./opensearch/init-indices.json:/usr/local/share/opensearch/init-indices.json:ro entrypoint: ["/usr/local/bin/opensearch-init.sh"] environment: OPENSEARCH_URL: ${SEARCH_URL:-http://opensearch:9200} networks: - factory-net # --------------------------------------------------------------------------- # Application services — started only with: docker compose --profile apps up -d # (They are NOT started by the default `docker compose up` / `pnpm docker:up`.) # All of them expect the repo-root .env (see env_file). Build contexts are the # repo root so multi-stage Dockerfiles can install the whole workspace. # --------------------------------------------------------------------------- api: image: ai-software-factory/api:local build: context: ../.. dockerfile: apps/api/Dockerfile container_name: factory-api profiles: ["apps"] env_file: - path: ../../.env required: false environment: NODE_ENV: production PORT: "4000" # Inside the compose network services are reachable by name. DATABASE_URL: postgresql://${POSTGRES_USER:-factory}:${POSTGRES_PASSWORD:-factory}@postgres:5432/${POSTGRES_DB:-factory}?schema=public REDIS_URL: redis://redis:6379 S3_ENDPOINT: http://minio:9000 SEARCH_URL: http://opensearch:9200 ports: - "4000:4000" depends_on: postgres: condition: service_healthy redis: condition: service_healthy minio: condition: service_healthy networks: - factory-net restart: unless-stopped web: image: ai-software-factory/web:local build: context: ../.. dockerfile: apps/web/Dockerfile container_name: factory-web profiles: ["apps"] env_file: - path: ../../.env required: false environment: NODE_ENV: production # NEXT_PUBLIC_* values are baked in at BUILD time by Next.js; they are set # here so the image build of a full-stack compose run sees them too. NEXT_PUBLIC_API_URL: ${API_URL:-http://localhost:4000} NEXT_PUBLIC_REALTIME_URL: ${REALTIME_URL:-ws://localhost:4001} ports: - "3000:3000" depends_on: - api networks: - factory-net restart: unless-stopped worker: image: ai-software-factory/worker:local build: context: ../.. dockerfile: apps/worker/Dockerfile container_name: factory-worker profiles: ["apps"] env_file: - path: ../../.env required: false environment: NODE_ENV: production DATABASE_URL: postgresql://${POSTGRES_USER:-factory}:${POSTGRES_PASSWORD:-factory}@postgres:5432/${POSTGRES_DB:-factory}?schema=public REDIS_URL: redis://redis:6379 S3_ENDPOINT: http://minio:9000 # Health server (TASK-0009 item 4): /health + /health/ready on 4003. HEALTH_PORT: "4003" ports: - "4003:4003" # health only — the worker has no business HTTP surface depends_on: postgres: condition: service_healthy redis: condition: service_healthy networks: - factory-net restart: unless-stopped realtime: image: ai-software-factory/realtime:local build: context: ../.. dockerfile: apps/realtime/Dockerfile container_name: factory-realtime profiles: ["apps"] env_file: - path: ../../.env required: false environment: NODE_ENV: production REALTIME_PORT: "4001" DATABASE_URL: postgresql://${POSTGRES_USER:-factory}:${POSTGRES_PASSWORD:-factory}@postgres:5432/${POSTGRES_DB:-factory}?schema=public REDIS_URL: redis://redis:6379 ports: - "4001:4001" # ws - "4002:4002" # health (/health, /health/ready, /metrics) depends_on: postgres: condition: service_healthy redis: condition: service_healthy networks: - factory-net restart: unless-stopped