Skip to content

Toolbox — building without host dependencies ​

The repository brings its own build environment. A fresh machine needs three things on $PATH:

ToolWhy 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).
makeIt is the entry point that builds and enters the image.
gitIt 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 it

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

TargetWhat it does
make toolbox-imageBuilds 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-shellOpens an interactive bash in the container, for ad-hoc go test ./internal/..., kubectl, or npm work.
make toolbox-cleanRemoves 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 as sqlc-generate, reuse-lint, and docs-check start 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 .git directory mounted.
  • The host's Docker daemon. /var/run/docker.sock is mounted, so make docker-build, make dev, make e2e, and the testcontainers behind make test-integration talk 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. Point TOOLBOX_DOCKER_SOCK at a different socket when the daemon does not listen on the default one.
  • The host network. The container runs with --network host. kind writes https://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 dev writes the kubeconfig and a later make dev-down in a fresh container has to read it back, so the directory is shared with the host. A kubectl on the host sees the same contexts.
  • Two named volumes. plexsphere-toolbox-go is mounted over /go and holds the module cache plus every tool a recipe go installs at its pinned version (golangci-lint, zed, buf, oapi-codegen, govulncheck, actionlint, syft). plexsphere-toolbox-cache holds the Go build cache and the npm cache. The first make lint on a machine compiles golangci-lint; every later run finds it in the volume.
  • Selected environment variables. Host variables matching PLEXSPHERE_*, PLEXD_*, DEV_*, E2E_*, LOAD_*, SFTP_*, RELEASE_*, CHAINSAW_*, plus GITHUB_TOKEN, GH_TOKEN, NO_COLOR, TERM, VERSION, COMMIT, and DATE are forwarded. Name further ones per invocation with make <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 session

The Makefile resolves the mode in this order:

  1. Inside the toolbox container the mode is always native.
  2. An explicit TOOLBOX=0 or TOOLBOX=1 — command line or environment — wins.
  3. With CI set the default is native. The hosted runners install their pinned tools through setup actions and keep exercising the native recipes.
  4. 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 build leaves in bin/ are Linux executables that do not run on the Mac host. A contributor who wants a native bin/plexctl builds with TOOLBOX=0.
  • Running go test by hand. Several suites shell out to make and assert on the native recipe. Under make test they inherit the mode of their parent; started directly with go test on the host they need TOOLBOX=0 in the environment, otherwise the nested make delegates and the assertion sees a docker run line.
  • Switching between modes on one checkout is safe for Go, but node_modules/ directories carry platform-specific binaries. Every npm-backed target starts with npm 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 argumentSource
GO_VERSION/.go-version
NODE_VERSION/.nvmrc
DOCKER_VERSION, KUBECTL_VERSION, KIND_VERSION, GH_VERSIONTOOLBOX_*_VERSION in the Makefile
HELM_VERSION, KUSTOMIZE_VERSION, CHAINSAW_VERSIONTOOLBOX_*_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 ​

SymptomCause and fix
docker not found on PATHNo container runtime. Install Docker, or use TOOLBOX=0 with host tools.
permission denied while trying to connect to the Docker daemon socketThe 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 macOSHost 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 expectThe test was started with a bare go test on the host. Export TOOLBOX=0.
The image rebuilds although nothing changedA TOOLBOX_*_VERSION override in the environment changes the tag digest. Unset it.