NativeLink

Deploy with Docker Compose

Bring up a complete CAS, scheduler and worker on one host with the compose files in the repository: what each service is, which ports it publishes, and the four things that make this a development setup rather than a production one.

Who this is for: you want the whole system (cache and remote execution) running on one machine, either to try it or to give a small team something shared. What you'll have at the end: three containers serving a real CAS, scheduler and worker, a build pointed at them, and a clear picture of what separates this from production. Time: 20 minutes, most of it spent compiling.

Before you start

A config you wrote (see Your first full config), and a host with Docker on it.

Of the deployment shapes NativeLink ships, Compose is the one that runs end-to-end out of the box. The Kubernetes building blocks need image tags and a Gateway you supply; the Compose files need nothing but Docker.

The configuration

git clone https://github.com/TraceMachina/nativelink
cd nativelink/deployment-examples/docker-compose
docker compose up -d

The first run builds the image from source with Bazel, which takes a while; see the build is not fast below.

Three services come up:

ServiceConfig fileRolePublished ports
nativelink_local_caslocal-storage-cas.json5CAS, Action Cache, ByteStream50051 (plain), 50071 (TLS), and a 127.0.0.1:50061 mapping that nothing in the CAS config listens on
nativelink_schedulerscheduler.json5Execution, Capabilities, AC frontend; worker API50052
nativelink_executorworker.json5Runs actions. privileged: truenone

They talk to each other by service name. The worker config reaches the scheduler at grpc://${SCHEDULER_ENDPOINT:-127.0.0.1}:50061 and the CAS at grpc://${CAS_ENDPOINT:-127.0.0.1}:50051; Compose sets both variables, and NativeLink expands them at config-parse time; see CLI and environment.

Point a build at it

bazel build //... \
  --remote_cache=grpc://127.0.0.1:50051 \
  --remote_executor=grpc://127.0.0.1:50052 \
  --remote_default_exec_properties=cpu_count=2

Cache and executor are different endpoints. That is not an accident of the example: the CAS process and the scheduler process are genuinely separate here, exactly as they are in the production shape.

cpu_count is what the shipped README passes. The scheduler declares it as a minimum platform property and the worker advertises it from nproc, so an action is only matched to a worker with at least that many cores; ask for more than the worker has and the action queues silently forever. Omitting the property is fine: an action with no platform properties matches any worker. See platform properties.

What to change

Only the fields carrying a decision. Everything else in the shipped files is already correct for a single host.

FieldFileWhy you'd change it
eviction_policy.max_byteslocal-storage-cas.json5, worker.json5Ships at 10 GB for the CAS and 500 MB for the AC, plus a separate 10 GB fast tier for the worker, all under the same host directory. Size them to your disk. The worker's tier can reach roughly twice its max_bytes, because the executable-variant directory the worker creates next to content_path is not counted against it. See Runbooks.
NATIVELINK_DIRenvironmentWhere the CAS and worker caches land on the host. Defaults to ~/.cache/nativelink.
RUST_LOGenvironmentShips at warn, which hides the scheduler's INFO-level matching diagnostics. Set info the first time something does not run.
platform_propertiesworker.json5The worker's advertised capabilities. Whatever you add here must also appear in the scheduler's supported_platform_properties, or the scheduler rejects the worker's registration with Unknown platform property.
ADDITIONAL_SETUP_WORKER_CMDbuild argA bash string run inside the final image. This is the hook for installing whatever toolchain your actions need.
experimental_chunkinglocal-storage-cas.json5Enabled and pointed at a separate CHUNK_INDEX_STORE, for Bazel's --experimental_remote_cache_chunking. Remove both if you are not using that flag.

The chunk index must not be the CAS

CHUNK_INDEX_STORE holds blob-to-chunk layouts, not blobs. It must be a separate store that does not verify digests; pointing index_store at CAS_MAIN_STORE produces verification failures on every split.

The multi-worker variant

docker-compose-multi-worker.yml runs one CAS, one scheduler and three workers sharing a cas-data volume:

docker compose -f docker-compose-multi-worker.yml build
docker compose -f docker-compose-multi-worker.yml up -d

The shared volume is the entire point, and the README says so more loudly than anything else in it: all workers must share the same CAS storage path. When they do not, actions fail with

Object <digest> not found in either fast or slow store. If using multiple
workers, ensure all workers share the same CAS storage path.

which the worker reports to the scheduler as FAILED_PRECONDITION with that message intact, so that is the text your developers will see. Troubleshooting has the full path.

What makes this a development setup

Four things, all of them deliberate in the example and all of them disqualifying for production.

The executor runs privileged

nativelink_executor:
  privileged: true # So we can do use_namespaces

worker.json5 sets use_namespaces: true and use_mount_namespace: true, which need CLONE_NEWUSER/CLONE_NEWNS, hence the flag. Note what this buys and what it does not: namespaces here are a hermeticity and process-reaping mechanism, not a security boundary. Actions still run as the worker's own uid through an identity uid map, still see the host filesystem, and still have network access to every NativeLink port. A privileged container running arbitrary build actions is a lateral-movement primitive. Security hardening covers what the sandbox actually does.

The TLS certificates are expired examples

The 50071 listener loads example-do-not-use-in-prod-rootca.crt and example-do-not-use-in-prod-key.pem from the same directory. They are committed to the repository, self-signed, CN=localhost, and expired in October 2024. Every client must pass --insecure or equivalent, which is what the repository's own TLS integration test does. No certificate-generation script exists anywhere in the tree; you supply real material yourself.

Nothing is authenticated

Not a Compose limitation: open-source NativeLink has no inbound authentication on any service, in any deployment. docker-compose.yml does not publish the scheduler's port 50061 at all; the worker reaches it over the Compose network by service name. That is the only thing standing between an attacker on your network and the ability to register as a worker. Keep it that way.

The build is not fast

The Dockerfile compiles NativeLink from the repository root with Bazel, in a three-stage build, and the final image is a fresh Ubuntu with the binary copied in and curl installed. The Ubuntu base is digest-pinned and the bazelisk download is checksum-verified, so the build is reproducible, but it is a full from-source build every time the layer cache misses. For repeated use, build once and push the image to a registry your hosts can pull from.

docker-compose.yml CAS config worker config

You did it right if

docker compose ps shows all three services Up.

A bazel build with the two flags above completes. Run it twice; the second run reports a high remote cache hit count in the summary line.

docker compose logs nativelink_executor contains Worker registered with scheduler. If it instead repeats Could not connect to endpoint grpc://nativelink_scheduler:50061 roughly twice a second, the worker never reached port 50061; reconnect is a hard-coded 0.5-second retry with no backoff, so that cadence is the signature.

ls ~/.cache/nativelink shows content_path-cas growing as you build.

Troubleshooting

SymptomCauseFix
Build hangs with no action ever startingThe scheduler has no worker matching your platform properties, and a value mismatch queues silentlySet RUST_LOG=info and look for No workers matched!; see Troubleshooting
Unknown platform property at onceThe property name is not in the scheduler's supported_platform_propertiesAdd it to scheduler.json5 and restart the scheduler
Actions fail with FAILED_PRECONDITION about fast or slow storeWorkers do not share CAS storageUse the shared volume in the multi-worker file
Executor container exits at startupuse_namespaces: true without privileged: trueKeep the flag, or set both namespace options to false
TLS client refuses port 50071The committed certificate expired in 2024Use port 50051, or supply your own certificate and key
docker compose ps shows containers unhealthy but everything worksThe multi-worker healthchecks probe a port with no health serviceIgnore it, or move the healthcheck to 50061
Disk fills, then every write errorsThe 10 GB eviction policy is smaller than what the store actually consumesRunbooks

What's next

NextProduction configuration

The same three processes, configured for people who depend on them.

SidewaysMove it to real hosts

One binary per role under systemd, with a load balancer in front.

SidewaysBefore anyone else can reach it

Nothing is authenticated. Plan the network accordingly.

On this page