Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

TagMeaning
7.0.0The image built for the v7.0.0 release; Docker version tags omit the v.
latestUpdated by stable release publication; also selected when the image reference omits a tag.
7.1.0-alpha.0One exact pre-release. Release candidates (-rc.N) are cut from main; other pre-releases come from the next branch.
edgeThe 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 pathPurpose
/usr/local/bin/zebradNode executable.
/usr/local/bin/entrypoint.shStartup script.
/home/zebra/.cache/zebraDefault state-cache, peer-cache, and RPC-cookie directory.
/home/zebra/.cache/zebra/.cookieRPC authentication file, created only when RPC and cookie authentication are enabled.
/home/zebra/.config/zebrad.tomlOptional 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.

VariableDefault or behavior
HOMEImage default /home/zebra; supplies the fallback cache and cookie paths.
UIDImage default 10001; target owner and execution UID. Changing it at runtime does not create a corresponding account for setpriv --init-groups.
GIDImage default 10001; target owner and execution GID. Prefer changing account IDs at build time.
ZEBRA_STATE__CACHE_DIRWhen unset or empty, directory preparation uses $HOME/.cache/zebra.
ZEBRA_RPC__COOKIE_DIRWhen unset or empty, directory preparation uses $HOME/.cache/zebra, independently of the state-cache setting.
ZEBRA_TRACING__LOG_FILEUnset by default. When nonempty, prepares the file’s parent directory.
ZEBRA_ZCASHD_COMPAT__ZCASHD_DATADIRUnset 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_PATHUnset by default; see mounted configuration.
ZCASHD_COMPAT_ENABLEDIgnored. Values true, TRUE, 1, yes, YES, on, and ON print a removal warning; other values have no effect.
NEXTEST_PROFILEUnset by default. In test mode, selects cargo nextest run instead of the supplied command, except for an explicit zebrad command.
NEXTEST_FILTERUnset by default. Adds --filter-expr in nextest mode; required for profiles ci-stateful and ci-e2e.
FEATURESImage default default-release-binaries; passed to nextest in test mode. Setting it on a runtime container cannot change compiled features.
SHORT_SHAPreserved 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:

TargetContents
depsRust toolchain and build dependencies on Debian Trixie.
testsSource tree, Cargo, nextest, prebuilt release tests, dynamically linked RocksDB, and the lightwalletd v0.5.3 executable; defaults to entrypoint.sh test cargo test.
releaseBuilds only the zebrad release executable for the runtime stage.
runtimeDebian 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:

ArgumentDefaultPurpose
FEATURESdefault-release-binariesFeatures passed to Cargo.
RUST_VERSION1.91.0Rust base-image tag; the FROM instruction also pins a digest, which must match any replacement toolchain image.
UID10001Created user’s numeric ID.
GIDThe UID argumentCreated group’s numeric ID.
USERzebraAccount name.
HOME/home/${USER}Home and working directory.
CARGO_HOME${HOME}/.cargoCargo dependency and tool cache.
CARGO_TARGET_DIR${HOME}/targetBuild artifacts.
CARGO_INCREMENTALUnset; the build sets 0Cargo incremental compilation.
SOURCE_DATE_EPOCH0Build timestamp input; publishing workflows supply the source commit timestamp.
SHORT_SHAUnsetCommit identifier embedded in the build; retained at runtime in test images.
SKIP_IPV6_TESTSUnset; the test image sets 1Test-image environment setting.
BUILDPLATFORMSupplied by BuildKitSelects 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.

FilePurpose and differences
docker/docker-compose.ymlSingle 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.ymlZebra 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.ymlBuilds 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.ymlBuilds the tests target, loads docker/test.env, and mounts Zebra and lightwalletd caches.
docker/mining/docker-compose.ymlMining-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
ServiceHost endpoint after loopback restriction
Grafanahttp://127.0.0.1:3000
Prometheushttp://127.0.0.1:9094 (container port 9090)
Jaeger UIhttp://127.0.0.1:16686
Alertmanagerhttp://127.0.0.1:9093
Jaeger trace receiversgRPC on 127.0.0.1:4317; HTTP on 127.0.0.1:4318.
Jaeger span metricshttp://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.