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.
| Vectors | Dimensions | Raw vector data |
|---|---|---|
| 1 million | 768 | about 3.1 GB |
| 10 million | 768 | about 31 GB |
| 10 million | 1,536 | about 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.authorizationEnabledset totrue, 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.
- 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-backupsand put the archive in/srv/milvus-backups. - 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 - 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 - 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 - 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 - 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 - 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 startedIf unpacking or starting stops after Milvus was moved aside, use the undo below. - Once it reports healthy in
docker compose -f /srv/milvus/docker-compose.yml ps, runbash /srv/milvus/login-check.shand look forlogin 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. - 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 - When happy, delete
/srv/milvus.before-restoreand 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.
- Paste these in one root shell (
sudo -i, since/srv/milvusis root-only), in order;NEWmust carry over. Start withbash /usr/local/sbin/milvus-save.shand wait forsaved. - Put the new tag from the GitHub releases page in
NEWand 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 - 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 - 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 - Read the last lines of
docker compose -f /srv/milvus/docker-compose.yml logs -n 100 standalone, then confirm all three are up indocker 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.