Why Docker produces the images
Unkey uses Docker for image production for three reasons:- Plain Dockerfiles. Local dev, image loading, and releases use Docker files.
- Multi-arch releases. The release workflow uses Docker Buildx to publish
linux/amd64andlinux/arm64variants. - Simple local builds. Tilt compiles binaries on the host for fast incremental rebuilds. Dashboard Compose compiles its services once in a shared Docker build stage.
What ships in a release image
Every release service image is the same shape:- A single static Go binary at
/unkey. No libc, no shared libraries, no init scripts. The image entrypoint is the binary path directly, not a shell invocation. - Distroless base. No shell, no busybox, no package manager. Attack
surface is minimal and the image contains exactly what the service needs to
run. This is also why container probes have to be HTTP-based; there is no
shto exec. - Multi-arch manifest. Each release publishes an OCI index referencing
amd64 and arm64 variants built in parallel from the same Go source.
docker pullresolves to the right architecture automatically, so deployments don’t care whether the cluster is x86 or Graviton.
Where things live
Build configuration is split by concern:build/<service>/main.gois the entrypoint. It wires a TOML config command viabuild/utiland calls into the service’sRunfunction. Runtime behavior (instance IDs, clocks, TLS loading, normalization) stays insvc/.../run.go. The entrypoint is intentionally thin so it does not become a second service runtime.dev/Dockerfilecompiles the services required by Dashboard Compose in one shared stage, then packages each binary in a named busybox stage.Dockerfile.releaseowns release image packaging. It copies the prebuilt binary that GoReleaser produces for each platform. The image carries only the repository source label..goreleaser.service.yamlbuilds release binaries and publishes multi-arch images withDockerfile.release.dev/Tiltfilecompiles each service on the host with the local Go build cache, then packages the binary withdev/Dockerfile.binary. The image is built once per service; after that, Tilt’slive_updatesyncs the rebuilt binary into the running container and restarts the process in place, so a code change never rebuilds an image or rolls a pod.web/apps/dashboard/dev/docker-compose.yamlselects the service stages fromdev/Dockerfile.
Local images
Tilt and the dashboarddocker-compose setup both produce local
unkey/<service>:dev tags:
Local development images use a busybox base. Tilt needs
sh, date, and tar
for live_update, while Dashboard Compose uses the same base for consistency.
Release images use distroless.
Adding a new service image
- Add
build/<service>/main.gowith a call toutil.RunServiceCommand. - Add the service’s tag pattern to
.depot/workflows/service-release.yamlso the release workflow picks up<service>/vx.y.zpushes. - Add the service to
.goreleaser.service.yaml. - Wire it into local dev: a
go_service_imagecall indev/Tiltfile, and any compose file that needs it.
build/<service>/main.go.
