Skip to content

MikiEEE/RustBuilder

Repository files navigation

RustBuilder — Hot-Reload for Multi-Service Rust + Docker Compose

License: MIT Rust

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-builder container handles all cargo 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/prod Dockerfile 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

How it works

  ┌─────────────────────────────────────────┐
  │  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
  1. Watchrust_builder_daemon.py runs 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.
  2. Buildrust_build.py runs cargo build --locked -p <package> serially, writing the binary into the shared /cargo-target volume.
  3. Signal — After a successful build, the builder touches a restart stamp in Redis (or /workspace-signals in filesystem mode).
  4. Restartservice_entrypoint.py inside 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.

Prerequisites

  • Docker Engine 24+ or Docker Desktop
  • Docker Compose v2
  • macOS, Linux, or Windows via WSL2
  • Rust is not required on the host

Quick Start

  1. 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-base

This 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.

  1. Start the demo stack:
docker compose -f docker-compose.demo.yml up -d
  1. Check the logs:
docker compose -f docker-compose.demo.yml logs -f rust-builder app-a app-b
  1. 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.rs

The 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.

Integrating into an existing Docker Compose Rust project

If you already have a Compose-based Rust app, you can adopt the builder without restructuring the whole project.

  1. Copy the rust-builder/ directory, config/rust-builder.toml, and docker/dev-rust-base.Dockerfile into your repository. The Dockerfile is the intended customization point — add your project's native dependencies in the apt-get block and adjust the Rust version args as needed.
  2. Add the rust-dev-base service from docker-compose.demo.yml so the shared dev image can be built locally, pointing its dockerfile: at wherever you placed dev-rust-base.Dockerfile.
  3. Point each Rust app service at your own Dockerfile, but keep the runtime contract the same:
    • CARGO_TARGET_DIR=/cargo-target
    • RUST_SIGNAL_BACKEND=redis or filesystem
    • RUST_SIGNAL_NAMESPACE unique to your Compose project
    • the app command in stamp/binary form, for example ['my-app', '/cargo-target/debug/my-app']
  4. Mount the shared volumes into both rust-builder and each app container:
    • /cargo-target
    • /usr/local/cargo/registry
    • /usr/local/cargo/git
    • /sccache
    • /workspace-signals if you use the filesystem backend
  5. Keep your existing non-Rust services unchanged. They do not need to know about the builder.

Recommended Dockerfile layout:

  • dev stage: uses the Rust builder base image and the stamp-driven entrypoint
  • prod stage: 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 -d

If you already have a redis service, reuse it. Otherwise, add the Redis service from the demo compose file.

Minimal integration skeleton

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.

Adding services

  1. Add a new [[services]] entry to config/rust-builder.toml.
  2. Add a service container to your compose file.
  3. Point the container command at the stamp name and compiled binary path under /cargo-target/debug/.
  4. 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.

Troubleshooting

  • no such image / pull access denied for rust-builder-starter-base on up. This does not mean you need to authenticate. rust-builder-starter-base:local is 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, then up again. (docker images | grep rust-builder-starter-base confirms whether the image exists.)
  • Build fails with failed to receive status: ... EOF or 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.

Compared to alternatives

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

Files

About

Rust Docker Compose starter for multi-service workspaces with shared builder containers, hot reload, and shared library rebuilds.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors