# ============================================================================= # ai-compose.yml -> deployed to ~/ai-compose.yml on yau-sls-poc-lin001 # # House style, inherited from ~/docker-compose.yml (host brief section 7): # - restart: unless-stopped on everything # - log rotation 10 MB x 3 on everything # - NO published host ports: reach services through Caddy on the proxy network # - secrets in 0600 env files under ~/ai/, never here and never in Git # # Orphan-container warnings are expected (shared Compose project name) - ignore. # # docker compose -f ~/ai-compose.yml up -d # ============================================================================= services: # --------------------------------------------------------------------------- # pg-ai - pgvector, reference data, Cube pre-aggregations. # Deliberately NOT on proxy: no UI, nothing outside the AI stack reaches it. # Pinned image - do NOT add to Watchtower's update list. # --------------------------------------------------------------------------- pg-ai: image: pgvector/pgvector:pg16 container_name: pg-ai restart: unless-stopped networks: [ai-internal] env_file: - /home/azureuser/ai/pg-ai.env # 0600, not in Git environment: POSTGRES_DB: plant POSTGRES_USER: postgres PGDATA: /var/lib/postgresql/data/pgdata volumes: - /datadisk/pg-ai:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres -d plant"] interval: 10s timeout: 5s retries: 5 logging: driver: json-file options: { max-size: "10m", max-file: "3" } # --------------------------------------------------------------------------- # cubestore - Cube's own store. Queue, cache and pre-aggregations. Deployed # because Cube v1 does not run without it, not because we wanted another # container. Version pinned in lockstep with cube: a mismatched pair is a # documented Cube failure mode. Do NOT add either to Watchtower's list. # # Data goes on /datadisk. It grows with the pre-aggregations, and / is 62 GB # and has hit 100% on this host before. # sudo install -d -o 1000 -g 1000 /datadisk/cubestore # No ports, ai-internal only: nothing outside the AI stack talks to it, and # it has no authentication of its own. # --------------------------------------------------------------------------- cubestore: image: cubejs/cubestore:v1.1.7 container_name: cubestore restart: unless-stopped networks: [ai-internal] environment: CUBESTORE_DATA_DIR: /cube/data volumes: - /datadisk/cubestore:/cube/data logging: driver: json-file options: { max-size: "10m", max-file: "3" } # --------------------------------------------------------------------------- # cube - semantic layer. Reads imh over TDS/1433 with the read-only login, or # the fixture tables in pg-ai while USE_FIXTURES=true. Pinned - not in # Watchtower's list. # # PRE-AGGREGATIONS LIVE IN CUBE STORE, NOT pg-ai. This is a deviation from # the build spec, forced by the pinned version and found on lin001 deploying # Phase 5, not in review. Cube v1 will not materialise pre-aggregations into # Postgres - Cube Store is the only supported external store - so naming # Postgres as CUBEJS_EXT_DB_TYPE fails every query with # "It`s not possible to use Cube Store as queue/cache driver without using # it as external" # Cube Store is also the queue and cache driver, and it is not optional: # CUBEJS_CACHE_AND_QUEUE_DRIVER=memory does not fall back, it hangs /readyz # and every query forever, with nothing in the log at level warn. # So `cubestore` below is a hard dependency of cube, not an optimisation. # cube_preagg in pg-ai and its grants in db/003_roles.sql are now unused; # they are left in place rather than dropped, because nothing else changes if # a future Cube version restores Postgres as an external store. # --------------------------------------------------------------------------- cube: image: cubejs/cube:v1.1.7 container_name: cube restart: unless-stopped depends_on: pg-ai: condition: service_healthy cubestore: condition: service_started networks: [ai-internal, proxy] env_file: - /home/azureuser/ai/api.env # 0600, not in Git environment: CUBEJS_DEV_MODE: "false" CUBEJS_LOG_LEVEL: warn # Queue, cache and pre-aggregation store. Mandatory - see the note above. CUBEJS_CUBESTORE_HOST: cubestore CUBEJS_CUBESTORE_PORT: "3030" # Without this NOTHING builds the pre-aggregations, and every query that # matches one fails with "No pre-aggregation partitions were built yet" # rather than falling back to the source. Single node, so the API # instance is also the refresh worker. CUBEJS_REFRESH_WORKER: "true" # CUBEJS_DB_* and CUBEJS_API_SECRET come from api.env. volumes: - /home/azureuser/ai/cube/model:/cube/conf/model:ro healthcheck: # The image has neither wget nor curl - the original wget healthcheck # marked a perfectly healthy cube unhealthy on every deploy. node is # what it does have. test: ["CMD", "node", "-e", "require('http').get('http://localhost:4000/readyz', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"] interval: 30s timeout: 5s retries: 3 logging: driver: json-file options: { max-size: "10m", max-file: "3" } # --------------------------------------------------------------------------- # ai-api - FastAPI. Classifier, agent, contracts, guardrails. # --------------------------------------------------------------------------- ai-api: build: context: /home/azureuser/ai/api dockerfile: Dockerfile image: yau/ai-api:local container_name: ai-api restart: unless-stopped depends_on: pg-ai: condition: service_healthy networks: [ai-internal, proxy] env_file: - /home/azureuser/ai/api.env # 0600, not in Git # Phase 9 - the upload inbox, and the ONLY writable path this container # gets. Deliberately not /datadisk/ai-docs: a file that has been uploaded # but not yet approved must not be visible to `ai-ingest --all`. # # COMMENTED OUT UNTIL PHASE 9, on the same principle as caddy/ai-routes.caddy: # add each piece at the phase that needs it. Docker creates a missing bind # source as a ROOT-OWNED directory, and ai-api does not run as root - so # deploying this before the directory exists gives you an inbox the API # cannot write to, on the growing disk of a live shared host. # Create it first, then uncomment: # sudo install -d -o 10002 -g 10002 /datadisk/ai-docs-inbox # volumes: # - /datadisk/ai-docs-inbox:/inbox healthcheck: test: ["CMD", "python", "-m", "app_healthcheck"] interval: 30s timeout: 5s retries: 3 logging: driver: json-file options: { max-size: "10m", max-file: "3" } # --------------------------------------------------------------------------- # ai-web - React/Vite build served by nginx. proxy only; the browser talks to # the API through its public hostname, so it needs nothing on ai-internal. # --------------------------------------------------------------------------- ai-web: build: context: /home/azureuser/ai/web dockerfile: Dockerfile args: # Empty in a real deployment: the page is same-origin, because Caddy # routes /ask under ai.yokogawa.tech to ai-api. Set only for a # tunnelled demo build, which has no Caddy and no Authelia in front # of it. VITE_API_BASE: "${VITE_API_BASE:-}" image: yau/ai-web:local container_name: ai-web restart: unless-stopped networks: [proxy] logging: driver: json-file options: { max-size: "10m", max-file: "3" } # --------------------------------------------------------------------------- # ai-ingest - on demand, not a service. Docling -> chunk -> embed -> pg-ai. # docker compose -f ~/ai-compose.yml run --rm ai-ingest --all # The profile keeps it out of `up -d`. # --------------------------------------------------------------------------- ai-ingest: build: context: /home/azureuser/ai/ingest dockerfile: Dockerfile image: yau/ai-ingest:local container_name: ai-ingest profiles: [ingest] restart: "no" networks: [ai-internal] env_file: - /home/azureuser/ai/api.env # 0600, not in Git volumes: - /datadisk/ai-docs:/docs:ro logging: driver: json-file options: { max-size: "10m", max-file: "3" } # --------------------------------------------------------------------------- # ai-docs-worker - Phase 9. The ai-ingest IMAGE with worker.py as entrypoint, # so an uploaded document is parsed and chunked by exactly the same code as a # file ingested from the command line - by construction, not by discipline. # # Two jobs: pre-scan `uploaded` rows for a header proposal, and publish # `approved` ones. It is the only container that can write doc_chunks # (ingest_rw), and it has no HTTP surface and no place on the proxy network. # # /docs is READ-WRITE here, unlike the ai-ingest CLI service above, because # publishing moves the approved file into the folder that determines its # doc_type. That is the one write, and it happens only after a human has # confirmed the header. # # Both mounts must be writable by the image's uid 10002 (ingestuser): # sudo install -d -o 10002 -g 10002 /datadisk/ai-docs-inbox # sudo chown -R 10002:10002 /datadisk/ai-docs # ai-api writes the inbox as its own non-root uid - give the inbox group # write and put both uids in the group rather than making it world-writable. # --------------------------------------------------------------------------- ai-docs-worker: build: context: /home/azureuser/ai/ingest dockerfile: Dockerfile image: yau/ai-ingest:local container_name: ai-docs-worker # Kept out of `up -d` by the profile, exactly as ai-ingest is. worker.py # does not exist yet, so an unguarded service here would crash-loop on a # live shared host. Drop the profile in the same commit that adds the file. # docker compose -f ~/ai-compose.yml --profile worker up -d ai-docs-worker profiles: [worker] restart: unless-stopped depends_on: pg-ai: condition: service_healthy networks: [ai-internal] env_file: - /home/azureuser/ai/api.env # 0600, not in Git entrypoint: ["python", "worker.py"] command: [] volumes: - /datadisk/ai-docs-inbox:/inbox - /datadisk/ai-docs:/docs # rw - see above # Withdrawn documents are MOVED here, not deleted. It is outside the four # doc_type folders on purpose: ingest_file() inserts every chunk with # superseded = FALSE, so a withdrawn file left in /docs comes back LIVE on # the next `ai-ingest --all`. - /datadisk/ai-docs-withdrawn:/withdrawn logging: driver: json-file options: { max-size: "10m", max-file: "3" } networks: ai-internal: driver: bridge proxy: external: true