I tried to cut our twelve-container stack down to four. Two of my three conclusions were wrong.
SOIT Team

Self-hosted projects lose most of their prospective users at the first command in the README, and ours names twelve services on a single line. Issue #25 asked the obvious question: I just want to look at it, do I really need all of them? The answer is no. Seven of the twelve are enough, and because three of those seven are one-shot jobs that exit when they finish, only four containers stay resident: postgres, minio, api and web. The more useful part of this exercise, though, is that the trimmed stack was actually run rather than reasoned about. Two of the three conclusions drawn from reading the code turned out to be wrong, and both of the wrong ones were exactly the kind that would have left a first-time reader staring at a dead URL.
The fastest way to find out whether a dependency is real is not the deployment guide but the health check, because a guide documents intent while a health check documents behaviour. In the readiness endpoint, three backends are treated differently: if the database is unreachable the endpoint raises a 503, if object storage is unreachable it raises a 503, and if the vector store is unreachable the field simply reads unavailable and the endpoint still returns 200. That asymmetry names the hard requirements outright: a reachable Postgres and a writable storage root. Everything else is negotiable in principle, and the rest of this walk is about which of those negotiations survive contact with the published image.
Storage root is not the same word as MinIO, and the storage adapter does resolve to a local file URI when nothing else is configured, so the first conclusion was that the object store could go. It cannot. Pointing storage at a local directory brought the API up returning object storage is unavailable, and constructing the adapter by hand inside the container gave the real error: permission denied on a path nobody had asked for. The cause is a normalisation helper that strips slashes from both ends of the root path, which turns an absolute path into a relative one; the relative path is then resolved against the image working directory, which the Dockerfile copies in as root while the process runs as an unprivileged user. Mounting a volume does not rescue it either, since Docker creates the mount point owned by root as well. So MinIO stays, which is cheap enough at a couple of hundred megabytes, and the underlying bug is filed as issue #43.
The vector group leaves as a unit, and it is worth being precise about what that costs. Milvus depends on etcd for metadata and on MinIO for data, so those two names travel together. The API still starts without them because the vector connection is established lazily on first use rather than during dependency injection. What it does not do is degrade into empty results: the in-memory vector implementation is reserved for test runs and, unlike the secrets port, it never consults the environment setting. The platform therefore boots, non-vector features work, readiness honestly reports the vector store as unavailable while still calling itself ready, and knowledge retrieval raises the moment you use it. That conclusion held up in the run.
The second wrong conclusion is the one no amount of reading would have produced. That honest readiness response took thirty-four seconds to come back, because the vector probe has to resolve a hostname that no longer exists and nothing bounds how long it may wait. The probe was written to fail soft; it was not written to fail fast. Compose, meanwhile, health-checks the API with a three-second request and a five-second allowance, so the check cannot pass, and the API container sits permanently marked unhealthy even though the service behind it is fine and accepts logins. The web container declares that it depends on the API being healthy, so a normal bring-up means the UI never starts at all. The fix is one flag, starting web with dependencies skipped, and the missing probe timeout is filed as issue #44.
Three services genuinely are optional, each for a different reason. Vault can stay down: leaving its URL and token empty swaps in an in-process secret store, which the wiring permits in development environments, at the price named in its own class, that secrets do not survive a restart. Redis is not one question but three, and they have different answers: the event bus can be switched back to its in-memory default, which delivers within a single process only, the permission cache degrades gracefully because an unreachable Redis reads as a cache miss and the caller re-checks the database, and the rate limiter has no in-memory equivalent at all but is only ever called when a per-tool limit or a daily quota is configured. Dropping Redis from a demo is therefore safe as long as you do not configure those, which is the one item on this list that depends on what you plan to demonstrate.
One service is not removed but folded in. The outbox dispatcher runs the transactional outbox, and its setting controls where that work happens rather than whether it happens at all: compose disables the in-process path and runs the same logic in a dedicated container, and a single environment variable inverts that for a demo. Production goes the other way and enforces it in code rather than advising it in documentation, because dispatching and request handling in one process compete for the same resources and a restart interrupts both at once. The knowledge ingest worker, finally, has one job and no reason to exist if your demo never uploads a document; while we are here, the heavy machine-learning dependencies people assume live in that image are actually in a separate optional group.
None of this is a supported deployment shape, and it is worth saying why the default asks for twelve. Every service cut here maps to something production enforces at startup: the runtime validation refuses to boot without the Redis event bus, the dedicated dispatcher process, a real secret manager, telemetry, and plugin signature verification. The default topology targets the production shape, not the demo shape, and the distance between the two turns out to be measurable in a handful of environment variables, which happens to be the fastest way to understand the architecture. Keep the other lesson too: reading the code gives you hypotheses, running it gives you conclusions. The repository is at github.com/soit-ai/soit, the full quickstart is in docs/quickstart.md, and if you get stuck on a step of the trimmed version the issue tracker is the right place to say so.