# Container shape for the Mode A site (docs/phase-2-static-site.md §2.1). # # Values are fixed by the specification and must not drift: # mem_limit == memswap_limit == 120m so the app cannot eat the host's swap # ports published on 127.0.0.1 only Caddy on .35 is the only public listener # logging capped one bad day cannot fill the disk # # Run it (build the artefacts first — Phase 3's deploy.sh does both): # # make build dist # docker compose -f deploy/compose.yml up -d # # `curl http://127.0.0.1:8096/healthz` must answer with static ready. name: ai-resume services: resume: image: ai-resume:local build: context: .. dockerfile: deploy/Dockerfile restart: unless-stopped mem_limit: 120m memswap_limit: 120m # The app listens on 0.0.0.0 inside the container namespace; the host-side # binding is loopback, so nothing is reachable from the LAN except through # docker-proxy on 127.0.0.1:8096 — which is what Caddy proxies to. ports: - "127.0.0.1:8096:8096" read_only: true security_opt: - no-new-privileges:true logging: driver: json-file options: max-size: 10m max-file: "3" healthcheck: test: ["CMD", "/usr/bin/resume-server", "--health-check"] interval: 30s timeout: 3s retries: 3 start_period: 5s # The project network gets an explicit, small subnet. The host's docker daemon # has every default address pool allocated by other projects ("all predefined # address pools have been fully subnetted"), so an implicit compose network # cannot be created. Pinning /28 of RFC1918 space that no other bridge or route # on .13 uses keeps this deploy independent of the host's pool exhaustion and # does not touch any other container. Single-service stack: two addresses is # plenty. networks: default: ipam: config: - subnet: 10.89.0.0/28 gateway: 10.89.0.1 # Phase 4/5 add at most three more containers (Qdrant, Prefect worker, query # service) inside the 4-container and 1.0 GiB budget in AGENTS.md §4.3. Mode A # needs none of them and must keep working when they are stopped (test A1).