Workload · Hosting a vector database

Running Milvus Standalone for your search or RAG app, on a server of your own

To run Milvus Standalone yourself, you need one Linux machine with Docker, a minimum of 8 GB of RAM (the project recommends 16 GB), a processor offering AVX-512, AVX2, AVX or SSE4.2, and an SSD, ideally NVMe. One Compose file from the Milvus releases brings up the database and its two helpers. Before your app connects, close the three gaps that file leaves (public ports, logins switched off and a well-known admin password).

The short version
  1. RAM sized to loaded collections
  2. Private ports, logins on, a fresh admin password
  3. A nightly copy that checks itself, and a tested way back
  4. Updates change one image tag, after a backup

What you are actually running

Milvus keeps bookkeeping in etcd and data in MinIO; the Standalone file starts all three plus a one-shot ownership helper, listening as follows.

  • 19530, where apps and SDKs connect, over gRPC or the REST API.
  • 9091, the health check, metrics and a WebUI at /webui/.
  • 9000 and 9001, MinIO's API and console, keys minioadmin.
  • etcd publishes nothing to the host.

Without an address in front, Docker opens those ports on every address, 0.0.0.0 and [::], ahead of ufw's rules, and logins are off in the default milvus.yaml. The setup below closes both first.

How much server your vectors need

The Milvus prerequisites ask for an Intel 2nd Gen Core or later (or Apple Silicon) with a suggestion of at least 4 cores, plus a SATA 3.0 SSD as the floor and NVMe preferred.

Memory matters most, because a collection is loaded into RAM, index files and all field data, before Milvus can search it. Float vectors alone take count × dimensions × 4 bytes; by our own arithmetic, before any index, that gives these figures.

VectorsDimensionsRaw vector data
1 million768about 3.1 GB
10 million768about 31 GB
10 million1,536about 61 GB

The index and other fields come on top, depending on the index type; the Milvus Sizing Tool on milvus.io estimates the rest, to be tested against your own data. For etcd's sake the prerequisites recommend local NVMe, checked with fio for over 500 IOPS and 99th-percentile fsync below 10 ms.

The server that fits

A dedicated server means one physical machine running your work and nobody else's, with root access, unmetered bandwidth and no contract. You manage the server; we keep the hardware and the network running. Good starting points follow.

  • For most RAG apps, a Xeon D-1587 cartridge, giving you 16 cores, between 16 and 128 GB of memory and a 1.92 TB NVMe drive. Bucharest also has the Xeon E-2286M, with two NVMe drives and up to 64 GB. See our NVMe server page for more drive choices.
  • For hundreds of gigabytes in memory, the two-processor Xeon Gold 6130 (up to 256 GB) or four-processor machines (up to half a terabyte). Ask for NVMe on a server built to your spec.
  • Skip the X-Gene ARM cartridge, which installs nothing newer than Ubuntu 18.04. With only Intel and Apple Silicon listed, test Ampere before relying on ARM (our caution).

A ready server is handed over within 30 minutes of approval; a build to your own spec takes hours, not minutes. Rack servers and blades include a remote console that works even when the system will not boot, as do two cartridge models, m510 and m710x; other cartridges give you power control alone.

Installing it with the doors closed

On Ubuntu 24.04, first set up Docker Engine and its Compose plugin using Docker's Ubuntu instructions. Milvus itself needs Docker 19.03+ with Compose V2; this guide wants Engine 28 or later, since only from that release is a port bound to 127.0.0.1 reachable from the server alone. Become root with sudo -i and paste this. ufw allow OpenSSH only covers port 22, so if your SSH listens on another port, allow that one first.

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
TAG=v3.0.5; M=/srv/milvus; C=$M/docker-compose.yml; Y=$M/milvus.yaml
check() {
  grep -qx '    authorizationEnabled: true' "$Y" &&
  [ "$(grep -c '^ *- "127\.0\.0\.1:[0-9]*:[0-9]*"$' "$F")" -eq 4 ] &&
  ! grep -Eq '^ +- "[0-9]+:[0-9]+"' "$F" &&
  [ "$(grep -c '^    restart: unless-stopped$' "$F")" -eq 3 ] &&
  grep -qx '      - ./milvus.yaml:/milvus/configs/milvus.yaml' "$F" &&
  docker compose -f "$F" config -q
}
ev=$(docker version -f '{{.Server.Version}}') && [ "${ev%%.*}" -ge 28 ] && install -d -m 750 $M &&
if [ -e $C ]; then F=$C && check && echo "existing files look right"
else F=$C.tmp && R=https://github.com/milvus-io/milvus &&
  curl -fsSL $R/releases/download/$TAG/milvus-standalone-docker-compose.yaml -o $C.tmp &&
  curl -fsSL https://raw.githubusercontent.com/milvus-io/milvus/$TAG/configs/milvus.yaml -o $Y &&
  sed -i 's/^    authorizationEnabled: false$/    authorizationEnabled: true/' $Y &&
  sed -i -E 's/- "(19530|9091|9000|9001):/- "127.0.0.1:\1:/' $C.tmp &&
  sed -i '/^    container_name: milvus-/a\    restart: unless-stopped' $C.tmp &&
  sed -i '/^  standalone:/,/^    healthcheck:/ s#^      - .*/volumes/milvus:/var/lib/milvus$#&\n      - ./milvus.yaml:/milvus/configs/milvus.yaml#' $C.tmp &&
  check && mv $C.tmp $C && echo "files ready for $TAG"
