Docker
Nervix publishes a rolling multi-architecture Debian image to
ghcr.io/nervix-io/nervix:debian-latest. The image contains nervix-server, nervix-cli, and
nervix-nspl-format. The examples on this page create a three-node cluster for local evaluation.
For a reproducible or production deployment, replace the rolling tag with a reviewed timestamped tag or image digest. Use TLS, a secret manager, and a client-reachable advertise hostname instead of the local-development defaults shown here.
CUDA Images
Build a local CUDA image from a repository checkout with just docker-build-cuda; the build
requirements are in Developing Nervix. The image
uses the cuda target in Dockerfile.debian; the CPU image uses the cpu target. Both copy
the same Nervix binaries from one shared builder stage. The CUDA image
contains ONNX Runtime 1.30.0’s CUDA 13 provider, CUDA 13.2.2, and cuDNN 9.25.1. It supports Linux
AMD64 and pins the Debian base to debian:trixie-20260918-slim and its image digest. CUDA and
cuDNN occupy one layer directly above that base. The next layer updates and upgrades Debian
packages while holding CUDA and cuDNN at their pinned versions. Nervix’s layers follow and
reuse that CUDA layer across application builds.
To expose an NVIDIA GPU, install a host driver supporting CUDA 13.2 and the NVIDIA Container
Toolkit, set NERVIX_IMAGE=nervix:cuda, and add --gpus all to the docker run commands below.
The container toolkit supplies the host driver; CUDA and cuDNN are supplied by the image.
Run Three Nodes With Docker
Choose the image and initial password. The password environment variable is expanded by the shell and passed only to the bootstrap node, which creates the default user with it. The other nodes need no password: they read that user from the cluster, and a node that stops gracefully asks the leader to drain it under its node certificate.
export NERVIX_IMAGE=ghcr.io/nervix-io/nervix:debian-latest
export NERVIX_INIT_DEFAULT_USER_PASSWORD='replace-with-a-password'
docker network create nervix
docker pull "$NERVIX_IMAGE"
Provision a CA and one certificate/key pair per node under ./tls before starting the containers:
tls/ca.pem
tls/node-1.pem
tls/node-1-key.pem
tls/node-2.pem
tls/node-2-key.pem
tls/node-3.pem
tls/node-3-key.pem
Each node certificate must permit TLS server and client authentication. It must contain its Docker
hostname (nervix-1, nervix-2, or nervix-3) as a DNS SAN and exactly one identity URI matching
its configured cluster and node, such as nervix://cluster/nervix-docker/node/node-1. Sign all
three certificates with the CA in ca.pem.
Start the bootstrap node:
docker run --detach \
--name nervix-1 \
--hostname nervix-1 \
--network nervix \
--restart unless-stopped \
--stop-timeout 60 \
--volume nervix-node-1-data:/var/lib/nervix \
--volume "$PWD/tls/ca.pem:/etc/nervix/interconnect/ca.pem:ro" \
--volume "$PWD/tls/node-1.pem:/etc/nervix/interconnect/node.pem:ro" \
--volume "$PWD/tls/node-1-key.pem:/etc/nervix/interconnect/node-key.pem:ro" \
--publish 47391:47391 \
--publish 47421:47420 \
--publish 9091:9090 \
--env NERVIX_ADDR=0.0.0.0:47391 \
--env NERVIX_GRPC_ADVERTISE_ADDR=127.0.0.1:47391 \
--env NERVIX_WEB_CONSOLE_LISTEN_ADDR=0.0.0.0:47420 \
--env NERVIX_WEB_CONSOLE_ADVERTISE_ADDR=127.0.0.1:47421 \
--env NERVIX_OBSERVABILITY_LISTEN_ADDR=0.0.0.0:9090 \
--env NERVIX_CLUSTER_ID=nervix-docker \
--env NERVIX_NODE_ID=node-1 \
--env NERVIX_INTERCONNECT_LISTEN_ADDR=0.0.0.0:47395 \
--env NERVIX_INTERCONNECT_ADVERTISE_ADDR=nervix-1:47395 \
--env NERVIX_INTERCONNECT_TLS_CA=/etc/nervix/interconnect/ca.pem \
--env NERVIX_INTERCONNECT_TLS_CERT=/etc/nervix/interconnect/node.pem \
--env NERVIX_INTERCONNECT_TLS_KEY=/etc/nervix/interconnect/node-key.pem \
--env NERVIX_REPLICA_COUNT=3 \
--env NERVIX_ALLOW_BOOTSTRAP=true \
--env NERVIX_INIT_DEFAULT_USER_PASSWORD \
"$NERVIX_IMAGE"
Start the other two nodes. Both discover the cluster through nervix-1 on the private Docker
network:
Keep each node’s named data volume across restarts. Once the three nodes have joined, their Raft
membership retains the advertised interconnect names. If nervix-1 stops while the other nodes
continue, it uses those names to find a survivor when it restarts; it does not need a second
configured bootstrap host. The joining nodes still use NERVIX_CLUSTER_BOOTSTRAP_HOST for their
first contact with a fresh cluster. Once they have recovered Raft peer endpoints from their volumes,
they can restart while that bootstrap host does not resolve and rejoin through another peer whose
name resolves. Keep the other members’ advertised names resolvable to the restarting node.
docker run --detach \
--name nervix-2 \
--hostname nervix-2 \
--network nervix \
--restart unless-stopped \
--stop-timeout 60 \
--volume nervix-node-2-data:/var/lib/nervix \
--volume "$PWD/tls/ca.pem:/etc/nervix/interconnect/ca.pem:ro" \
--volume "$PWD/tls/node-2.pem:/etc/nervix/interconnect/node.pem:ro" \
--volume "$PWD/tls/node-2-key.pem:/etc/nervix/interconnect/node-key.pem:ro" \
--publish 47392:47391 \
--publish 47422:47420 \
--publish 9092:9090 \
--env NERVIX_ADDR=0.0.0.0:47391 \
--env NERVIX_GRPC_ADVERTISE_ADDR=127.0.0.1:47392 \
--env NERVIX_WEB_CONSOLE_LISTEN_ADDR=0.0.0.0:47420 \
--env NERVIX_WEB_CONSOLE_ADVERTISE_ADDR=127.0.0.1:47422 \
--env NERVIX_OBSERVABILITY_LISTEN_ADDR=0.0.0.0:9090 \
--env NERVIX_CLUSTER_ID=nervix-docker \
--env NERVIX_NODE_ID=node-2 \
--env NERVIX_INTERCONNECT_LISTEN_ADDR=0.0.0.0:47395 \
--env NERVIX_INTERCONNECT_ADVERTISE_ADDR=nervix-2:47395 \
--env NERVIX_INTERCONNECT_TLS_CA=/etc/nervix/interconnect/ca.pem \
--env NERVIX_INTERCONNECT_TLS_CERT=/etc/nervix/interconnect/node.pem \
--env NERVIX_INTERCONNECT_TLS_KEY=/etc/nervix/interconnect/node-key.pem \
--env NERVIX_REPLICA_COUNT=3 \
--env NERVIX_CLUSTER_BOOTSTRAP_HOST=nervix-1:47395 \
"$NERVIX_IMAGE"
docker run --detach \
--name nervix-3 \
--hostname nervix-3 \
--network nervix \
--restart unless-stopped \
--stop-timeout 60 \
--volume nervix-node-3-data:/var/lib/nervix \
--volume "$PWD/tls/ca.pem:/etc/nervix/interconnect/ca.pem:ro" \
--volume "$PWD/tls/node-3.pem:/etc/nervix/interconnect/node.pem:ro" \
--volume "$PWD/tls/node-3-key.pem:/etc/nervix/interconnect/node-key.pem:ro" \
--publish 47393:47391 \
--publish 47423:47420 \
--publish 9093:9090 \
--env NERVIX_ADDR=0.0.0.0:47391 \
--env NERVIX_GRPC_ADVERTISE_ADDR=127.0.0.1:47393 \
--env NERVIX_WEB_CONSOLE_LISTEN_ADDR=0.0.0.0:47420 \
--env NERVIX_WEB_CONSOLE_ADVERTISE_ADDR=127.0.0.1:47423 \
--env NERVIX_OBSERVABILITY_LISTEN_ADDR=0.0.0.0:9090 \
--env NERVIX_CLUSTER_ID=nervix-docker \
--env NERVIX_NODE_ID=node-3 \
--env NERVIX_INTERCONNECT_LISTEN_ADDR=0.0.0.0:47395 \
--env NERVIX_INTERCONNECT_ADVERTISE_ADDR=nervix-3:47395 \
--env NERVIX_INTERCONNECT_TLS_CA=/etc/nervix/interconnect/ca.pem \
--env NERVIX_INTERCONNECT_TLS_CERT=/etc/nervix/interconnect/node.pem \
--env NERVIX_INTERCONNECT_TLS_KEY=/etc/nervix/interconnect/node-key.pem \
--env NERVIX_REPLICA_COUNT=3 \
--env NERVIX_CLUSTER_BOOTSTRAP_HOST=nervix-1:47395 \
"$NERVIX_IMAGE"
The host ports are:
| Node | gRPC | Web console | Health and metrics |
|---|---|---|---|
node-1 | 47391 | 47421 | 9091 |
node-2 | 47392 | 47422 | 9092 |
node-3 | 47393 | 47423 | 9093 |
Connect an installed CLI to http://127.0.0.1:47391, or open the
web console at http://127.0.0.1:47421/console/, and run:
SHOW CLUSTER STATUS;
The cluster status should show one local node, two live peer nodes, and all three nodes in the Raft membership. The CLI follows leader redirects through the three published gRPC ports.
Each node advertises its container name, such as nervix-1:47395, and finds its peers by those
names through Docker’s embedded DNS, which Docker names in the container’s /etc/resolv.conf. A
node resolves peer names, and the hosts its connectors reach, with its own asynchronous resolver
from that file and /etc/hosts, read once at startup; Name Resolution
describes the options that point it at other files or name servers, and what it does not support.
docker stop sends each node SIGTERM, which starts graceful shutdown, and kills a container only
if its node is still running when the stop timeout ends. A node ends its own shutdown within its
shutdown timeout, 50 seconds by default, which covers the 30-second default drain timeout and the
services that stop after it. The --stop-timeout 60 above keeps Docker waiting longer than that,
so a node that cannot finish shutting down exits with status 1 on its own rather than being
killed; with Docker’s default of ten seconds, a node could be killed partway through its drain.
Shutdown And Recovery explains what a node loses when its
grace period ends first.
Stop the nodes, then remove the containers and private network:
docker stop nervix-1 nervix-2 nervix-3
docker rm nervix-1 nervix-2 nervix-3
docker network rm nervix
The named volumes remain so the cluster can be recreated with its state intact. Removing
nervix-node-1-data, nervix-node-2-data, and nervix-node-3-data permanently deletes that
state.
Run Three Nodes With Docker Compose
Save the following as docker-compose.yml:
x-nervix-common: &nervix-common
image: ${NERVIX_IMAGE:-ghcr.io/nervix-io/nervix:debian-latest}
restart: unless-stopped
stop_grace_period: 60s
environment: &nervix-environment
NERVIX_ADDR: 0.0.0.0:47391
NERVIX_WEB_CONSOLE_LISTEN_ADDR: 0.0.0.0:47420
NERVIX_OBSERVABILITY_LISTEN_ADDR: 0.0.0.0:9090
NERVIX_CLUSTER_ID: nervix-docker
NERVIX_INTERCONNECT_LISTEN_ADDR: 0.0.0.0:47395
NERVIX_INTERCONNECT_TLS_CA: /etc/nervix/interconnect/ca.pem
NERVIX_INTERCONNECT_TLS_CERT: /etc/nervix/interconnect/node.pem
NERVIX_INTERCONNECT_TLS_KEY: /etc/nervix/interconnect/node-key.pem
NERVIX_REPLICA_COUNT: "3"
networks:
- nervix
services:
nervix-1:
<<: *nervix-common
hostname: nervix-1
environment:
<<: *nervix-environment
NERVIX_NODE_ID: node-1
NERVIX_GRPC_ADVERTISE_ADDR: 127.0.0.1:47391
NERVIX_WEB_CONSOLE_ADVERTISE_ADDR: 127.0.0.1:47421
NERVIX_INTERCONNECT_ADVERTISE_ADDR: nervix-1:47395
NERVIX_ALLOW_BOOTSTRAP: "true"
NERVIX_INIT_DEFAULT_USER_PASSWORD: ${NERVIX_INIT_DEFAULT_USER_PASSWORD:?set NERVIX_INIT_DEFAULT_USER_PASSWORD}
ports:
- "47391:47391"
- "47421:47420"
- "9091:9090"
volumes:
- nervix-node-1-data:/var/lib/nervix
- ./tls/ca.pem:/etc/nervix/interconnect/ca.pem:ro
- ./tls/node-1.pem:/etc/nervix/interconnect/node.pem:ro
- ./tls/node-1-key.pem:/etc/nervix/interconnect/node-key.pem:ro
nervix-2:
<<: *nervix-common
hostname: nervix-2
depends_on:
- nervix-1
environment:
<<: *nervix-environment
NERVIX_NODE_ID: node-2
NERVIX_GRPC_ADVERTISE_ADDR: 127.0.0.1:47392
NERVIX_WEB_CONSOLE_ADVERTISE_ADDR: 127.0.0.1:47422
NERVIX_INTERCONNECT_ADVERTISE_ADDR: nervix-2:47395
NERVIX_CLUSTER_BOOTSTRAP_HOST: nervix-1:47395
ports:
- "47392:47391"
- "47422:47420"
- "9092:9090"
volumes:
- nervix-node-2-data:/var/lib/nervix
- ./tls/ca.pem:/etc/nervix/interconnect/ca.pem:ro
- ./tls/node-2.pem:/etc/nervix/interconnect/node.pem:ro
- ./tls/node-2-key.pem:/etc/nervix/interconnect/node-key.pem:ro
nervix-3:
<<: *nervix-common
hostname: nervix-3
depends_on:
- nervix-1
environment:
<<: *nervix-environment
NERVIX_NODE_ID: node-3
NERVIX_GRPC_ADVERTISE_ADDR: 127.0.0.1:47393
NERVIX_WEB_CONSOLE_ADVERTISE_ADDR: 127.0.0.1:47423
NERVIX_INTERCONNECT_ADVERTISE_ADDR: nervix-3:47395
NERVIX_CLUSTER_BOOTSTRAP_HOST: nervix-1:47395
ports:
- "47393:47391"
- "47423:47420"
- "9093:9090"
volumes:
- nervix-node-3-data:/var/lib/nervix
- ./tls/ca.pem:/etc/nervix/interconnect/ca.pem:ro
- ./tls/node-3.pem:/etc/nervix/interconnect/node.pem:ro
- ./tls/node-3-key.pem:/etc/nervix/interconnect/node-key.pem:ro
networks:
nervix:
volumes:
nervix-node-1-data:
nervix-node-2-data:
nervix-node-3-data:
Set the required initial password and start the cluster from the directory containing that file:
export NERVIX_INIT_DEFAULT_USER_PASSWORD='replace-with-a-password'
docker compose up --detach
Set NERVIX_IMAGE before docker compose up to use a pinned tag or digest instead of
debian-latest.
Inspect the containers and follow their logs:
docker compose ps
docker compose logs --follow
Use the same host ports in the table above. Stop the cluster while preserving its volumes:
docker compose down
docker compose down sends each node SIGTERM and waits up to the file’s stop_grace_period of 60
seconds, longer than each node’s 50-second shutdown timeout, for graceful shutdown to finish before
it kills a container. To permanently delete all three nodes’ persisted state, add --volumes.