Skip to content

Docker (OCI) Outputs

Many real-world CI/CD pipelines don’t just produce files or directories but package entire applications into Docker (OCI) images. Building images can be time-consuming, especially for large applications, making them perfect candidates for caching.

Grog can cache Docker (OCI) images, allowing you to:

  • Skip building images when their inputs haven’t changed
  • Share images between team members using a remote cache
  • Integrate Docker builds into your dependency graph
  • Push images to registries only when needed

This guide covers how to set up Docker caching, configure storage backends, and define Docker outputs in your build targets.

Grog supports two storage backends for Docker images:

The filesystem backend stores Docker images as content-addressable blobs (manifest, config, and layers) using your configured (remote) filesystem cache:

[oci]
backend = "fs" # This is the default, so it's optional

Advantages:

  • Simple to set up
  • Works with any filesystem cache
  • No additional infrastructure required
  • Layers and manifests are deduplicated across images, reducing cache size

Disadvantages:

  • Loading still streams layers back into the Docker daemon locally

The registry backend stores Docker images in a Docker registry:

[oci]
backend = "registry"
registry_url = "https://your-registry-url"

Advantages:

  • More efficient storage through layer deduplication
  • Better performance for large images
  • Familiar workflow for Docker users

Disadvantages:

  • Requires a registry to be running and accessible
  • Requires docker to be installed on your machine
  • Needs authentication setup

When using the registry backend, Grog expects your current session to be authenticated with the registry. Here’s how to authenticate with common registries:

Once your Docker storage is set up, you can define container image outputs for your targets. The prefix is oci:: regardless of which tool builds the image:

targets:
- name: build_api_image
inputs:
- Dockerfile
- src/**/*.go
- go.mod
- go.sum
command: |
# Build the Docker image
docker build -t api-service:latest .
outputs:
# Cache the local image
- oci::api-service:latest

You can use Docker images built by one target in dependent targets:

targets:
- name: build_api_image
inputs:
- Dockerfile
- src/**/*.go
command: docker build -t api-service:latest .
outputs:
- oci::api-service:latest
- name: integration_test
dependencies:
- :build_api_image
command: |
# The api-service image is now available locally
docker run --rm api-service:latest --version
# Run tests against the image
go test -tags=integration ./tests/...

oci:: outputs are cache declarations. To also ship an image to a user-facing registry, add an oci_push block to the target and run grog build --push.

oci_push keys must match the local name of an oci:: output on the same target; values are one destination or a list for multi-destination. Tag interpolation works best in Starlark and Pkl, where you can substitute Grog’s predeclared GROG_GIT_HASH variable directly into the destination string (YAML has no built-in interpolation, so you’d have to template the file yourself).

BUILD.star
target(
name = "build_api_image",
command = "docker build -t api-service .",
inputs = ["Dockerfile", "src/**/*.go"],
outputs = ["oci::api-service"],
oci_push = {
"api-service": "us-east1-docker.pkg.dev/proj/api:" + GROG_GIT_HASH,
},
)

Then run it with:

Terminal window
grog build --push :build_api_image

Cache misses build the image, cache it, and ship it. Cache hits skip the build, restore the image, and still ship it — the destination is HEAD-probed first and the push is short-circuited when the registry already holds the same digest. --push applies transitively across the build selection, so a grog build --push //services/api invocation ships every oci_push entry on //services/api and on every dependency it reached.

The oci_push map is not part of the cache key, so bumping the tag in your destination string reuses the cached image and only re-runs the push.

By default Grog pushes over HTTPS. To allow plain HTTP destinations (local registries, internal hosts without TLS) declare them in grog.toml:

[oci]
insecure_registries = ["127.0.0.1:5000", "registry.internal"]

Entries can be host-only (matches any port) or host:port (matches that port exactly). Mirrors Docker’s own insecure-registries model. If a push fails with a TLS handshake error against an undeclared host, the error message includes a hint pointing at this config knob.

Push uses the ambient Docker keychain — Grog reads ~/.docker/config.json just like docker push does. In CI, run your registry’s normal login step (gcloud auth configure-docker, aws ecr get-login-password | docker login, docker login ghcr.io, etc.) before grog build --push.

For building multi-platform Docker images, see the Multi-platform Builds guide.