Zebra with Docker
The Zebra runtime image packages zebrad and its startup script. It includes
no configuration file, including docker/default-zebra-config.toml, no chain
database, and no zcashd. Containerized nodes have the same
resource requirements as native nodes.
Images and tags
Releases and pre-releases publish to Docker Hub
as zfnd/zebra; the same tag selects linux/amd64 or linux/arm64.
| Tag | Meaning |
|---|---|
7.0.0 | The image built for the v7.0.0 release; Docker version tags omit the v. |
latest | Updated by stable release publication; also selected when the image reference omits a tag. |
7.1.0-alpha.0 | One exact pre-release. Release candidates (-rc.N) are cut from main; other pre-releases come from the next branch. |
edge | The most recent pre-release from next; may not exist. Never a release candidate. |
Release builds do not publish commit-SHA tags. Pre-releases never move
latest, and publishing workflows do not maintain moving beta or rc tags.
Use a version tag for an explicit upgrade policy, or an image digest to pin exact image content. Pulling a newer image does not replace a running container; recreate the container with its existing state volume.
Non-release image publishing belongs to continuous delivery.
Quick start
Run a Mainnet node with a persistent named volume:
docker run -d \
--name zebra \
--restart unless-stopped \
-p 8233:8233 \
-v zebrad-cache:/home/zebra/.cache/zebra \
zfnd/zebra:7.0.0
To start from existing state, populate the volume from a state snapshot before starting Zebra.
Use a separate volume and port for a Testnet node:
docker run -d \
--name zebra-testnet \
--restart unless-stopped \
-e ZEBRA_NETWORK__NETWORK=Testnet \
-e 'ZEBRA_NETWORK__LISTEN_ADDR=[::]:18233' \
-p 18233:18233 \
-v zebrad-testnet-cache:/home/zebra/.cache/zebra \
zfnd/zebra:7.0.0
Inspect startup and synchronization logs with docker logs -f zebra. Stop
Zebra without Docker’s forced-kill deadline before replacing its container or
restoring state:
docker stop --timeout -1 zebra
Docker’s default Linux stop timeout is ten seconds; use -1
to allow Zebra’s shutdown to finish. Stop and remove
the old container before reusing its name and state volume. Never run two
Zebra processes against the same state database.
Paths and permissions
The image’s zebra account has UID and GID 10001 and home /home/zebra:
| Container path | Purpose |
|---|---|
/usr/local/bin/zebrad | Node executable. |
/usr/local/bin/entrypoint.sh | Startup script. |
/home/zebra/.cache/zebra | Default state-cache, peer-cache, and RPC-cookie directory. |
/home/zebra/.cache/zebra/.cookie | RPC authentication file, created only when RPC and cookie authentication are enabled. |
/home/zebra/.config/zebrad.toml | Optional configuration file discovered by Zebra. |
The entrypoint starts as root, prepares writable directories, then executes
Zebra through setpriv --reuid=10001 --regid=10001 --init-groups.
Before dropping privileges, the entrypoint recursively changes ownership of
the state-cache directory, cookie directory, and configured log-file directory.
It also changes ownership of each immediate parent unless that parent is /
or $HOME. Bind mounts therefore change ownership on the host, not just
inside the container. Mount dedicated directories, not a shared home or log
tree. Recursive ownership changes also traverse an existing state database on
every startup.
Mount cache directories read-write. Mount configuration files read-only and
make them readable by UID 10001; the entrypoint does not repair config-file
permissions. Paths configured only in TOML must already be writable by the
container user because directory preparation reads environment variables, not
the TOML file.
The Docker --user option bypasses root startup but does not skip the
entrypoint’s ownership operations. It can therefore fail on mounts whose
ownership the selected user cannot change. To bypass preparation deliberately,
use both --user 10001:10001 and --entrypoint /usr/local/bin/zebrad, and
prepare every writable mount beforehand. This also bypasses CONFIG_FILE_PATH;
pass --config directly when using that override.
Configuring Zebra
Use Zebra’s environment variables or mount a TOML configuration.
Mounted configuration
Generate a configuration on the host:
docker run --rm zfnd/zebra:7.0.0 zebrad generate > zebrad.toml
Edit the fields you need, then mount the file at the default discovery path:
docker run -d \
--name zebra \
-p 8233:8233 \
-v zebrad-cache:/home/zebra/.cache/zebra \
--mount type=bind,src="$PWD/zebrad.toml",dst=/home/zebra/.config/zebrad.toml,readonly \
zfnd/zebra:7.0.0
For a different path, mount the file there and set CONFIG_FILE_PATH to its
container path. The entrypoint supplies --config only when that path is an
existing regular file. A missing path does not cause the entrypoint to fail;
Zebra falls back to normal configuration discovery. To require a particular
file, pass zebrad --config /path/in/container/zebrad.toml start after the
image name instead.
Include zebrad before CLI options or subcommands in Docker commands. The
entrypoint removes its first argument when that argument starts with -, so
passing a bare --config or --help after the image name loses that option.
Entrypoint environment variables
The entrypoint interprets these variables; other settings go to Zebra’s configuration loader.
| Variable | Default or behavior |
|---|---|
HOME | Image default /home/zebra; supplies the fallback cache and cookie paths. |
UID | Image default 10001; target owner and execution UID. Changing it at runtime does not create a corresponding account for setpriv --init-groups. |
GID | Image default 10001; target owner and execution GID. Prefer changing account IDs at build time. |
ZEBRA_STATE__CACHE_DIR | When unset or empty, directory preparation uses $HOME/.cache/zebra. |
ZEBRA_RPC__COOKIE_DIR | When unset or empty, directory preparation uses $HOME/.cache/zebra, independently of the state-cache setting. |
ZEBRA_TRACING__LOG_FILE | Unset by default. When nonempty, prepares the file’s parent directory. |
ZEBRA_ZCASHD_COMPAT__ZCASHD_DATADIR | Unset by default. When nonempty, creates the directory and changes only its top-level ownership, not the existing database tree. A failed ownership change warns rather than aborting. |
CONFIG_FILE_PATH | Unset by default; see mounted configuration. |
ZCASHD_COMPAT_ENABLED | Ignored. Values true, TRUE, 1, yes, YES, on, and ON print a removal warning; other values have no effect. |
NEXTEST_PROFILE | Unset by default. In test mode, selects cargo nextest run instead of the supplied command, except for an explicit zebrad command. |
NEXTEST_FILTER | Unset by default. Adds --filter-expr in nextest mode; required for profiles ci-stateful and ci-e2e. |
FEATURES | Image default default-release-binaries; passed to nextest in test mode. Setting it on a runtime container cannot change compiled features. |
SHORT_SHA | Preserved in test images from the build argument. The entrypoint unsets an empty value in test mode to avoid unnecessary Cargo rebuilds. |
Use docker/.env for node configuration examples and docker/test.env for
test settings. ZEBRA_SECTION__KEY variables override values in mounted
configuration files; unknown ZEBRA_* configuration fields fail loading.
RPC
Enable RPC on the container’s network interface so Docker forwarding or other containers can reach it. Publish it only on host loopback, never on a public interface:
docker run -d \
--name zebra \
-p 8233:8233 \
-p 127.0.0.1:8232:8232 \
-e ZEBRA_RPC__LISTEN_ADDR=0.0.0.0:8232 \
-v zebrad-cache:/home/zebra/.cache/zebra \
zfnd/zebra:7.0.0
For clients on the same private Docker network, omit the RPC -p mapping
entirely and connect to Zebra’s service name and container port. Container
loopback (127.0.0.1) is not the host loopback or another container’s loopback.
Use cookie authentication; the container’s default cookie path is listed above.
Make an authenticated request inside the container without publishing RPC:
docker exec --user 10001:10001 zebra sh -c '
curl --fail --silent --show-error \
--user "$(cat /home/zebra/.cache/zebra/.cookie)" \
--header "Content-Type: application/json" \
--data '\''{"jsonrpc":"2.0","id":1,"method":"getblockchaininfo","params":[]}'\'' \
http://127.0.0.1:8232
'
For a client in another container, mount a dedicated ZEBRA_RPC__COOKIE_DIR
read-only and give its UID permission to read the cookie. This avoids sharing
the state database. Share the directory, not a copied credential, so the
client can reload the cookie after Zebra restarts.
Set ZEBRA_RPC__ENABLE_COOKIE_AUTH=false only for a client that cannot use
cookie authentication, on an isolated network with trusted clients. Cookie
authentication is not a reason to publish RPC publicly.
Health endpoints
The runtime image includes curl but no Docker HEALTHCHECK. To enable
health checks, add these fields to the Zebra service in a Compose file:
environment:
- ZEBRA_HEALTH__LISTEN_ADDR=127.0.0.1:8080
healthcheck:
test: ["CMD", "curl", "--fail", "--silent", "http://127.0.0.1:8080/healthy"]
interval: 30s
timeout: 10s
retries: 3
start_period: 90s
Use /ready to gate dependent traffic, not to require a new node
to finish synchronizing during startup. Health checks need no RPC or cookie.
For probes outside the container, bind to 0.0.0.0:8080 and restrict exposure
to the probe network.
P2P networking
Without a published P2P port, a bridge-networked container synchronizes through outbound connections but cannot receive unsolicited connections from outside its Docker network.
When a firewall, network address translation (NAT), or load balancer changes
the externally reachable address, configure [network] external_addr with
the public IP and mapped port. For example, add
-e ZEBRA_NETWORK__EXTERNAL_ADDR=203.0.113.42:8233, replacing the documentation
address with your node’s actual address. Open the corresponding P2P port in
the firewall. Publishing a port does not enable its listener.
Custom images
Build from the repository root using the multi-stage Dockerfile:
git clone --depth 1 --branch v7.0.0 https://github.com/ZcashFoundation/zebra.git
cd zebra
docker build --file docker/Dockerfile --target runtime --tag zebra:local .
The build uses BuildKit bind and cache mounts. Select runtime for a node;
other stages serve the build and test pipeline:
| Target | Contents |
|---|---|
deps | Rust toolchain and build dependencies on Debian Trixie. |
tests | Source tree, Cargo, nextest, prebuilt release tests, dynamically linked RocksDB, and the lightwalletd v0.5.3 executable; defaults to entrypoint.sh test cargo test. |
release | Builds only the zebrad release executable for the runtime stage. |
runtime | Debian 13 (trixie-slim), zebrad, curl, and the entrypoint; the final/default stage. |
Building with custom features
The FEATURES build argument adds Cargo features.
The Dockerfile does not pass --no-default-features, so an empty argument does
not disable the default feature set.
For example, add spend indexing with the indexer feature:
docker build \
--file docker/Dockerfile \
--target runtime \
--build-arg FEATURES="default-release-binaries indexer" \
--tag zebra:custom-features \
.
The Dockerfile declares these build arguments:
| Argument | Default | Purpose |
|---|---|---|
FEATURES | default-release-binaries | Features passed to Cargo. |
RUST_VERSION | 1.91.0 | Rust base-image tag; the FROM instruction also pins a digest, which must match any replacement toolchain image. |
UID | 10001 | Created user’s numeric ID. |
GID | The UID argument | Created group’s numeric ID. |
USER | zebra | Account name. |
HOME | /home/${USER} | Home and working directory. |
CARGO_HOME | ${HOME}/.cargo | Cargo dependency and tool cache. |
CARGO_TARGET_DIR | ${HOME}/target | Build artifacts. |
CARGO_INCREMENTAL | Unset; the build sets 0 | Cargo incremental compilation. |
SOURCE_DATE_EPOCH | 0 | Build timestamp input; publishing workflows supply the source commit timestamp. |
SHORT_SHA | Unset | Commit identifier embedded in the build; retained at runtime in test images. |
SKIP_IPV6_TESTS | Unset; the test image sets 1 | Test-image environment setting. |
BUILDPLATFORM | Supplied by BuildKit | Selects the nextest download architecture in the test stage. |
To use different filesystem IDs, build with both --build-arg UID=... and
--build-arg GID=.... Update mount permissions and any hard-coded Compose paths
when changing the account or home directory.
zcashd-compat image removal
Zebra 7.0.0 removes the runtime-zcashd-compat Docker target and its
ZCASHD_COMPAT_ARCHIVE_URL and ZCASHD_COMPAT_ARCHIVE_SHA256 build arguments.
The runtime image no longer bundles zcashd or zcash-cli.
Use zcashd-compat mode with a separately supplied zcashd process and configuration; the image no longer fills in sidecar configuration.
Compose deployments
Run these commands from the repository root. Before running examples concurrently, give each unique container names, port mappings, and state volumes.
| File | Purpose and differences |
|---|---|
docker/docker-compose.yml | Single Mainnet node using zfnd/zebra (latest), docker/.env, the mounted docker/default-zebra-config.toml, and a persistent cache volume. Publishes P2P port 8233; pins linux/amd64. |
docker/docker-compose.lwd.yml | Zebra plus lightwalletd, with separate state volumes. Enables unauthenticated RPC and plaintext lightwalletd endpoints; pins both services to linux/amd64. Requires the network restrictions below. |
docker/docker-compose.observability.yml | Builds the runtime image locally and adds Prometheus, Grafana, Jaeger, and Alertmanager. Enables metrics, tracing, and unauthenticated RPC. Requires the network restrictions below. |
docker/docker-compose.test.yml | Builds the tests target, loads docker/test.env, and mounts Zebra and lightwalletd caches. |
docker/mining/docker-compose.yml | Mining-pool example with Zebra, Redis, S-NOMP, and optional nheqminer under the miner profile. Defaults to Testnet and keeps RPC and Zebra’s health listener internal. |
For the base node, pin the service’s image to zfnd/zebra:7.0.0 if a moving
latest tag is not appropriate. Remove or change its platform: linux/amd64
setting to select the ARM64 runtime image on an ARM64 host. Then start it:
docker compose -f docker/docker-compose.yml up -d
To switch the base Compose file to Testnet, change the network, its explicitly configured listen address, and the published P2P port.
Running Zebra with lightwalletd
The supplied example disables Zebra’s cookie authentication but does not
publish RPC to the host. Lightwalletd connects to zebra:8232 over the Compose
network. Keep that network limited to trusted containers.
The example also runs lightwalletd with --no-tls-very-insecure. For local
testing, restrict its port mappings to 127.0.0.1:9067:9067 (gRPC) and
127.0.0.1:9068:9068 (HTTP). Do not expose these plaintext endpoints as a
production service. Configure TLS and access controls for an externally
accessible deployment; see lightwalletd.
After making these changes, start the example:
docker compose -f docker/docker-compose.lwd.yml up -d
The dependency condition waits for the Zebra container to start, not for
Zebra to synchronize. This file does not load docker/.env; put overrides
in its service environment settings or explicitly add an env_file.
Running Zebra with Prometheus and Grafana
The observability example exports metrics to Prometheus and traces to Jaeger.
It sets ZEBRA_TRACING__OPENTELEMETRY_ENDPOINT=http://jaeger:4318, service name
zebra, and sample percentage 100. Reduce
ZEBRA_TRACING__OPENTELEMETRY_SAMPLE_PERCENT when full trace collection is not
required; see OpenTelemetry export.
Zebra’s RPC and metrics ports are not published to the host; Prometheus
scrapes zebra:9999 over the Compose network. Restrict the remaining non-P2P
published ports to host loopback by prefixing each mapping with 127.0.0.1:.
These include the monitoring interfaces, Jaeger’s receivers, and Jaeger’s
span-metrics endpoint. For example, replace 9094:9090 with
127.0.0.1:9094:9090. Keep RPC disabled altogether if no trusted client needs
it by removing ZEBRA_RPC__LISTEN_ADDR.
Export GF_SECURITY_ADMIN_PASSWORD with a strong, unique password in the
shell running Compose. An unset or empty value prevents startup. Then build
and start the stack:
docker compose -f docker/docker-compose.observability.yml up -d --build
| Service | Host endpoint after loopback restriction |
|---|---|
| Grafana | http://127.0.0.1:3000 |
| Prometheus | http://127.0.0.1:9094 (container port 9090) |
| Jaeger UI | http://127.0.0.1:16686 |
| Alertmanager | http://127.0.0.1:9093 |
| Jaeger trace receivers | gRPC on 127.0.0.1:4317; HTTP on 127.0.0.1:4318. |
| Jaeger span metrics | http://127.0.0.1:8889/metrics |
Grafana provisions the supplied dashboards and Prometheus/Jaeger data
sources. Log in as admin with the password supplied through
GF_SECURITY_ADMIN_PASSWORD. This initializes new Grafana volumes only;
change existing account passwords in Grafana. Prometheus’s admin and
lifecycle APIs are disabled. The supplied Jaeger configuration stores traces
in memory, capped at 100000 traces. Alertmanager defines receiver names but
no notification destinations; configure receivers before relying on alerts.
The configuration and dashboard files are under
docker/observability/.
Running CI tests locally
The test image contains the source tree and nextest. Configure test selection
in docker/test.env, or pass overrides when starting the container. Select
the ci profile to run the current unit and integration test set:
docker compose -f docker/docker-compose.test.yml build
docker compose -f docker/docker-compose.test.yml run --rm \
-e NEXTEST_PROFILE=ci zebra
The entrypoint runs nextest with --locked --release, the image’s FEATURES,
and --run-ignored=all. The ci profile excludes stateful and end-to-end
tests through its default filter. Select ci-stateful or ci-e2e with a
nonempty NEXTEST_FILTER expression for those suites, and supply their
required cached state. See the
nextest profiles
and continuous integration.
Without NEXTEST_PROFILE, the test image executes its command, which defaults
to cargo test; this does not use the prebuilt release-test configuration.
The runtime image does not contain Cargo or test binaries.
Mining example limitations
The mining example requires a reward address in docker/mining/.env.
S-NOMP waits for Zebra’s internal /healthy endpoint before starting, but
mining still requires a synchronized node. See
Mining with Docker for configuration and limitations.