What the Compose file starts
| Service | Container | Role | Host ports as shipped |
|---|---|---|---|
| milvus-volume-init | unnamed | Runs once as root, hands /var/lib/milvus to the image's milvus user, leaves a marker file | none, no network |
| etcd | milvus-etcd | Metadata; auto-compaction, 4 GiB backend quota | none |
| minio | milvus-minio | Object storage for Milvus data, default keys minioadmin | 9000 API, 9001 console |
| standalone | milvus-standalone | Milvus; /healthz checked every 30 s after a 90 s start period | 19530 gRPC and REST, 9091 health, metrics and WebUI |
Data goes to volumes/etcd, volumes/minio and volumes/milvus beside the Compose file, unless DOCKER_VOLUME_DIRECTORY is set. There is no restart policy, so a reboot leaves everything down. A mapping with no address publishes on 0.0.0.0 and [::], and Docker-published traffic bypasses ufw, so all four ports, MinIO with its default keys included, would face the internet.
Hardware the prerequisites page sets
| Component | Required | Recommended |
|---|---|---|
| CPU | Intel 2nd Gen Core or newer, or Apple Silicon | 4 cores or more for Standalone |
| Instruction sets | At least one of SSE4.2, AVX, AVX2, AVX-512 | Same list |
| RAM | 8 GB for Standalone | 16 GB for Standalone |
| Disk | SATA 3.0 SSD or better | NVMe SSD or better |
| Linux software | Docker 19.03 or later with Compose plugin V2 | Docker's own packages |
Disk latency is critical to etcd, since slow responses trigger frequent elections that degrade it, so it recommends local NVMe and a fio result above 500 sustained IOPS with 99th-percentile fsync under 10 ms. On a delivered machine, grep -o -w -E 'sse4_2|avx|avx2|avx512f' /proc/cpuinfo | sort -u should name at least one extension.
Sizing memory to the collection
Loading a collection, needed before any search, puts its index files and raw field data in memory, so RAM follows what you keep loaded. Raw float32 vectors cost count × dimensions × 4 bytes, so ten million at 768 dimensions is about 30.7 GB before indexes (our arithmetic). The milvus.io sizing tool estimates the rest; test its figures with real data.
Lines that fit
On a dedicated server Milvus gets all cores, RAM and local disks, with root. Classes that fit follow.
- Small to mid collections, the Xeon D-1587 cartridge line (16 cores, 16 to 128 GB, one 1.92 TB NVMe-SSD) or the Xeon E3-1585L v5 (4 cores, 16 to 64 GB, NVMe-SSD 240 GB); see NVMe dedicated servers.
- Large loaded collections, the dual-socket Xeon Gold 6130 and E5-2630L v3 lines (to 1 TB of memory) or the four-socket Gold 6230 (to 2 TB). They ship with SATA-SSDs; for NVMe, choose drives in a custom build, built in 4 to 24 hours.
- Leave the X-Gene ARM cartridge aside, since it boots only Ubuntu 16.04 and 18.04. The prerequisites name only Intel chips and Apple Silicon, so if you want ARM, trial the Ampere Altra line first (our caution).
A racked server takes about 30 minutes once payment is approved. Racked single-, dual- and quad-socket servers have an out-of-band console (HPE iLO, Dell iDRAC or a Supermicro BMC); of the cartridges only the m510 and m710x do, the rest offer power control only.
Install with ports on loopback and authentication on
This assumes Ubuntu 24.04 with Docker's own docker-ce packages. The prerequisites ask for Docker 19.03 or later; we ask for Engine 28 or newer, because from 28 a 127.0.0.1 mapping is reachable only from the host, and the block checks it first. Run it as root after sudo -i. ufw allow OpenSSH opens port 22 only; if sshd listens elsewhere, allow that port before enabling.
apt-get update && apt-get install -y ufw curl openssl
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
ufw allow OpenSSH && ufw --force enable
V=v3.0.5; D=/opt/milvus; f=$D/docker-compose.yml; y=$D/milvus.yaml
safe() { [ "$(grep -c '^ authorizationEnabled: true$' $y)" = 1 ] &&
[ "$(grep -c '"127.0.0.1:' $c)" = 4 ] && ! grep -Eq '^ +- "[0-9]+:' $c &&
[ "$(grep -c 'restart: unless-stopped' $c)" = 3 ] &&
grep -qx ' - ./milvus.yaml:/milvus/configs/milvus.yaml' $c &&
docker compose -f $c config -q; }
v=$(docker version --format '{{.Server.Version}}') && [ "${v%%.*}" -ge 28 ] &&
install -d -m 750 $D && if [ -e $f ]; then c=$f; safe && echo "already set up, checks pass"
else c=$f.new; curl -fsSL https://github.com/milvus-io/milvus/releases/download/$V/milvus-standalone-docker-compose.yaml -o $c &&
curl -fsSL https://raw.githubusercontent.com/milvus-io/milvus/$V/configs/milvus.yaml -o $y &&
sed -i 's/^ authorizationEnabled: false$/ authorizationEnabled: true/' $y &&
sed -i -E -e 's/- "(19530|9091|9000|9001):/- "127.0.0.1:\1:/' \
-e '/^ container_name: milvus-/a\ restart: unless-stopped' \
-e '/^ standalone:/,/^ healthcheck:/ s#^ - .*/volumes/milvus:/var/lib/milvus$#&\n - ./milvus.yaml:/milvus/configs/milvus.yaml#' $c &&
safe && mv $c $f && echo "prepared $V"
fi || echo "STOP - Engine older than 28, a download broke, or a check did not pass"
The Compose file is replaced only after every check passes; a rerun just re-checks it, printing already set up, checks pass or STOP (before any data exists, delete /opt/milvus/docker-compose.yml and repeat). Our changes are these; the rest is as shipped.
milvus.yamlmounted at/milvus/configs/milvus.yamlinstandalone, withcommon.security.authorizationEnabledset totrue.- The four published ports (19530, 9091, 9000, 9001) prefixed with
127.0.0.1. - An unless-stopped restart policy on
milvus-etcd,milvus-minioandmilvus-standalone; the init service keeps its no-restart setting.
Replacing the root password and connecting clients
Once authentication is on, Milvus creates root with the password Milvus. This block writes /opt/milvus/login-check.sh, starts the stack, waits until healthy, saves a new password in a mode 600 file, sets it through /v2/vectordb/users/update_password and tests it. curl reads the password on stdin, so ps never shows it.
D=/opt/milvus; f=$D/docker-compose.yml; pw=$D/root-password; lc=$D/login-check.sh
install -m 700 /dev/null $lc && cat > $lc <<'EOF'
p=${1:-$(cat /opt/milvus/root-password)}
printf 'Authorization: Bearer root:%s\n' "$p" | curl -fsS http://127.0.0.1:19530/v2/vectordb/collections/list \
-H @- -H 'Content-Type: application/json' -d '{}' | grep -Eq '"code": ?0[,}]' &&
echo "login works" || { echo "STOP - Milvus did not accept that root password, or is not answering on 127.0.0.1:19530"; exit 1; }
EOF
if [ -e $pw ]; then echo "STOP - $pw exists, root already has its own password"
else docker compose -f $f up -d &&
timeout 600 bash -c 'until [ "$(docker inspect -f "{{.State.Health.Status}}" milvus-standalone)" = healthy ]; do sleep 10; done' &&
p="Mv-$(openssl rand -hex 16)" && [ ${#p} -eq 35 ] &&
install -m 600 /dev/null $pw && printf '%s\n' "$p" > $pw &&
r=$(printf '{"userName":"root","password":"Milvus","newPassword":"%s"}' "$p" |
curl -fsS http://127.0.0.1:19530/v2/vectordb/users/update_password \
-H 'Authorization: Bearer root:Milvus' -H 'Content-Type: application/json' --data @-) &&
grep -Eq '"code": ?0[,}]' <<<"$r" && bash $lc &&
echo "root password changed and stored in $pw" ||
echo "STOP - Compose failed to start, Milvus was not healthy after 10 minutes, or the change or its check failed; see below"
fi
If it stops after root-password was written, run bash /opt/milvus/login-check.sh Milvus. If that prints login works, the change never happened, so remove root-password and run the block again. If it prints STOP while docker compose -f /opt/milvus/docker-compose.yml ps shows Milvus healthy, the change went through and root-password holds the new password; bash /opt/milvus/login-check.sh should then print login works. Local clients use http://127.0.0.1:19530 with the token user:password; your own containers can join the milvus Docker network and use standalone:19530. Give applications their own users and roles, not root.
For the WebUI, which asks for no password here, tunnel through your own login with ssh -N -L 19530:127.0.0.1:19530 -L 9091:127.0.0.1:9091 you@your-server and browse http://127.0.0.1:9091/webui/. The Attu desktop app works through the same tunnel. MinIO keeps minioadmin, reachable only from a shell on the box; if others get logins, change its keys in the Compose file and in milvus.yaml together, which that file's comments say must match. Remote clients use the same SSH tunnel, also over a WireGuard link if you run one; 19530 stays on 127.0.0.1, never on 0.0.0.0 or the public IP.
Backups, restore and rollback
The upgrade guide requires a current backup but gives no commands. Ours is a cold copy of all of /opt/milvus, kept only if the archive lists every file and volume folder; Milvus is down while tar runs. Seven days are kept, pruned only after a verified archive. Run it as root.
install -d -m 700 /var/backups/milvus
cat > /usr/local/sbin/milvus-backup.sh <<'EOF'
set -uo pipefail
umask 077
B=/var/backups/milvus; f=/opt/milvus/docker-compose.yml
a="$B/milvus-$(date +%F-%H%M%S).tar.gz"
find "$B" \( -name 'log-*.txt' -mtime +30 -o -name '*.part' -mtime +1 \) -delete
trap 'docker compose -f "$f" start >/dev/null 2>&1' EXIT
docker compose -f "$f" stop || exit 1
tar -czf "$a.part" -C /opt milvus; rc=$?
docker compose -f "$f" start
[ "$rc" -eq 0 ] || { echo "backup FAILED, tar status $rc"; rm -f "$a.part"; exit 1; }
l=$(tar -tzf "$a.part") || { echo "backup FAILED, archive unreadable"; exit 1; }
for m in docker-compose.yml milvus.yaml root-password login-check.sh volumes/etcd/ volumes/minio/ volumes/milvus/; do
grep -qxF "milvus/$m" <<<"$l" || { echo "backup FAILED, milvus/$m missing"; rm -f "$a.part"; exit 1; }
done
mv "$a.part" "$a" && find "$B" -name 'milvus-*.tar.gz' -mtime +6 -delete && echo "ok $a"
EOF
printf '%s\n' '15 3 * * * root bash /usr/local/sbin/milvus-backup.sh > /var/backups/milvus/log-$(date +\%F).txt 2>&1' \
> /etc/cron.d/milvus-backup
bash /usr/local/sbin/milvus-backup.sh
Copy /var/backups/milvus off the machine with your own rsync -a job, never --delete. Restores run as root.
- On a rebuilt machine, install Docker as above.
ufw allow OpenSSHopens port 22 only, so if sshd listens elsewhere, allow that port first. Thenapt-get update && apt-get install -y ufw curl openssl,sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw,ufw allow OpenSSHandufw --force enable, theninstall -d -m 700 /var/backups/milvusand copy the archive into it. - In one root shell, run these in turn; each prints ok or STOP and acts only if the one before succeeded (it leaves a marker under
/root); Milvus keeps running until the archive passes. First, Docker and any old set-aside copy.rm -f /root/restore.ok /root/restore.members; docker info --format json | grep -Eq 'ServerVersion.:.(2[89]|[3-9][0-9])' && test ! -e /opt/milvus.previous && touch /root/restore.ok && echo ok || echo STOP, old Engine or /opt/milvus.previous exists - Put the archive name in
Aand test it.A=/var/backups/milvus/milvus-YYYY-MM-DD-HHMMSS.tar.gz; test -e /root/restore.ok && gzip -t $A && echo $A > /root/restore.ok && echo ok || echo STOP, run the first step again or the archive is damaged - Count its members.
[ $(tar -tzf $(cat /root/restore.ok) | grep -cxE 'milvus/(docker-compose.yml|milvus.yaml|root-password|login-check.sh|volumes/(etcd|minio|milvus)/)') = 7 ] && touch /root/restore.members && echo ok || echo STOP, archive incomplete - Stop Milvus and move the current folder to
/opt/milvus.previous.test -e /root/restore.members && test ! -e /opt/milvus.previous && docker compose -p milvus down && { [ ! -e /opt/milvus ] || mv /opt/milvus /opt/milvus.previous; } && echo ok || echo STOP, nothing moved - Extract and start.
test -e /root/restore.members && test ! -e /opt/milvus && tar --numeric-owner -xzf $(cat /root/restore.ok) -C /opt && docker compose -f /opt/milvus/docker-compose.yml up -d && rm /root/restore.* && echo restored || echo STOP, use the rollback - When
docker compose -f /opt/milvus/docker-compose.yml psshowsmilvus-standalonehealthy, runbash /opt/milvus/login-check.sh; it must printlogin works. Extracting as root with--numeric-ownerkeeps the original numeric owners; the Compose file's own comment says to remove the.milvus-volume-owner-*marker only after restoring files owned by a different user. - If the data is wrong, set the failed copy aside as
/opt/milvus.failed,test -d /opt/milvus.previous && test ! -e /opt/milvus.failed && docker compose -p milvus down && { [ ! -e /opt/milvus ] || mv /opt/milvus /opt/milvus.failed; } && echo ok || echo STOP, nothing moved, then put the old one back.test ! -e /opt/milvus && mv /opt/milvus.previous /opt/milvus && docker compose -f /opt/milvus/docker-compose.yml up -d && echo rolled back || echo STOP, look in /opt - Once satisfied, remove whichever of
/opt/milvus.previousand/opt/milvus.failedexists; on a rebuilt machine, rerun the backup block.
Upgrading to a newer Milvus image
The upgrade guide swaps only the image tag, keeps the Compose file and dependency versions, recreates only standalone, and starts its 3.0 procedure from 2.6.x. An image-only rollback is unvalidated after new writes, so recovery is the backup. Read the upgrade guide for the release you are moving to first; if it asks for more than a new tag, follow it instead.
- Run
bash /usr/local/sbin/milvus-backup.sh; it must end withok. - As root, set
Nto the new tag from the releases page and pull it.N=vX.Y.Z; [[ $N =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] && docker pull milvusdb/milvus:$N && echo ok || echo STOP, bad tag or pull failed - In the same shell, point the Compose file at it, keeping the old file.
f=/opt/milvus/docker-compose.yml; docker image inspect milvusdb/milvus:$N >/dev/null && test ! -e $f.before-$N && cp $f $f.before-$N && sed -i "s#milvus:v[0-9.]*\$#milvus:$N#" $f && echo ok || echo STOP, no image, tag reused, or edit failed - Recreate Milvus.
grep -q milvusdb/milvus:$N$ /opt/milvus/docker-compose.yml && docker compose -f /opt/milvus/docker-compose.yml up -d standalone && echo upgraded || echo STOP, if the file names $N the restart failed, so restore the step 1 backup, otherwise nothing changed - Check
docker compose -f /opt/milvus/docker-compose.yml psanddocker compose -f /opt/milvus/docker-compose.yml logs --tail 100 standalone. If it misbehaves, restore the step 1 archive instead of switching the tag back.
Licence and limits
Milvus is Apache License 2.0 (the LICENSE file in milvus-io/milvus). Our general reading, not legal advice. It is permissive, so you may run and modify it, and copies you pass on keep the licence text and notices; there is no duty to publish your source.
Standalone is one server holding one copy. The installation overview points to Milvus Distributed on Kubernetes once data outgrows a server.
Questions
Is Milvus Standalone fit for production use?
The installation overview calls a single machine with sufficient memory a good option when you want to avoid Kubernetes; a tested restore stands in for replication.
What are the default Milvus credentials?
With authentication enabled, Milvus creates root with the password Milvus. The shipped milvus.yaml leaves authentication off, and the setup above turns it on and replaces that password.
How much RAM should a Milvus box have?
At least the 8 GB floor (16 GB recommended), plus enough to hold the index files and field data of every collection you keep loaded.