Custom Docker Images for Feature Branches and Sandbox Testing
Who is this for? This guide is for contributors developing Michelangelo AI's core platform services (API server, controller manager, worker). If you're building ML pipelines using the Michelangelo AI SDK, you don't need custom Docker images — the default sandbox images work out of the box. See the Sandbox Setup Guide for getting started.
This guide explains two ways to build and test custom images:
- Option A — Local build: build on your machine, import directly into k3d. Fast iteration loop — no GitHub push or CI required.
- Option B — CI build: push your branch, let GitHub Actions build multi-arch images and push them to GHCR, then point the sandbox at those images.
Option A — Local build (no GitHub push required)
Use this option when you want a fast feedback loop or are not ready to push your branch.
Prerequisites
- Docker with BuildKit enabled (Docker Desktop ≥ 4.x)
- A sandbox cluster (create one with
ma sandbox create— see Sandbox Setup) - The bazel-managed Go 1.24 binary (downloaded automatically on the first
bazel build):Set a shell variable for convenience:# macOS
find /private/var/tmp/_bazel_$(whoami) -name "go" -path "*/rules_go~~go_sdk~*/bin/go" 2>/dev/null | head -1
# Linux
find ~/.cache/bazel -name "go" -path "*/rules_go~~go_sdk~*/bin/go" 2>/dev/null | head -1export GOEXEC=$(find /private/var/tmp/_bazel_$(whoami) -name "go" -path "*/rules_go~~go_sdk~*/bin/go" 2>/dev/null | head -1)
# Linux: replace /private/var/tmp/_bazel_$(whoami) with ~/.cache/bazel
Step 1: Build the binaries
Run all three builds in parallel from the repo root. Set GOARCH to match your machine (arm64 for Apple Silicon, amd64 for Intel/Linux).
cd go
ARCH=arm64 # or amd64 for Intel/Linux
GOOS=linux GOARCH=$ARCH CGO_ENABLED=0 $GOEXEC build -o ../apiserver ./cmd/apiserver &
GOOS=linux GOARCH=$ARCH CGO_ENABLED=0 $GOEXEC build -o ../controllermgr ./cmd/controllermgr &
GOOS=linux GOARCH=$ARCH CGO_ENABLED=0 $GOEXEC build -o ../worker ./cmd/worker &
wait && echo "All builds done"
Why
CGO_ENABLED=0? The distroless base image has no C runtime. Disabling CGO produces a fully static binary that works without it.
Step 2: Build the Docker images
cd .. # repo root
for svc in apiserver controllermgr worker; do
docker build --platform linux/$ARCH \
-f docker/service.Dockerfile \
--build-arg BINARY_PATH=$svc \
--build-arg CONFIG_PATH=go/cmd/$svc/config \
-t ghcr.io/michelangelo-ai/$svc:local-dev \
--quiet . && echo "$svc image: OK" &
done
wait && echo "All images built"
Step 3: Delete and recreate the sandbox
cd python
ma sandbox delete
ma sandbox create --workflow cadence # or --workflow temporal
Step 4: Import images into k3d
k3d image import \
ghcr.io/michelangelo-ai/apiserver:local-dev \
ghcr.io/michelangelo-ai/controllermgr:local-dev \
ghcr.io/michelangelo-ai/worker:local-dev \
-c michelangelo-sandbox
Step 5: Point deployments at the local images
kubectl set image deployment/michelangelo-apiserver apiserver=ghcr.io/michelangelo-ai/apiserver:local-dev
kubectl set image deployment/michelangelo-controllermgr app=ghcr.io/michelangelo-ai/controllermgr:local-dev
kubectl set image deployment/michelangelo-worker app=ghcr.io/michelangelo-ai/worker:local-dev
Note:
kubectl set imageis ephemeral — runningma sandbox syncorhelm upgradewill revert the images to the Helm defaults. For a durable override, pass--setflags when creating the sandbox:ma sandbox create \
--set images.apiserver=ghcr.io/michelangelo-ai/apiserver:local-dev \
--set images.controllermgr=ghcr.io/michelangelo-ai/controllermgr:local-dev \
--set images.worker=ghcr.io/michelangelo-ai/worker:local-dev
Step 6: Verify
kubectl rollout status deployment/michelangelo-apiserver \
deployment/michelangelo-controllermgr \
deployment/michelangelo-worker
kubectl get pods -l app.kubernetes.io/instance=michelangelo
All three pods should reach Running. Confirm the image is the local build:
kubectl describe pod -l app.kubernetes.io/component=apiserver | grep Image:
Iterating on changes
After each code change, rebuild only the affected binary and image, then re-import and restart that one pod:
# Example: rebuilding only the worker
cd go && GOOS=linux GOARCH=$ARCH CGO_ENABLED=0 $GOEXEC build -o ../worker ./cmd/worker
cd ..
docker build --platform linux/$ARCH -f docker/service.Dockerfile \
--build-arg BINARY_PATH=worker \
--build-arg CONFIG_PATH=go/cmd/worker/config \
-t ghcr.io/michelangelo-ai/worker:local-dev --quiet .
k3d image import ghcr.io/michelangelo-ai/worker:local-dev -c michelangelo-sandbox
kubectl rollout restart deployment/michelangelo-worker
kubectl rollout status deployment/michelangelo-worker
Troubleshooting
- Pod stuck in
ImagePullBackOff: The defaultpullPolicyisIfNotPresent. If k3d can't find the image, make sure you rank3d image importafter building. Check withk3d image ls -c michelangelo-sandbox | grep local-dev. exec format error: Architecture mismatch — you built forarm64but the k3d node isamd64(or vice versa). Rebuild with the correctARCH.- Bazel Go binary not found: Run any
./tools/bazel buildtarget first to trigger the SDK download, then re-run thefindcommand.
Cleanup
Remove the locally built binaries from the repo root:
rm -f apiserver controllermgr worker
To tear down the sandbox:
ma sandbox delete
Option B — CI build via GitHub Actions
Use this option when your branch is ready to share, you need multi-arch images (linux/amd64 + linux/arm64), or you want a stable image tag to share with teammates.
Prerequisites
- Push access to the repository (or a fork with GitHub Actions enabled)
- GHCR pull access from your k3d cluster (public images work automatically)
1) Create or switch to your feature branch
git checkout -b my-feature-branch
# or
git checkout my-feature-branch
2) Update the dev release workflow to build images from your branch
Edit .github/workflows/dev-release.yml and set the on.push.branches list to your branch name:
on:
workflow_dispatch:
push:
branches: [ my-feature-branch ]
- The workflow builds multi-arch images for these services via a matrix:
controllermgr,worker, andapiserver. - Images are pushed to
ghcr.io/michelangelo-ai/<service>and tagged automatically, including a tag matching your branch name (viatype=ref,event=branch).
3) Commit changes and push your branch to trigger the build
git add .github/workflows/dev-release.yml
git commit -m "Enable dev release for my-feature-branch"
git push origin $(git branch --show-current)
Caution: Only use
git push -f(force push) if you intentionally need to overwrite remote history. In most cases, a regulargit pushis sufficient and safer.
Important: Don't merge the
dev-release.ymlchange intomain— it's only for your branch. Revert it before opening a PR, or exclude it from your PR's commits.
4) Wait for images to be published
- Monitor the GitHub Actions run for
Dev Releaseon your branch. - Upon success, images will be available as:
ghcr.io/michelangelo-ai/apiserver:my-feature-branchghcr.io/michelangelo-ai/controllermgr:my-feature-branchghcr.io/michelangelo-ai/worker:my-feature-branch
5) Point the sandbox at your branch images
The sandbox deploys core services via the Helm chart at helm/michelangelo/. Override the image tags using --set when creating the sandbox:
cd python
ma sandbox create \
--set images.apiserver=ghcr.io/michelangelo-ai/apiserver:my-feature-branch \
--set images.controllermgr=ghcr.io/michelangelo-ai/controllermgr:my-feature-branch \
--set images.worker=ghcr.io/michelangelo-ai/worker:my-feature-branch
Or, if the sandbox is already running, import and swap the images:
kubectl set image deployment/michelangelo-apiserver apiserver=ghcr.io/michelangelo-ai/apiserver:my-feature-branch
kubectl set image deployment/michelangelo-controllermgr app=ghcr.io/michelangelo-ai/controllermgr:my-feature-branch
kubectl set image deployment/michelangelo-worker app=ghcr.io/michelangelo-ai/worker:my-feature-branch
6) Verify deployment
- Ensure the pods for
apiserver,controllermgr, andworkerare running. - Confirm they are using your branch image tags via
kubectl describe pod <pod-name> | grep Image:. - Exercise your changes via the sandbox workflows or APIs as needed.
Troubleshooting
- Builds not triggering: Confirm
.github/workflows/dev-release.ymlincludes your branch underon.push.branchesand that you pushed to the exact branch name. - Image pull errors: Ensure the action completed successfully and images exist at
ghcr.io/michelangelo-ai. If private, verify permissions for your cluster's image puller. - Wrong image tag: Double-check your
--setflags reference the exact branch name. - Multi-arch issues: The workflow builds
linux/amd64andlinux/arm64. Confirm your cluster nodes match one of these.
Cleanup
ma sandbox delete
Next Steps
- Sandbox Setup Guide — create and configure the local development environment
- Building from Source — full build instructions for all components
- Helm Chart Reference —
values.yamlreference includingimages.*overrides