Use case · Vector database

Milvus Standalone on a dedicated server with Docker Compose, authentication and backups

Milvus Standalone is the single-machine form of the Milvus vector database. One official Docker Compose file starts etcd for metadata, MinIO for object storage and Milvus itself, which serves gRPC and REST on port 19530 and health and its WebUI on 9091. The prerequisites page asks for 8 GB of RAM (16 GB recommended), a CPU with SSE4.2, AVX, AVX2 or AVX-512, and an SSD, preferably NVMe. As shipped, every published port listens on all addresses and authentication is off, so the setup below binds them to 127.0.0.1, enables authentication and replaces the default root password first.

See the machines

What the Compose file starts

ServiceContainerRoleHost ports as shipped
milvus-volume-initunnamedRuns once as root, hands /var/lib/milvus to the image's milvus user, leaves a marker filenone, no network
etcdmilvus-etcdMetadata; auto-compaction, 4 GiB backend quotanone
miniomilvus-minioObject storage for Milvus data, default keys minioadmin9000 API, 9001 console
standalonemilvus-standaloneMilvus; /healthz checked every 30 s after a 90 s start period19530 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

ComponentRequiredRecommended
CPUIntel 2nd Gen Core or newer, or Apple Silicon4 cores or more for Standalone
Instruction setsAt least one of SSE4.2, AVX, AVX2, AVX-512Same list
RAM8 GB for Standalone16 GB for Standalone
DiskSATA 3.0 SSD or betterNVMe SSD or better
Linux softwareDocker 19.03 or later with Compose plugin V2Docker'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.yaml mounted at /milvus/configs/milvus.yaml in standalone, with common.security.authorizationEnabled set to true.
  • The four published ports (19530, 9091, 9000, 9001) prefixed with 127.0.0.1.
  • An unless-stopped restart policy on milvus-etcd, milvus-minio and milvus-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.

  1. On a rebuilt machine, install Docker as above. ufw allow OpenSSH opens port 22 only, so if sshd listens elsewhere, allow that port first. Then apt-get update && apt-get install -y ufw curl openssl, sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw, ufw allow OpenSSH and ufw --force enable, then install -d -m 700 /var/backups/milvus and copy the archive into it.
  2. 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
  3. Put the archive name in A and 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
  4. 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
  5. 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
  6. 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
  7. When docker compose -f /opt/milvus/docker-compose.yml ps shows milvus-standalone healthy, run bash /opt/milvus/login-check.sh; it must print login works. Extracting as root with --numeric-owner keeps 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.
  8. 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
  9. Once satisfied, remove whichever of /opt/milvus.previous and /opt/milvus.failed exists; 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.

  1. Run bash /usr/local/sbin/milvus-backup.sh; it must end with ok.
  2. As root, set N to 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
  3. 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
  4. 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
  5. Check docker compose -f /opt/milvus/docker-compose.yml ps and docker 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.

Pick the memory, then the machine

Run the sizing tool against your collections, then configure a server with that RAM and NVMe under etcd.

See the machines

Related on Server Room

Other use cases