Rust compile times inside Docker are painful. Without a shared build cache, every service restart triggers a near-full cargo build — even for an unchanged crate. RustBuilder fixes this with a single shared builder container that watches your workspace, compiles incrementally with sccache, and signals each service to restart only when its binary is fresh.
Drop it into an existing Docker Compose project in minutes. No repo restructuring required.
- One
rust-buildercontainer handles allcargo build— no per-service build steps - Shared sccache + Cargo registry and git cache eliminates redundant downloads and recompilation across every container
- Redis-coordinated stamp-based restarts (filesystem fallback included)
- TOML-driven service manifest — onboard a new service in a few lines
dev/prodDockerfile split: same service definition works for hot-reload and release builds- Two-app demo workspace to try it end-to-end in under five minutes
┌─────────────────────────────────────────┐
│ Your workspace (bind-mounted) │
│ Cargo.toml shared/ services/api/ │
└──────────────────┬──────────────────────┘
│ file-change events (watchexec)
▼
┌─────────────────────────┐
│ rust-builder │ ← single shared container
│ cargo build --locked │
└────────┬────────────────┘
│ writes artifacts + stamps
┌──────────┼─────────────────┐
▼ ▼ ▼
/cargo-target /sccache Redis
shared vol shared vol (or /workspace-signals)
│ │
└──────────┬───────────────┘
│ stamp update → exec restart
┌─────┴──────┐
▼ ▼
app-a app-b
container container
- Watch —
rust_builder_daemon.pyruns watchexec over your workspace. A change to a shared crate queues a global build; a change under one service's directory queues only that service. - Build —
rust_build.pyrunscargo build --locked -p <package>serially, writing the binary into the shared/cargo-targetvolume. - Signal — After a successful build, the builder touches a restart stamp in Redis (or
/workspace-signalsin filesystem mode). - Restart —
service_entrypoint.pyinside each app container polls its stamp and exec-replaces the running binary the moment the stamp updates.
For the full configuration reference — environment variables, build strategies, and the manifest field guide — see rust-builder/README.md.
- Docker Engine 24+ or Docker Desktop
- Docker Compose v2
- macOS, Linux, or Windows via WSL2
- Rust is not required on the host
- Build the shared base image. This step must finish successfully before step 2 — every other service runs on this locally-built image:
docker compose -f docker-compose.demo.yml --profile base build rust-dev-baseThis downloads the Rust base image and compiles watchexec and sccache, so the first run takes several minutes. If it dies partway through with a network error such as failed to receive status: ... EOF, that is a transient registry hiccup — just run the command again until it completes.
- Start the demo stack:
docker compose -f docker-compose.demo.yml up -d- Check the logs:
docker compose -f docker-compose.demo.yml logs -f rust-builder app-a app-b- Try both hot-reload paths:
# app-only change — rebuilds and restarts only app-a
printf '\n// app-a test change\n' >> demo/app-a/src/main.rs
# shared/global change — rebuilds and restarts every service
printf '\n// shared test change\n' >> demo/shared/src/lib.rsThe demo uses its own Compose project name and rust-builder-starter-base:local image tag, so it will not collide with an existing local project using its own builder.
If you already have a Compose-based Rust app, you can adopt the builder without restructuring the whole project.
- Copy the
rust-builder/directory,config/rust-builder.toml, anddocker/dev-rust-base.Dockerfileinto your repository. The Dockerfile is the intended customization point — add your project's native dependencies in theapt-getblock and adjust the Rust version args as needed. - Add the
rust-dev-baseservice from docker-compose.demo.yml so the shared dev image can be built locally, pointing itsdockerfile:at wherever you placeddev-rust-base.Dockerfile. - Point each Rust app service at your own Dockerfile, but keep the runtime contract the same:
CARGO_TARGET_DIR=/cargo-targetRUST_SIGNAL_BACKEND=redisorfilesystemRUST_SIGNAL_NAMESPACEunique to your Compose project- the app command in stamp/binary form, for example
['my-app', '/cargo-target/debug/my-app']
- Mount the shared volumes into both
rust-builderand each app container:/cargo-target/usr/local/cargo/registry/usr/local/cargo/git/sccache/workspace-signalsif you use the filesystem backend
- Keep your existing non-Rust services unchanged. They do not need to know about the builder.
Recommended Dockerfile layout:
devstage: uses the Rust builder base image and the stamp-driven entrypointprodstage: standard multi-stage release build that copies the compiled binary into a slim runtime image
That way the same service definition supports both local hot-reload development and a traditional production build.
Typical adoption flow:
docker compose -f docker-compose.yml build rust-dev-base
docker compose -f docker-compose.yml up -dIf you already have a redis service, reuse it. Otherwise, add the Redis service from the demo compose file.
Drop this into your own docker-compose.yml and replace the placeholders. It shows the two things the builder needs: the shared rust-builder service and each app service pointed at its stamp/binary, both mounting the same set of volumes.
services:
redis:
image: redis:7-alpine
# One shared builder for the whole workspace.
rust-builder:
image: rust-builder-starter-base:local # built from docker/dev-rust-base.Dockerfile
working_dir: /workspace
depends_on: [redis]
environment:
CARGO_TARGET_DIR: /cargo-target
RUST_SIGNAL_BACKEND: redis
RUST_SIGNAL_REDIS_URL: redis://redis:6379/0
RUST_SIGNAL_NAMESPACE: my-project # unique per project so stacks don't collide
RUST_BUILDER_CONFIG: /workspace/config/rust-builder.toml
command: ["/usr/bin/python3", "/usr/local/bin/rust_builder_daemon.py"]
volumes: &builder-volumes
- ./:/workspace
- cargo-target:/cargo-target
- cargo-registry:/usr/local/cargo/registry
- cargo-git:/usr/local/cargo/git
- sccache:/sccache
- signals:/workspace-signals
# One entry per Rust service. The command is [stamp-name, compiled-binary-path].
my-app:
build:
context: .
dockerfile: ./my-app/Dockerfile # your service's dev/prod Dockerfile
args:
DEV_BASE_IMAGE: rust-builder-starter-base:local
depends_on: [redis]
environment:
CARGO_TARGET_DIR: /cargo-target
RUST_SIGNAL_BACKEND: redis
RUST_SIGNAL_REDIS_URL: redis://redis:6379/0
RUST_SIGNAL_NAMESPACE: my-project
command: ["my-app", "/cargo-target/debug/my-app"]
volumes: *builder-volumes # must share the builder's volumes
volumes:
cargo-target:
cargo-registry:
cargo-git:
sccache:
signals:For the fully-wired reference — build args, health checks, and the nocopy volume options tuned for Docker Desktop — see docker-compose.demo.yml.
- Add a new
[[services]]entry to config/rust-builder.toml. - Add a service container to your compose file.
- Point the container command at the stamp name and compiled binary path under
/cargo-target/debug/. - Mount the shared target, cache, and signal volumes into both the builder and the new service container.
You do not need to edit the daemon or build worker when onboarding a new service. See rust-builder/README.md for the full manifest field reference.
no such image/pull access denied for rust-builder-starter-baseonup. This does not mean you need to authenticate.rust-builder-starter-base:localis built locally by step 1, not pulled from a registry — this error means that build never completed. Re-run the step 1 build command and let it finish, thenupagain. (docker images | grep rust-builder-starter-baseconfirms whether the image exists.)- Build fails with
failed to receive status: ... EOFor another network error mid-download. Transient registry hiccup while pulling the Rust base image. Re-run the same build command; Docker resumes from the layers it already fetched. - If the builder does not restart an app, check
docker compose logs -f rust-builder. - On macOS and Windows, bind mounts may be slower; the demo defaults to a safer poll interval.
- If Redis is unavailable, fall back to filesystem signaling with
RUST_SIGNAL_BACKEND=filesystem. - If Docker Desktop reports file-descriptor pressure, lower
CARGO_BUILD_JOBS.
cargo-watch and watchexec are the most common alternatives — both trigger cargo build on file changes, but they run on the host, don't share the Cargo cache with containers, and don't coordinate restarts across multiple services. bacon is a continuous test runner, not suited to multi-container dev workflows. Docker Compose's built-in watch mode (Compose 2.22+) can sync files and trigger rebuilds, but it does not provide a shared sccache layer or a single serialized build queue across services.
| Approach | Shared build cache | Multi-container restarts | No host toolchain |
|---|---|---|---|
| RustBuilder | ✓ | ✓ | ✓ |
cargo-watch on host |
✗ | ✗ | ✗ |
watchexec on host |
✗ | ✗ | ✗ |
Docker Compose watch |
✗ | partial | ✓ |
- rust-builder/README.md: configuration reference and runtime wiring
- rust-builder/RUST_BUILDER_TESTING.md: manual validation checklist
- docker-compose.demo.yml: runnable demo stack (also the fully-wired integration reference)