fi || echo "STOP - Docker Engine below 28, or a download or check went wrong"

The edited Compose file only goes into place after it tests clean. Pasted again, the block just re-tests and prints existing files look right or STOP (with no data loaded yet, delete /srv/milvus/docker-compose.yml and paste again). Here is everything we change.

  • Ports 19530, 9091, 9000 and 9001 bound to 127.0.0.1.
  • The etcd, MinIO and Milvus containers get the unless-stopped policy, so they return after a reboot (the file ships with none); the one-shot helper keeps its no-restart setting.
  • In milvus.yaml, common.security.authorizationEnabled set to true, and the Milvus container reads that file from /milvus/configs/milvus.yaml, following the Docker configuration page.

Changing the admin password before anything connects

Turning logins on creates the user root with the password Milvus, which everyone knows. This block saves a login check as /srv/milvus/login-check.sh, starts Milvus, then sets and tests a random password kept in a root-only file. A pipe keeps that password out of ps.

M=/srv/milvus; K=$M/root.pass; L=$M/login-check.sh
install -m 700 /dev/null $L && cat > $L <<'EOF'
p=${1:-$(cat /srv/milvus/root.pass)}
printf '%s\n' "Authorization: Bearer root:$p" | curl -fsS -X POST -d '{}' -H 'Content-Type: application/json' \
  -H @- http://127.0.0.1:19530/v2/vectordb/users/list | grep -Eq '"code": ?0[,}]' &&
  echo "login works" || { echo "STOP - that root password was not accepted, or nothing answers on 127.0.0.1:19530"; exit 1; }
