Skip to main content
Production deployments often require a container image for a service or application, to be run in a Docker daemon, Kubernetes cluster, or other container-based runtime. Container images are merely archive (tar) files, containing some metadata and layers, each of which is another archive file. Container images are a great use case to be built with Bazel, which has advantages over the Dockerfile / docker build workflow. It is more reproducible, and much higher performance since individual layers can be built in parallel.

Build a container with a Go binary

The Go starter template includes support for OCI containers. When you run aspect gazelle you should see a go_image rule was automatically written in each BUILD file next to the go_binary target. Of course, it’s also possible to write this target by hand. By default, go_image uses the base image from the Distroless project, to minimize dependencies and reduce your exposure to vulnerabilities. See the oci.pull call in MODULE.bazel.
Building this target results in a container image written to bazel-out, as a directory following the OCI image layout specification:
Q. Why does it produce a folder rather than a .tar file? A. This is because container images can be large, and when a Bazel action produces a large output, it will be stored in the remote cache. This causes expensive and slow network transfer, especially when a large number of image cache entries are invalidated by some code changes.

Running a local container

We’ll load the image data into a locally running daemon, like with Docker or Podman. Make sure you have a container daemon running, and maybe set the DOCKER_HOST environment variable so that it can be located by commands like docker load. The go_image macro in our BUILD file expands to several targets, which you can inspect with bazel query. One is oci_load rule //cmd/hello:image.load which is executable, so we can load our image by running it.
This printed the image and tag (”latest”) which the daemon has stored. Starting the container then uses the docker or podman command like you’d use outside of Bazel, for example:
Navigate again to http://localhost:8080 in your browser to access the application.

Pushing to a remote registry

To run the application in a remote environment like on a Cloud service, we’ll need to run a push command. The oci_push rule creates an executable target that can be used with bazel run. See https://github.com/bazel-contrib/rules_oci/blob/main/docs/push.md However we would need credentials for a registry to perform a push, so this lesson doesn’t include that.

Continuous Delivery

Ideally our go_image should be pushed to the remote registry any time we’ve landed code changes that affect the binary. This way the developer workflow is simply to merge changes, then run a deployment command that promotes a binary built during CI. This is better than running a push command locally, because we are guaranteed that the container image is exactly the one that passed all the tests. See Selective delivery for our recommended approach.