infra/docker-compose.product.yml from a developer’s point of view — what to run, what each service does, how readiness works. This page covers the same stack from an operations point of view: it is the only environment where you can rehearse a backup/restore cycle, inspect object-store versioning, or reproduce a production incident without touching a cloud account. Read both; this one assumes you already know the service list and readiness gate from the onboarding page.
Why this stack, not a shared cloud sandbox
docs/CLOUD_DEPLOYMENT.md states the goal directly: the same two container images (this repo’s Dockerfile and ../frontend/Dockerfile) run locally, on AWS, and on Azure. docker-compose.product.yml builds those exact images rather than approximating them, so a behavior you reproduce locally — a readiness failure, a worker retry, a migration ordering bug — is the same behavior a cloud deployment would show, not an artifact of a simplified dev harness. This is why the compose file is called out as an operations tool and not just a getting-started convenience: it is the cheapest, fastest place to rehearse a production failure mode.
Service-to-cloud-equivalent mapping
This is the same mapping documented indocs/LOCAL_SETUP.md and docs/CLOUD_DEPLOYMENT.md, restated here as the reference table for operations work:
Every override for the ports above is an environment variable:
CAUSELOOP_FRONTEND_PORT, CAUSELOOP_API_PORT, CAUSELOOP_POSTGRES_PORT, CAUSELOOP_REDIS_PORT, CAUSELOOP_MINIO_PORT, CAUSELOOP_MINIO_CONSOLE_PORT, CAUSELOOP_MAILPIT_SMTP_PORT, CAUSELOOP_MAILPIT_PORT. These ports are intentionally different from the historical research stack’s ports (infra/docker-compose.yml) so both can run on one machine at once.
Why MinIO and Mailpit are local-only emulators, not deployment components
Two of the eleven services in the compose file have no cloud counterpart in the deployment sense — they exist purely so a developer can observe behavior that, in the cloud, happens inside a managed provider you can’t easily inspect:- MinIO is an S3-API-compatible object store. Local code talks to it through
CAUSELOOP_OBJECT_PROVIDER: s3andCAUSELOOP_OBJECT_ENDPOINT: http://minio:9000— the same S3 SDK calls a real AWS deployment makes, just pointed at a local endpoint. AWS uses native S3 directly; Azure uses Blob Storage directly. Neither cloud template runs MinIO or anything like it. The only reason MinIO exists is so you can open its console at127.0.0.1:19001(credentialscauseloop/causeloop-dev, local-only) and see object versions, retention, and the effect of theminio-initbootstrap job with your own eyes. - Mailpit captures outbound SMTP locally instead of sending it.
docs/LOCAL_SETUP.mdis explicit that Mailpit is an ephemeral provider sink, not application state: the durable record of an invitation or password-reset is the encryptedcontrol.platform_jobsrow the API commits atomically with the auth-state change, not the message sitting in Mailpit’s UI. Production rejects thelocal-smtpprovider outright and never exposes a mailbox UI or logs a token — Mailpit’s entire reason for existing is to let you read a message you can never read in a real environment.
user: "65532:65532", read_only: true, security_opt: no-new-privileges:true) with a tmpfs mount for its own database file. MinIO runs with the image’s own defaults — no user, read_only, or security_opt override in the compose file.
Loopback-only port publishing
Every portdocker-compose.product.yml publishes is bound to 127.0.0.1, not 0.0.0.0:
0.0.0.0 publishing would — the same “API is never directly reachable” invariant holds locally and in the cloud, just enforced by loopback binding instead of a VPC/VNet security group.
Volume lifecycle
The stack’s state lives in three named (not bind-mounted) Docker volumes:down removes containers and the network but preserves the three named volumes. Every tenant, dataset, checkpoint, and uploaded object you created survives a restart. Only down -v deletes them, and there is no confirmation prompt — treat -v with the same caution you’d apply to dropping a cloud RDS instance or emptying a versioned S3 bucket.
Backup/restore rehearsal
scripts/rehearse_product_backup.sh is the local stand-in for the AWS and Azure quarterly restore drills described in docs/CLOUD_OPERATIONS.md — it’s the same underlying discipline (dump, restore into an isolated target, verify, clean up) at a scale you can run before every release instead of once a quarter:
- Runs
pg_dump -U causeloop -d causeloop -Fcinside the runningpostgrescontainer, writing a custom-format dump to a scratch path. - Creates a throwaway database named
causeloop_restore_rehearsal_<pid>and restores the dump into it withpg_restore— never touching the livecauseloopdatabase. - Asserts
control.tenantsresolves inside the restored copy (to_regclass('control.tenants') IS NOT NULL), proving the control-plane schema came back intact. - Separately confirms MinIO’s
causeloop-uploadsbucket still reports object versioning enabled (mc version info). - Drops the scratch database and deletes the dump file on exit via a trap — this cleanup runs even if an earlier step fails.
Rehearsing the incident recovery order locally
docs/CLOUD_OPERATIONS.md documents a strict startup order for an incident recovery: PostgreSQL first (it’s the metadata/business authority), then the referenced object versions, then the provisioner, then the product worker, then the API, then the frontend — with readiness required and any queued/running jobs reconciled before traffic reopens. The local stack’s own depends_on: condition: graph is a runnable version of exactly that order:
You can rehearse a scale-to-zero-and-recover drill locally with:
postgres, redis, and minio keep their volumes and stay up throughout (the data plane is never what you’re stopping in this drill); restarting the application-tier services exercises the same migrate → workers → api → frontend ordering a real incident recovery follows, against the same GET /health/ready gate described in Local product stack. If this doesn’t come back healthy locally, don’t assume the equivalent AWS/Azure restart order will either.
Env var parity, not just port parity
Beyond the port table above, the environment variable names the stack uses are the same names used in production, only pointed at local endpoints —CAUSELOOP_OBJECT_PROVIDER: s3, CAUSELOOP_OBJECT_BUCKET: causeloop-uploads, CAUSELOOP_OBJECT_ENDPOINT: http://minio:9000, and the standard AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_DEFAULT_REGION triple (set to the MinIO root credentials and a placeholder region locally). A real AWS deployment sets the same CAUSELOOP_OBJECT_PROVIDER: s3 and lets AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY come from the task’s IAM role instead of literal container environment values, with CAUSELOOP_OBJECT_ENDPOINT unset so the AWS SDK’s default S3 endpoint resolution applies. This is why the local stack is described as running “the same code,” not “equivalent code” — the object-store client in services/storage/object_store.py doesn’t know or care whether it’s talking to MinIO or S3; only the endpoint and credential source differ.