EOF
if [ -e $K ]; then echo "STOP - $K is already there; root was changed before"
else docker compose -f $M/docker-compose.yml up -d &&
  for i in $(seq 60); do s=$(docker inspect -f '{{.State.Health.Status}}' milvus-standalone); [ "$s" = healthy ] && break; sleep 10; done &&
  [ "$s" = healthy ] && new="Pc-$(openssl rand -hex 16)" && [ ${#new} -eq 35 ] &&
  install -m 600 /dev/null $K && printf '%s\n' "$new" > $K &&
  printf '{"userName":"root","password":"Milvus","newPassword":"%s"}' "$new" |
    curl -fsS -X POST http://127.0.0.1:19530/v2/vectordb/users/update_password \
      -H 'Authorization: Bearer root:Milvus' -H 'Content-Type: application/json' -d @- |
    grep -Eq '"code": ?0[,}]' && bash $L &&
  echo "done, the new root password is in $K" ||
  echo "STOP - Compose did not start, Milvus stayed unhealthy for 10 minutes, or the change or its check failed; read on below"
fi

If it stops after root.pass exists, run bash /srv/milvus/login-check.sh Milvus. A login works answer means the old password is still active, so delete root.pass and paste again. A STOP, at a time when docker compose -f /srv/milvus/docker-compose.yml ps lists Milvus as healthy, means the change did happen and root.pass holds the new password; bash /srv/milvus/login-check.sh should then answer login works. Local apps use http://127.0.0.1:19530 with a user:password token, and app containers can join the milvus network as standalone:19530.

From your laptop, logged in as yourself, run ssh -N -L 9091:127.0.0.1:9091 -L 19530:127.0.0.1:19530 you@your-server and open http://127.0.0.1:9091/webui/. Attu's desktop app uses that tunnel too. MinIO keeps minioadmin, reachable only locally; before others get shell accounts, change it in milvus.yaml and the Compose file together. Apps on other machines come in the same way, through an SSH tunnel that can also run across a WireGuard VPN server; the 19530 mapping stays on 127.0.0.1 and never moves to 0.0.0.0 or the public address.

Nightly copies you can bring back

The upgrade guide demands a backup without saying how. Our script stops the three containers briefly, packs the whole /srv/milvus folder (files, password, and the etcd, MinIO and Milvus data), starts them again, and only holds on to the archive once it has checked every piece is inside. Week-old copies go after a good save.

install -d -m 700 /srv/milvus-backups
cat > /usr/local/sbin/milvus-save.sh <<'EOF'
set -uo pipefail
umask 077
O=/srv/milvus-backups; C=/srv/milvus/docker-compose.yml
t="$O/vectors-$(date +%F-%H%M%S).tgz"
find "$O" \( -name 'run-*.log' -mtime +30 -o -name '*.tmp' -mtime +1 \) -delete
trap 'docker compose -f "$C" start >/dev/null 2>&1' EXIT
docker compose -f "$C" stop || exit 1
tar -czf "$t.tmp" -C /srv/milvus .; st=$?
docker compose -f "$C" start
if [ "$st" -ne 0 ]; then echo "backup FAILED, tar exit $st"; rm -f "$t.tmp"; exit 1; fi
list=$(tar -tzf "$t.tmp") || { echo "backup FAILED, cannot read $t.tmp"; rm -f "$t.tmp"; exit 1; }
for want in docker-compose.yml milvus.yaml root.pass login-check.sh volumes/etcd/ volumes/minio/ volumes/milvus/; do
  grep -qxF "./$want" <<<"$list" || { echo "backup FAILED, ./$want not in archive"; rm -f "$t.tmp"; exit 1; }
done
mv "$t.tmp" "$t" && find "$O" -name 'vectors-*.tgz' -mtime +6 -delete && echo "saved $t"
EOF
printf '%s\n' '40 2 * * * root bash /usr/local/sbin/milvus-save.sh > /srv/milvus-backups/run-$(date +\%F).log 2>&1' \
  > /etc/cron.d/milvus-save
bash /usr/local/sbin/milvus-save.sh

Push /srv/milvus-backups elsewhere using rsync -a (never --delete) from a job you control. Restores run as root.

  1. On a fresh server, install Docker 28+ as above. The next line lets SSH in on port 22 alone; if your SSH uses another port, open it first. Then paste apt-get update && apt-get -y install curl openssl ufw && sed '/^IPV6=/s/=.*/=yes/' -i /etc/default/ufw && ufw allow 22/tcp comment ssh && ufw --force enable && install -d -m 700 /srv/milvus-backups and put the archive in /srv/milvus-backups.
  2. Paste the next lines one by one in the same root shell. Each answers with a word or a STOP and does nothing unless the previous one worked, so Milvus is only stopped after the archive checks out. First, Docker and any leftover copy. rm -f /root/milvus.*; docker info -f json | grep -Eq 'ServerVersion.:.(29|28|[3-9][[:digit:]])' && test ! -e /srv/milvus.before-restore && touch /root/milvus.restore && echo ready || echo STOP, Docker below 28 or a before-restore folder exists
  3. Fill in the archive name in T. T=/srv/milvus-backups/vectors-YYYY-MM-DD-HHMMSS.tgz; test -e /root/milvus.restore && gzip -t $T && echo $T > /root/milvus.restore && echo fine || echo STOP, archive unreadable or first step not done
  4. Make sure every piece is inside. [ $(tar -tzf $(cat /root/milvus.restore) | grep -cxE '\./(docker-compose\.yml|milvus\.yaml|root\.pass|login-check\.sh|volumes/(etcd|minio|milvus)/)') -eq 7 ] && touch /root/milvus.checked && echo complete || echo STOP, pieces missing
  5. Stop Milvus and keep what was live as /srv/milvus.before-restore. test -e /root/milvus.checked && test ! -e /srv/milvus.before-restore && docker compose --project-name milvus down && if [ -e /srv/milvus ]; then mv /srv/milvus /srv/milvus.before-restore; fi && echo moved aside || echo STOP, nothing moved
  6. Unpack the archive. test -e /root/milvus.checked && test ! -e /srv/milvus && install -d -m 750 /srv/milvus && tar -xz --numeric-owner -f $(cat /root/milvus.restore) -C /srv/milvus && touch /root/milvus.unpacked && echo unpacked || echo STOP, unpacking failed
  7. Start it. test -e /root/milvus.unpacked && docker compose -f /srv/milvus/docker-compose.yml up --detach && rm /root/milvus.* && echo back up || echo STOP, not unpacked or not started If unpacking or starting stops after Milvus was moved aside, use the undo below.
  8. Once it reports healthy in docker compose -f /srv/milvus/docker-compose.yml ps, run bash /srv/milvus/login-check.sh and look for login works. Because tar runs as root with --numeric-owner, files keep their numeric owners; a comment inside the Compose file explains that only files restored from a different user need the .milvus-volume-owner-* marker deleted.
  9. To undo it, move the restored folder out of the way, test -d /srv/milvus.before-restore && test ! -e /srv/milvus.bad-restore && docker compose --project-name milvus down && if [ -e /srv/milvus ]; then mv /srv/milvus /srv/milvus.bad-restore; fi && echo cleared || echo STOP, nothing moved, and bring the earlier one back. test ! -e /srv/milvus && mv /srv/milvus.before-restore /srv/milvus && docker compose -f /srv/milvus/docker-compose.yml up --detach && echo live again || echo STOP, look in /srv
  10. When happy, delete /srv/milvus.before-restore and any /srv/milvus.bad-restore; on a fresh server, paste the backup block again.

Moving to a newer Milvus

In Milvus's own procedure the one thing that changes is the image tag; it keeps the Compose file and the etcd and MinIO versions, and recreates just the Milvus container. Its 3.0 guide starts from 2.6.x, and switching the tag back is not validated after the new release writes anything, so your way back is the backup. Before each upgrade, check the guide written for your target release, and if it involves more than swapping the tag, do what it says.

  1. Paste these in one root shell (sudo -i, since /srv/milvus is root-only), in order; NEW must carry over. Start with bash /usr/local/sbin/milvus-save.sh and wait for saved.
  2. Put the new tag from the GitHub releases page in NEW and pull it first, so a typo changes nothing. NEW=vX.Y.Z; echo $NEW | grep -Eqx 'v[[:digit:]]+(\.[[:digit:]]+){2}' && docker pull milvusdb/milvus:$NEW && echo image ready || echo STOP, tag or pull problem
  3. Then, in the same shell, swap the image tag inside the Compose file, keeping a copy. C=/srv/milvus/docker-compose.yml; docker image ls -q milvusdb/milvus:$NEW | grep -q . && test ! -e $C.pre-$NEW && cp $C $C.pre-$NEW && sed -i -E "s#(milvusdb/milvus:)v[.[:digit:]]+\$#\1$NEW#" $C && echo tag set || echo STOP, file unchanged
  4. Recreate the Milvus container. cd /srv/milvus && grep -q milvusdb/milvus:$NEW$ docker-compose.yml && docker compose up --detach standalone && echo now on $NEW || echo STOP, tag not in the file or restart failed
  5. Read the last lines of docker compose -f /srv/milvus/docker-compose.yml logs -n 100 standalone, then confirm all three are up in docker compose -f /srv/milvus/docker-compose.yml ps. If something is off, restore the copy from the first step.

When one server stops being enough

Standalone is one copy on one machine; outgrow it and the overview suggests Distributed, on Kubernetes.

Milvus is licensed under Apache 2.0 (the LICENSE file in its GitHub repository). This is how we read it, not legal advice. Use and change it freely; copies you hand to others must carry the licence text and notices.

Questions people ask

Can I use Milvus Standalone for a live app, or only for testing?
Milvus's overview calls one machine with enough memory a sensible choice without Kubernetes. It holds one copy, so test your restore.
Do I have to pay for Milvus itself?
No. It is open source under Apache 2.0, so only the server costs money.
Does Attu work with this setup?
Yes, the desktop app works through the SSH tunnel above, signed in as a Milvus user.

Give your vectors a server of their own

Work out the memory your collections need, then pick a machine with that RAM and an NVMe drive.

Choose a server for Milvus

Read next