
A standalone Milvus deployment is not a single container but a small coordinated stack of three: etcd for metadata, MinIO for S3-compatible object storage, and the Milvus service itself. Each container plays a distinct role in how collections, segments, indexes, and vector data are persisted and served, and misunderstanding the boundaries between them is the most common source of operational confusion on a Compose-based setup. This article walks through what each container actually does, how to wire them together with healthchecks and depends_on conditions, and when the standalone topology starts to limit throughput. It is aimed at engineers adding production-grade vector search to a Compose stack without committing to Kubernetes on day one.

A Milvus deployment described as "standalone" is, in practice, a Compose stack of three coordinated containers. Calling it standalone reflects the scale — one node, no clustering — but not the shape. Even at single-node scale, Milvus keeps its distributed architecture intact and simply runs the components on the same host. The three containers are:
quay.io/coreos/etcd, the metadata store. Milvus uses it for collection schemas, segment descriptors, index state, and the coordination metadata that every query and write path depends on.minio/minio, an S3-compatible object store. Milvus writes vector data, index files, and segment payloads to it as objects, treating it the same way it would treat AWS S3 in a clustered setup.milvusdb/milvus, the actual service that handles gRPC and REST traffic, query planning, and search execution. It reads metadata from etcd and pulls bulk data from MinIO.The reference for this layout is the standalone Compose file shipped in the milvus-io/milvus repository, which is the canonical source for the dependency graph and default configuration. Third-party guides and community examples — such as the Ramnode VPS guide and the Spheron GPU cloud guide — reproduce the same three-service structure, confirming it as the standard topology rather than an optional add-on.
Milvus is engineered as a distributed system first and a single-node deployment second. The authors deliberately decoupled storage (MinIO), metadata (etcd), and compute (Milvus) so the same binary could later scale horizontally without redesigning the storage layer. In standalone mode you still get the full architecture; you just get one instance of each role on one host.
The practical consequence is that the milvus service has hard dependencies on the other two. It points at etcd:2379 via the ETCD_ENDPOINTS environment variable and at minio:9000 via MINIO_ADDRESS, and the Compose file wires those names through Docker's internal network (Ramnode guide). If either dependency is missing, unhealthy, or unreachable on startup, Milvus fails to come up cleanly.
Understanding this three-container shape is foundational because every later operational decision follows from it:
./volumes/etcd, ./volumes/minio, ./volumes/milvus) rather than as a single data directory (Ramnode guide).depends_on conditions target each container individually, not the stack as a whole.Without that mental model first, the Compose file reads as a tangle of unrelated services rather than a deliberately partitioned system.

In a Docker Compose Milvus deployment, etcd is the metadata source of truth. Every piece of control-plane information that Milvus needs to make decisions lives there: collection and field schemas, partition definitions, the locations of sealed and growing segments, channel checkpoints used for exactly-once recovery, credential data for the built-in user-and-role system, and the cluster topology that coordinators watch to elect leaders. Even in the standalone topology, where most coordinator roles collapse into a single process, etcd remains a separate container because it is the only durable, consistent store the Milvus services rely on for distributed coordination.
The standalone Milvus Compose files use the quay.io/coreos/etcd image (commonly pinned to a v3.5.x tag) with a small but deliberate set of environment variables. The four flags that matter for operation are:
ETCD_AUTO_COMPACTION_MODE=revision — keep compaction tied to the key-space revision number rather than to elapsed time.ETCD_AUTO_COMPACTION_RETENTION=1000 — retain the last 1000 revisions and compact older history automatically.ETCD_QUOTA_BACKEND_BYTES=4294967296 — cap the database file size at 4 GiB so a runaway key count cannot fill the host disk silently.ETCD_SNAPSHOT_COUNT=50000 — trigger a snapshot every 50,000 committed transactions to keep WAL replay bounded on restart.The data directory is mounted at /etcd and persisted to a host path (commonly volumes/etcd:/etcd) so the metadata survives container restarts. The container's command line binds the client API to 0.0.0.0:2379 while advertising 127.0.0.1:2379 to peers, and the Milvus service reaches it through the Compose-internal etcd:2379 address.
A useful healthcheck exercises the same endpoint the Milvus client will use:
healthcheck:
test: ["CMD", "etcdctl", "endpoint", "health"]
interval: 30s
timeout: 20s
retries: 3
This probe calls etcdctl endpoint health against port 2379, which reports whether the local member considers itself a usable leader. The Milvus service should depends_on: { etcd: { condition: service_healthy } } so it does not race past etcd before quorum is ready — startup races here are a common cause of "Milvus connected but cannot list collections" errors after a fresh docker compose up.
Milvus is a metadata-churning system. Every flush, every sealed segment, every channel checkpoint, and every credential change increments the etcd revision. Without periodic compaction, etcd's MVCC key-space grows monotonically, which inflates memory, slows range queries, and lengthens restart replay. Switching to revision mode with a retention of 1000 keeps the working set bounded while still preserving enough history for coordinator elections and watch streams to function normally during normal operation. The 4 GiB backend quota and50,000-snapshot count act as backstops if the workload spikes beyond expectations.
MinIO still holds the raw vectors and index files, but Milvus cannot locate them without the segment-to-object mapping that lives in etcd. On startup, the coordinators re-read their state from etcd; if the data is gone or corrupted, they have no way to reconcile what exists in object storage with what the schema says should exist. Treat the etcd volume as the most critical piece of the stack to back up, and never run docker volume rm on it without also dropping the MinIO bucket it describes.

