Run a node in Docker
Container images of tenzro-node for CPU, CUDA and Vulkan: the TPM passthrough, GPUs, ports, volumes, health check and a systemd service.
Every testnet v1 release publishes container images of tenzro-node next to
the release archives. Each image carries the binaries of the matching archive,
unchanged, on a minimal base image.
| Tag | Platforms | Base | Serves models on |
|---|---|---|---|
tenzro-node:testnet-v1-cpu | linux/amd64, linux/arm64 | Debian 12 slim | the CPU (CPU-tier models) |
tenzro-node:testnet-v1-cuda | linux/amd64, linux/arm64 | NVIDIA CUDA runtime, Ubuntu 24.04 | NVIDIA GPUs (llama.cpp and ONNX models) |
tenzro-node:testnet-v1-vulkan | linux/amd64, linux/arm64 | Debian 12 slim, Vulkan loader, Mesa Vulkan drivers | AMD, Intel and NVIDIA GPUs through Vulkan (llama.cpp models) |
Each tag also exists pinned to the release commit:
tenzro-node:testnet-v1-<commit>-<flavour>. Use the pinned tag in anything you
deploy, so a host always runs the build you checked.
The Vulkan image serves llama.cpp models on the GPU. ONNX Runtime has no Vulkan provider, so on an NVIDIA or AMD GPU it refuses ONNX models (embeddings, vision, audio and the other ONNX runtimes) and names the CUDA or ROCm build that serves them. A GPU image never falls back to the CPU: on a host where it finds no usable GPU it stops at startup and names the CPU image.
What the host needs
- Linux with a TPM 2.0 at
/dev/tpmrm0. The node's identity and keys live in the TPM. The container refuses to start without it; it never generates a software key instead. - The TPM service credential
tenzro-device-key-root, which opens the node's key derivation root. It is sealed to the host TPM withsystemd-credsand decrypted by systemd when the node's service starts. - For
cuda: the NVIDIA driver and the NVIDIA Container Toolkit. - For
vulkan: on AMD and Intel, the kernel GPU driver (/dev/dri); the image brings the Mesa Vulkan drivers. On NVIDIA, the NVIDIA driver and the NVIDIA Container Toolkit, which mounts the driver's Vulkan support.
Ports and volumes
The image runs as the unprivileged user tenzro (uid 10001, gid 10001).
| What | Where | Notes |
|---|---|---|
| Data volume | /data/tenzro | Chain state, models and the wrapped TPM key blobs (ciphertext under this host's TPM, useless anywhere else). Mount a named volume or a directory owned by uid 10001. |
| Service credential | $CREDENTIALS_DIRECTORY/tenzro-device-key-root | Mounted read-only from the host service's credential directory, see below. |
| P2P | UDP 9001 | Outbound is enough; inbound is optional. See Node networking. |
| JSON-RPC | TCP 8545, loopback | Never open it to the network; put your own reverse proxy in front. |
| Web API and health | TCP 8080, loopback | GET /health answers 200 while the node is up. |
| MCP | TCP 3001, loopback | |
| A2A | TCP 3002, loopback |
The node binds its APIs to loopback only. Run the container with
--network host so they are on the host's loopback, where your reverse proxy
reaches them, and the P2P endpoint uses the host's UDP port directly.
The image's health check calls the node's own /health endpoint on port 8080
every 30 seconds; docker ps shows the result.
One-time setup
Create a host user with the image's uid that may use Docker and the TPM, and seal the service credential to the TPM:
sudo useradd --system --uid 10001 --user-group --create-home tenzro
sudo usermod -aG docker,"$(stat -c %G /dev/tpmrm0)" tenzro
sudo install -d -m 0700 /etc/credstore.encrypted
head -c 32 /dev/urandom | sudo systemd-creds encrypt --with-key=tpm2 \
--name=tenzro-device-key-root - /etc/credstore.encrypted/tenzro-device-key-root
sudo install -d -o 10001 -g 10001 -m 0700 /srv/tenzroThe service
systemd decrypts the credential when the service starts and exposes it, owned
by the service user, in $CREDENTIALS_DIRECTORY; the unit mounts that
directory read-only into the container. In a unit file $$ passes a $ to
the shell. Save this as
/etc/systemd/system/tenzro-node.service, choosing the image line for your
hardware:
[Unit]
Description=Tenzro node
After=docker.service
Requires=docker.service
[Service]
User=tenzro
LoadCredentialEncrypted=tenzro-device-key-root:/etc/credstore.encrypted/tenzro-device-key-root
Environment=IMAGE=tenzro-node:testnet-v1-<commit>-cpu
# The group id of /dev/tpmrm0 (stat -c %g /dev/tpmrm0)
Environment=TPM_GID=<gid>
# NVIDIA: Environment=GPU=--gpus all
# AMD or Intel: Environment=GPU=--device /dev/dri --group-add <gid of /dev/dri/renderD128>
# Vulkan on NVIDIA: Environment=GPU=--gpus all -e NVIDIA_DRIVER_CAPABILITIES=compute,utility,graphics
Environment=GPU=
ExecStartPre=-/usr/bin/docker rm -f tenzro-node
ExecStart=/bin/sh -c 'exec /usr/bin/docker run --name tenzro-node --rm --network host \
--device /dev/tpmrm0 --group-add $$TPM_GID $$GPU \
--mount type=bind,source="$$CREDENTIALS_DIRECTORY",target=/run/credentials/tenzro-node,readonly \
-e CREDENTIALS_DIRECTORY=/run/credentials/tenzro-node \
-v /srv/tenzro:/data/tenzro \
"$$IMAGE" --roles validator,storage'
ExecStop=/usr/bin/docker stop tenzro-node
Restart=on-failure
[Install]
WantedBy=multi-user.targetCreate the node's TPM keys once, with the same credential and mounts, before
the first start (systemd-run expands $$ the same way a unit does):
sudo systemd-run --pipe --wait -p User=tenzro \
-p LoadCredentialEncrypted=tenzro-device-key-root:/etc/credstore.encrypted/tenzro-device-key-root \
/bin/sh -c 'docker run --rm --network none \
--device /dev/tpmrm0 --group-add '"$(stat -c %g /dev/tpmrm0)"' \
--mount type=bind,source="$$CREDENTIALS_DIRECTORY",target=/run/credentials/tenzro-node,readonly \
-e CREDENTIALS_DIRECTORY=/run/credentials/tenzro-node \
-v /srv/tenzro:/data/tenzro \
tenzro-node:testnet-v1-<commit>-cpu tpm provision'
sudo systemctl daemon-reload
sudo systemctl enable --now tenzro-nodeCheck the hardware first
hardware needs no TPM. It prints the build, the GPUs it can drive, the ONNX
Runtime providers it will use, and whether the node would start:
docker run --rm tenzro-node:testnet-v1-cpu hardware
docker run --rm --gpus all tenzro-node:testnet-v1-cuda hardware
docker run --rm --device /dev/dri tenzro-node:testnet-v1-vulkan hardwareA Vulkan image on a host with a GPU prints build: Vulkan, gpus: 1 (or
more) and startup: ok. Set GGML_VK_VISIBLE_DEVICES to a comma-separated
list of device indices to choose GPUs; an empty value hides every GPU.
When the container stops at once
| Message | Fix |
|---|---|
no TPM: /dev/tpmrm0 is not in this container | Add --device /dev/tpmrm0. |
the TPM at /dev/tpmrm0 is not readable and writable | Add --group-add "$(stat -c %g /dev/tpmrm0)". |
the TPM derivation root needs the service credential | Start it from the service above, which provides CREDENTIALS_DIRECTORY. |
this is the Vulkan build of tenzro-node, but no usable Vulkan GPU or driver was found | Pass the GPU (--device /dev/dri, or --gpus all on NVIDIA), or run the CPU image. |
this is the CUDA build of tenzro-node, but no usable CUDA GPU or driver was found | Add --gpus all and install the NVIDIA Container Toolkit, or run the CPU image. |