A drop-in replacement for bazel that runs commands in a persistent Docker container.
Implemented as a standalone ~150-line Bash script.
There is an official Bazel Docker image, which is basically just Ubuntu with Bazel pre-installed; a nice, canonical, Linux-based build environment.
Bazel recommends using it with docker run
by bind-mounting your workspace and build output directories into the container;
an elegant, simple solution
— with a couple major drawbacks:
- You have to restart the Bazel server and rebuild the analysis cache every time you run a command. This significantly slows iteration speed.
- When you bind-mount a MacOS directory to the container's build output directory directly, you can experience mysterious build failures that seem to be caused by latency issues in Docker's filesharing system. See Output Synchronization.
This script is a mitigation of both issues.
It's an API-compatible drop-in replacement for bazel:
bazel-docker build @repo//package/...
bazel-docker test --nocache_test_results //some:thing
bazel-docker run //... # etc.Basically, the script will:
- Generate a unique name based on the path to the root of the current workspace.
- Check to see if a container with that name is already running.
- If not, start a new instance of the official Bazel container with that name.
- Execute the Bazel command in that container
(with
docker execinstead ofdocker run).
After the initial startup, the container (and the running Bazel server that holds the analysis cache in memory) will persist until manually shut down, just like a normal Bazel server running directly on the host.
Any file path within the workspace or the build output roots will be the same on the container as it is on the host. In other words, build errors will always have accurate file paths.
The script also handles relative targets,
so you can run e.g. bazel-docker build :target from a subpackage directory,
and it will be like running bazel build :target.
It also bind-mounts ${HOME}/.ssh into the container (read-only)
so that git_override works the same as it would on the host.
You can get the unique name of the build container associated with the current working directory (which can be useful for tooling purposes) with:
bazel-docker --nameThis is the only API difference between the bazel-docker script and bazel itself.
Requirements:
- Docker
- Bazel
- Bash
It's a standalone, portable Bash script. You could just do this:
sudo curl --location --fail --silent --show-error \
--output=/usr/local/bin/bazel-docker \
'https://raw.githubusercontent.com/ouillie/bazel-docker/refs/heads/main/bazel-docker' \
&& sudo chmod +x /usr/local/bin/bazel-dockerYou certainly don't have to, but feel free to read it first! It's short and well-commented, and there is a lot of room to customize various things, like whether you want to inherit any environment variables from the host system, or add any other docker flags.
Bazel can have issues when the container's output directory is bind-mounted directly to a MacOS directory using either the VirtioFS or gRPC FUSE filesharing systems. To get around this, the output directory of every build container is instead bind-mounted to a single, shared, named volume. Each container naturally only accesses a dedicated subdirectory of that volume because of how Bazel works.
That volume is also bind-mounted into a single auxiliary container called bazel-output-sync,
whose only purpose is to synchronize the files from the volume
to a bind-mounted host directory using Unison.
Introducing this layer of indirection solves the build issues,
at the cost of ~2x disk usage
and a negligible amount of latency (perhaps a second or two)
before build outputs actually become available on the host.
Try running ./is-docker-for-mac-still-broken-somehow.sh
on both Mac and Linux.
See how the build fails on Mac, but succeeds on Linux.
Props to whoever can explain why.