In a Compose-based standalone deployment, MinIO is the bucket. Milvus treats it as an S3-compatible object store and pushes everything that does not belong in etcd into it: raw vector data, built index files, segment logs, binlog payloads, and insert/delete buffers. When a segment is flushed, sealed, or compacted, the resulting files are written as objects into a single bucket, and Milvus keeps only the pointers and metadata for those objects in etcd. If MinIO disappears, search results can still be served from in-memory caches, but inserts, flushes, and restarts will fail — which is why depends_on: condition: service_healthy is used in the Milvus service definition.
The standard Compose service defines MinIO with credentials, a data volume, and a startup command:
minio/minio (a pinned release tag such as RELEASE.2023-03-20T20-16-18Z is common). Environment variables MINIO_ROOT_USER and MINIO_ROOT_PASSWORD (or the older MINIO_ACCESS_KEY / MINIO_SECRET_KEY aliases) default to minioadmin / minioadmin./minio_data inside the container, e.g. ./volumes/minio:/minio_data. This directory is the only persistent backing store for vectors and indexes.minio server /minio_data, optionally with --console-address ":9001" to bind the web console to a fixed port.http://localhost:9000/minio/health/live on a 30-second interval, used by Milvus to gate startup.MinIO exposes two ports: 9000 for the S3 API used internally by Milvus, and 9001 for the browser-based web console. Port 9000 must be reachable from the Milvus container; port 9001 is optional and should generally not be exposed on a public interface.
The connection is configured in milvus.yaml under the minio block. A minimal configuration looks like:
minio:
address: minio
port: 9000
useSSL: false
bucketName: milvus-bucket
rootPath: file
address resolves through the Compose network to the MinIO service. bucketName names the bucket Milvus creates on first start, and rootPath defines the prefix under which all segment and index objects are written.
MinIO has no awareness of what it is storing. The objects it holds are opaque binary blobs; Milvus fully owns the on-disk layout, naming convention, and lifecycle. Operators managing MinIO directly (browsing, pruning, replicating) should treat the contents as a black box and never edit objects in place — only Milvus can interpret them safely.

When the milvusdb/milvus image is launched with the entrypoint milvus run standalone, the container does not start a single-purpose daemon. It boots a single Linux process that bundles every role of the Milvus distributed architecture into one address space. That consolidation is what makes the Compose standalone topology viable: instead of running seven to ten cooperating services, you run one process that wears all of the hats internally (kunalganglani.com).
The bundled roles include:
19530, the endpoint that PyMilvus, the Node.js SDK, and other clients connect to (ramnode.com).dataCoord.segment.maxSize (ramnode.com).queryNode.cache.memoryLimit and indexNode.scheduler.buildParallel apply directly to these in-process workers (ramnode.com).Beyond the gRPC API, the container exposes HTTP port 9091 with a /healthz endpoint. After the stack comes up, curl http://localhost:9091/healthz returning OK is the conventional signal that every in-process role has finished registering against etcd and MinIO, and that the proxy is ready to accept traffic on 19530 (ramnode.com).
Because all of these roles share one OS process, one CPU budget, and one memory pool, configuration that would otherwise be split across many YAML files is flattened into a single mounted milvus.yaml, normally placed under /milvus/configs/ and bound to /var/lib/milvus/data, /var/lib/milvus/logs, and /var/lib/milvus/configs (medium.com). Environment variables such as ETCD_ENDPOINTS and MINIO_ADDRESS are how this single process discovers its two sibling containers.
The consolidation is purely a packaging convenience. In distributed mode those same roles are deployed as separate containers on Kubernetes, where each can be scaled and restarted independently (dev.to). Standalone is the same binary with the orchestration compressed; the boundaries between roles still exist in code, they just are not enforceable as process or container boundaries — which is the architectural reason a single-node Compose stack eventually hits throughput ceilings as collection size and QPS grow.

The Milvus container is gated on its two dependencies becoming healthy before it starts. Compose's depends_on only waits for the container to be created, so a plain declaration is not enough — the Milvus service specifies condition: service_healthy against both etcd and minio. That condition is only satisfied after each dependency's healthcheck passes, which prevents the classic race where Milvus tries to register metadata before etcd has elected a leader or before MinIO has finished initialising its data volume.
etcd exposes its client API on port 2379, and the container's healthcheck invokes etcdctl endpoint health. When that command exits successfully, etcd is treated as ready to serve metadata writes. MinIO exposes its S3-compatible API on port 9000, and its healthcheck runs curl -f http://localhost:9000/minio/health/live, which returns success as soon as the MinIO process is accepting HTTP traffic. Both checks are typically configured with an interval of around 30 seconds, a 20-second timeout, and a small number of retries, which gives the stack a reliable readiness signal without slowing down cold starts.
Once both dependencies are healthy, Compose starts the Milvus container and injects two environment variables that point it at them:
ETCD_ENDPOINTS=etcd:2379 — the etcd service name on the Compose network, port 2379.MINIO_ADDRESS=minio:9000 — the MinIO service name on the Compose network, port 9000.These names resolve through Docker's embedded DNS, so no static IP addresses or extra_hosts entries are required.
The environment variables are convenient for the standalone binary, but most production-leaning Compose files also mount a custom milvus.yaml at /milvus/configs/milvus.yaml. That file must explicitly disable the embedded etcd path by setting etcd.use.embed: false, and then declare the remote etcd endpoints (etcd.endpoints: [etcd:2379]) and the MinIO coordinates (minio.address: minio, minio.port: 9000, minio.useSSL: false). With the embedded mode off, Milvus reads these addresses on startup and refuses to proceed if it cannot reach etcd or MinIO, which surfaces configuration mistakes early in the container log rather than during the first collection creation.

A standalone Milvus stack survives docker compose down and docker compose up -d only because three host directories are bind-mounted into the containers. Without these mounts, every restart would discard metadata, vector payloads, and the configuration overrides the standalone process reads at boot.
The canonical Compose layout creates a volumes/ tree under the deployment root (commonly /opt/milvus) with one subdirectory per service (Ramnode guide):
volumes/etcd mounted at /etcd inside the etcd container — holds the WAL and snapshots that record collection schemas, segment metadata, channel checkpoints, and credential tokens.volumes/minio mounted at /minio_data — holds the raw S3 objects Milvus writes: binlog files, index files, segment payloads, and the growing/sealed segments flushed from the query node.volumes/milvus mounted at /var/lib/milvus — holds Milvus-internal logs and the milvus.yaml override, plus any local caches the standalone process keeps on disk.volumes/etcd and volumes/minio are independently meaningful and can be snapshotted separately — etcdctl snapshot save for the catalog, and a recursive copy of the MinIO data directory for the bytes. Neither is sufficient on its own to reconstruct a working Milvus instance, however. etcd contains only the catalog (collection definitions, segment IDs, channel states), while MinIO contains only the raw data. Pointing a fresh Milvus container at one without the other will either fail to start or report schema/metadata errors on the first query. The two must be captured within a tight window of each other — ideally with Milvus quiesced or stopped — and restored together. volumes/milvus is largely cache and configuration and is rarely needed for disaster recovery.
The stack surfaces four ports to the host (Ramnode guide, Spheron blog):
/healthz endpoint, returns OK when the standalone service is ready.Only 19530 (and optionally 9091 for external liveness probes) normally needs to be reachable from outside the host. Port 9000 should stay on the internal Docker network — MinIO ships with default credentials (minioadmin/minioadmin) and exposes bucket-level read/write to anyone who can reach it. Exposing 9000 publicly is a meaningful security risk; remote access is better handled through SSH tunneling or a reverse proxy in front of Milvus itself (Ramnode guide).

The three-container Compose stack is convenient, but every layer of it is a single instance. The bundled Milvus process runs one query node, one index node, one data node, and the four coordinators in the same container, so a single CPU or memory ceiling caps query throughput and there is no horizontal scale-out path short of vertically resizing the host (kunalganglani.com). etcd and MinIO run as single pods too, which means an etcd quorum loss or a MinIO disk failure takes the entire vector layer offline. Any upgrade, restart, or host migration requires a downtime window because there is nowhere to drain queries.
Milvus publishes a separate docker-compose.yml for the cluster topology that splits the monolithic process into roughly eight role-specific services, plus the shared etcd and MinIO (kunalganglani.com). The coordinator layer fans out into rootcoord, datacoord, querycoord, and indexcoord; the worker layer fans out into proxy, querynode, indexnode, and datanode. From Milvus 2.6 onward, a woodpecker service replaces the Kafka/Pulsar message queue that earlier versions required, acting as the write-ahead log backed by object storage (dev.to/krunalkanojiya).
Scaling is done by raising the replica count of querynode and indexnode; everything else stays at one replica because coordinators are stateful and cannot be scaled horizontally without re-architecting. This stack still runs on a single Docker host, which is a useful way to validate the topology and rehearse upgrades before committing to a Kubernetes migration through the Milvus Operator (ramnode.com).
Two collection-level knobs drive most of the throughput headroom. The community rule of thumb is roughly one shard per 50 million vectors, paired with a replica count of two or more so a query node can be restarted without dropping the load (spheron.network). Configure both at creation time:
shards_num — set during collection creation to partition the index across query nodes.replica_number — set when calling collection.load(...) to duplicate each shard across query nodes.A team should plan the move off standalone once any of the following signals appear:
When two or more of these signals fire together, cluster mode on Compose is the appropriate next step, with a Kubernetes migration scheduled once the team is ready to absorb the operational surface of 7–10 microservices.