Appearance
Toolbox — building without host dependencies
The repository brings its own build environment. A fresh machine needs three things on $PATH:
| Tool | Why it cannot live in the image |
|---|---|
Docker (>= 24.0) | It runs the image. The invoking user must be allowed to talk to the daemon (on Linux: member of the docker group). |
make | It is the entry point that builds and enters the image. |
git | It produced the checkout, and the Makefile reads the version stamp from it. |
Everything else — the Go toolchain pinned in /.go-version, Node from /.nvmrc, the Docker CLI with buildx, kind, kubectl, helm, kustomize, chainsaw, gh, jq, rsync, python3 — comes from the toolbox image described by tools/toolbox/Dockerfile.
shell
git clone <repository> && cd plexsphere
make build # first run builds the image, then compiles inside itHow a target runs
make <target> on the host does not run the target's recipe. It checks that the toolbox image for this checkout exists, builds it when it does not, and then runs make <target> again inside a container started by tools/toolbox/run.sh. Several goals on one command line share one container and keep their order, and variables given on the command line (make docker-build VERSION=dev) travel with them.
Inside the container the recipes are the native ones — the same bytes CI runs. The image sets PLEXSPHERE_TOOLBOX=1, and the Makefile reads that as "already inside, do not delegate again".
make help and the three toolbox-* targets always run on the host; help on a fresh machine must not cost an image build.
| Target | What it does |
|---|---|
make toolbox-image | Builds the image when this checkout's tag is missing. Every delegated target depends on it, so calling it by hand is only useful to pre-warm a machine. |
make toolbox-shell | Opens an interactive bash in the container, for ad-hoc go test ./internal/..., kubectl, or npm work. |
make toolbox-clean | Removes every plexsphere-toolbox image and both cache volumes. |
What the container sees
- The checkout, at its host path. The repository is bind-mounted at the same absolute path it has on the host, not at a fixed
/src. Targets such assqlc-generate,reuse-lint, anddocs-checkstart sibling containers through the host's Docker socket and hand them-v <checkout>:…; the host daemon resolves that path on the host, so it has to name the same directory on both sides. A linked git worktree additionally gets its shared.gitdirectory mounted. - The host's Docker daemon.
/var/run/docker.sockis mounted, somake docker-build,make dev,make e2e, and the testcontainers behindmake test-integrationtalk to the daemon on the host. There is no Docker-in-Docker; images and kind clusters created from the toolbox are ordinary host-level objects. PointTOOLBOX_DOCKER_SOCKat a different socket when the daemon does not listen on the default one. - The host network. The container runs with
--network host. kind writeshttps://127.0.0.1:<port>into the kubeconfig and testcontainers publishes database ports on localhost; both resolve only when the toolbox shares the network namespace those ports are published in. ~/.kube.make devwrites the kubeconfig and a latermake dev-downin a fresh container has to read it back, so the directory is shared with the host. Akubectlon the host sees the same contexts.- Two named volumes.
plexsphere-toolbox-gois mounted over/goand holds the module cache plus every tool a recipego installs at its pinned version (golangci-lint,zed,buf,oapi-codegen,govulncheck,actionlint,syft).plexsphere-toolbox-cacheholds the Go build cache and the npm cache. The firstmake linton a machine compilesgolangci-lint; every later run finds it in the volume. - Selected environment variables. Host variables matching
PLEXSPHERE_*,PLEXD_*,DEV_*,E2E_*,LOAD_*,SFTP_*,RELEASE_*,CHAINSAW_*, plusGITHUB_TOKEN,GH_TOKEN,NO_COLOR,TERM,VERSION,COMMIT, andDATEare forwarded. Name further ones per invocation withmake <target> TOOLBOX_ENV="FOO BAR".
On Linux the container runs as the invoking user, joined to the group that owns the Docker socket, so build output in the checkout keeps its owner. On macOS it runs as root: Docker Desktop presents the socket as root-only inside its VM and maps bind-mount ownership back to the host user on its own.
TOOLBOX_RUN_ARGS appends raw docker run arguments — for example TOOLBOX_RUN_ARGS="-v $HOME/.ssh:/home/toolbox/.ssh:ro" for make docs-publish, which needs the publishing SSH key.
Switching the toolbox off
TOOLBOX=0 runs the native recipes against the tools installed on the host — the behaviour every target had before the toolbox existed.
shell
make lint TOOLBOX=0 # one invocation
export TOOLBOX=0 # the whole shell sessionThe Makefile resolves the mode in this order:
- Inside the toolbox container the mode is always native.
- An explicit
TOOLBOX=0orTOOLBOX=1— command line or environment — wins. - With
CIset the default is native. The hosted runners install their pinned tools through setup actions and keep exercising the native recipes. - Otherwise the default is
TOOLBOX=1.
Reasons to go native:
- macOS inner loops. Bind mounts through Docker Desktop are slower than the local filesystem, and the binaries
make buildleaves inbin/are Linux executables that do not run on the Mac host. A contributor who wants a nativebin/plexctlbuilds withTOOLBOX=0. - Running
go testby hand. Several suites shell out tomakeand assert on the native recipe. Undermake testthey inherit the mode of their parent; started directly withgo teston the host they needTOOLBOX=0in the environment, otherwise the nestedmakedelegates and the assertion sees adocker runline. - Switching between modes on one checkout is safe for Go, but
node_modules/directories carry platform-specific binaries. Every npm-backed target starts withnpm ci, which replaces them, so the cost is a reinstall, not a broken tree.
The image tag
The image is tagged plexsphere-toolbox:<digest>, where the digest covers the Dockerfile and every build argument. Editing the Dockerfile, bumping a TOOLBOX_*_VERSION variable, or moving /.go-version yields a new tag, and the next make invocation rebuilds. An unchanged checkout finds its image by name and starts immediately. Old tags are not pruned automatically; make toolbox-clean removes them.
The same build also moves the plexsphere-toolbox:current tag, which is what .devcontainer/devcontainer.json references. Opening the repository in a Dev Containers–aware editor runs make toolbox-image on the host and then attaches to that image with the same volumes, socket, and host-path mount run.sh uses.
Where the versions come from
The Dockerfile declares its versions as ARGs without defaults. The Makefile is their only home:
| Build argument | Source |
|---|---|
GO_VERSION | /.go-version |
NODE_VERSION | /.nvmrc |
DOCKER_VERSION, KUBECTL_VERSION, KIND_VERSION, GH_VERSION | TOOLBOX_*_VERSION in the Makefile |
HELM_VERSION, KUSTOMIZE_VERSION, CHAINSAW_VERSION | TOOLBOX_*_VERSION in the Makefile, held equal to the versions .github/workflows/ci.yaml installs |
Release binaries fetched during the build (kind, helm, kustomize, chainsaw, gh) are verified against the checksum file their upstream publishes next to the asset. Node, the Docker CLI, and kubectl are copied out of their official images.
tests/workspace/toolbox_image_test.go fails when the Dockerfile's ARG list and the Makefile's --build-arg list differ, when an ARG gains a default, when one of the three CI-mirrored pins drifts from the workflow, when a target is added below the Makefile's closing endif # TOOLBOX, or when the dev container stops pointing at the image the Makefile builds. internal/platform/make/toolbox_mode_test.go pins the mode resolution and the shape of the delegated command line. The toolbox-smoke CI job runs make build TOOLBOX=1 on a runner without a Go toolchain — see the CI guide.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
docker not found on PATH | No container runtime. Install Docker, or use TOOLBOX=0 with host tools. |
permission denied while trying to connect to the Docker daemon socket | The invoking user may not use Docker. On Linux: sudo usermod -aG docker $USER, then log in again. |
kubectl inside the toolbox cannot reach a kind cluster on macOS | Host networking is disabled in Docker Desktop. Enable it under Settings → Resources → Network, or run the dev-stack targets with TOOLBOX=0. |
A Go test fails on a docker run … run.sh make … line it did not expect | The test was started with a bare go test on the host. Export TOOLBOX=0. |
| The image rebuilds although nothing changed | A TOOLBOX_*_VERSION override in the environment changes the tag digest. Unset it. |