How to Build Docker Images with Cloud Build in GCP

This page explains how to build Docker images using Cloud Build in GCP, push them to Artifact Registry, and structure your pipeline so images are consistent, traceable, and ready for automated deployment. By the end you will understand the full flow from source code to stored image, and have enough to write a working cloudbuild.yaml that handles tagging, caching, and multi-stage builds.

What Cloud Build actually does when it builds a Docker image

If you have used Docker on your laptop, you know the basic flow: write a Dockerfile, run docker build, get an image. Cloud Build does the same thing. It runs those commands on a managed machine in GCP rather than on your own computer.

Here is how the pieces fit together. You have a Dockerfile that describes how to package your application: which base image to start from, which files to copy in, which command to run. You also have a cloudbuild.yaml file that tells Cloud Build what steps to execute. One step runs docker build using that Dockerfile. Another step runs docker push to send the finished image to Artifact Registry, GCP’s managed container storage service.

When the push step completes, the image is stored in Artifact Registry with a tag (typically a short commit SHA). From that point, any tool that knows the image path — Cloud Run, GKE, Cloud Deploy — can pull and run it.

The reason teams do this in CI/CD rather than building on laptops comes down to three things: consistency (every build runs in the same clean environment), traceability (every image is tied to a specific commit), and access control (no developer needs production credentials on their personal machine to push an image).

Analogy

Think of it like sending a job to a professional print shop. You drop off the files (your source code and Dockerfile). The shop runs the job on standardised equipment in a clean environment. The finished product goes into a labelled package (an image with a SHA tag) and onto a shelf (Artifact Registry). The job record stays in the log. The same files always produce the same result, regardless of who submits them or when.

How building Docker images with Cloud Build works

Here is the flow from commit to stored image, broken into clear steps:

  1. Code is committed or manually submitted. Either a push to a branch fires a Cloud Build trigger, or you submit a build manually with gcloud builds submit. In both cases, Cloud Build receives the source code and the build config.
  2. Cloud Build reads cloudbuild.yaml. The build configuration file in the root of your repo tells Cloud Build which steps to run, which builder images to use, and in what order.
  3. Docker builds the image. The step using gcr.io/cloud-builders/docker runs docker build on a fresh Cloud Build worker. It reads your Dockerfile and assembles the image layers.
  4. The image is tagged. You pass one or more -t flags to name the image and attach tags. A typical setup applies two tags: one with the commit SHA for permanence, and one with :latest as a convenience alias and cache source.
  5. The image is pushed to Artifact Registry. A separate push step sends the image layers to the repository you specify. Cloud Build authenticates to Artifact Registry automatically within the same GCP project.
  6. Cloud Build records the build. The build appears in Cloud Build history with logs, timing, the triggering commit, and the image digest. This is your audit trail.
  7. Downstream tools can now use the image. Cloud Run, GKE, or Cloud Deploy pull the image by its tag. Because the SHA tag is immutable, you can deploy a specific version or roll back to it at any time.

A working cloudbuild.yaml example

This configuration builds a Docker image and pushes it with two tags to Artifact Registry:

steps:
  - name: 'gcr.io/cloud-builders/docker'
    args:
      - 'build'
      - '-t'
      - 'europe-west2-docker.pkg.dev/$PROJECT_ID/api/api:$SHORT_SHA'
      - '-t'
      - 'europe-west2-docker.pkg.dev/$PROJECT_ID/api/api:latest'
      - '.'

  - name: 'gcr.io/cloud-builders/docker'
    args:
      - 'push'
      - '--all-tags'
      - 'europe-west2-docker.pkg.dev/$PROJECT_ID/api/api'

images:
  - 'europe-west2-docker.pkg.dev/$PROJECT_ID/api/api:$SHORT_SHA'

A few things worth understanding about this config:

  • The builder image is gcr.io/cloud-builders/docker. This is a Docker image maintained by Google with Docker pre-installed. It is the standard choice for any step that runs a docker command.
  • Two -t flags apply two tags to the same image. Tags are just pointers to the same set of layers, so there is no extra storage cost. You get both the immutable SHA tag and the mutable :latest alias from a single build step.
  • —all-tags pushes both tags in one command rather than requiring two separate push steps.
  • The images field tells Cloud Build to associate this image with the build record. It is optional but makes the image appear in the build summary in the Cloud Console.
Analogy

A Docker image tag is like a label on a jar. The SHA tag is a serial number permanently etched into the glass — it never changes and always refers to the same contents. The latest tag is a sticky label you peel off and move to the newest jar each time you make a new batch. You need both: the serial number for audits and rollbacks, the sticky label for convenience and caching.

When to build images with Cloud Build

Cloud Build image builds fit most production and team workflows. The cases where it works especially well:

  • Deploying to Cloud Run. Cloud Run requires a container image. Building in Cloud Build and pushing to Artifact Registry is the standard path for CI/CD pipelines targeting Cloud Run.
  • GKE workloads. Kubernetes pulls images from a registry. Teams deploying to GKE typically build images in Cloud Build, push them to Artifact Registry, and reference the image tag in their Kubernetes manifests or Helm charts.
  • Standard CI pipelines. Any team that wants automated builds on every push gets that with Cloud Build triggers. No infrastructure to maintain, no build server to patch.
  • Reproducible builds. Cloud Build starts fresh on every run in a clean environment. The same source code produces the same image every time. “It worked on my machine” stops being a useful diagnostic.
  • Auditable release workflows. Cloud Build records who triggered each build, which commit was used, what steps ran, and what image was produced. That trail is useful for compliance and incident investigation.
  • Centralised team builds. Rather than each developer pushing images with their own credentials, teams route all image builds through Cloud Build. The Cloud Build service account holds the push credentials, and individual developers do not need direct write access to the production registry.
Tip

Local builds are still reasonable during active Dockerfile development. If you are iterating on a Dockerfile and want fast feedback, building locally is quicker. Just avoid pushing locally built images to production registries, where they have no audit trail and no guaranteed build environment.

Cloud Build vs other approaches

Cloud Build vs building locally

Building locally is convenient but creates problems at the team and production level. The image produced on one developer’s machine may differ from another’s if their Docker version, base image cache, or build context differs. There is no audit record of who built what or when. Pushing from a personal machine to a production registry also means that person’s credentials have direct write access to production.

Cloud Build addresses these problems by providing a consistent, controlled environment. Every build uses the same Docker version, starts from a clean workspace, and produces an entry in Cloud Build history with a full log.

Cloud Build vs external CI systems

If your team already uses GitHub Actions, GitLab CI, or another external CI system, you can build Docker images there and push to Artifact Registry. That works well and is a reasonable choice for teams with existing CI investments or multi-cloud requirements.

The practical advantage of Cloud Build for GCP-focused teams is that authentication within the same project is automatic. Cloud Build also avoids egress costs when pushing images, since traffic stays within GCP.

Note

If you are using an external CI system like GitHub Actions, avoid long-lived service account keys to authenticate to Artifact Registry. Use Workload Identity Federation instead. It lets your external CI authenticate as a GCP service account without a key file sitting in your secrets store.

Cloud Build is not inherently better than external CI for this task. It is a natural default if you are already working in GCP and want fewer moving parts.

Multi-stage Dockerfiles

A multi-stage Dockerfile separates the build environment from the runtime environment. The build stage compiles your code using all the tools it needs. The runtime stage takes only the compiled output and runs it in a minimal base image: no compiler, no build tools, no package manager.

Here is a Go example:

# Build stage — full Go toolchain
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o api-server ./cmd/api

# Runtime stage — only the compiled binary
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/api-server /api-server
EXPOSE 8080
ENTRYPOINT ["/api-server"]

The final image contains only the compiled binary and the distroless base. This matters for three concrete reasons:

  • Faster container starts and pulls. A 15 MB distroless image pulls in seconds. A 500 MB image with a full Go toolchain does not. Every cold start in Cloud Run or GKE pays that cost.
  • Smaller attack surface. Vulnerability scanners flag every package in your image. A runtime image with no shell and no build tools has far fewer findings, and far less for an attacker to use if they gain container access.
  • Cleaner separation of concerns. Build and runtime environments have different requirements. A multi-stage Dockerfile makes that distinction explicit and prevents build artifacts from creeping into production containers.
Tip

Use Google’s gcr.io/distroless base images for production containers. They contain only the runtime libraries your application needs, with no shell and no package manager. This is the standard recommendation for production Go, Java, and Node.js containers in GCP.

Layer caching

Cloud Build starts fresh on every run. There is no persistent cache between builds. When a new build starts, it has no knowledge of what the previous build downloaded or compiled. This is good for reproducibility but can make builds slow when you install many dependencies on every run.

The standard workaround is to pull a previously built image from Artifact Registry and use it as the cache source for the current build. Docker’s layer reuse means unchanged layers do not need to be rebuilt:

steps:
  # Pull the previous image to use as the build cache.
  # allowFailure: true handles the first build, when no cached image exists yet.
  - name: 'gcr.io/cloud-builders/docker'
    args: ['pull', 'europe-west2-docker.pkg.dev/$PROJECT_ID/api/api:latest']
    allowFailure: true
    id: pull-cache

  - name: 'gcr.io/cloud-builders/docker'
    args:
      - 'build'
      - '--cache-from'
      - 'europe-west2-docker.pkg.dev/$PROJECT_ID/api/api:latest'
      - '-t'
      - 'europe-west2-docker.pkg.dev/$PROJECT_ID/api/api:$SHORT_SHA'
      - '-t'
      - 'europe-west2-docker.pkg.dev/$PROJECT_ID/api/api:latest'
      - '.'
    waitFor: ['pull-cache']
Warning

The first build always runs without a cache, because no :latest image exists in the registry yet. If you forget allowFailure: true on the pull step, the pull fails with an error and the entire build exits before building anything. This is one of the most common first-run failures with this pattern.

A few other things worth knowing about caching:

  • Why :latest as the cache source? The :latest tag always points to the most recent build. Because it represents the previous state of the image, its layers are the most likely to overlap with the current build, making it a practical cache source.
  • Cache is not guaranteed. If a base image update invalidates a layer, or if dependencies change, those layers will be rebuilt regardless. Structure your Dockerfile so that stable, slow-changing layers come before frequently changing application code.

Build substitutions

Substitution variables let you use dynamic values anywhere in cloudbuild.yaml without hardcoding them. There are two kinds: built-in substitutions that Cloud Build sets automatically, and custom substitutions that you define yourself.

Built-in substitutions

  • $PROJECT_ID — the GCP project ID where the build runs
  • $SHORT_SHA — the first 7 characters of the triggering commit SHA (trigger-based builds only)
  • $BRANCH_NAME — the name of the triggering branch
  • $REPO_NAME — the name of the connected repository
  • $BUILD_ID — a unique identifier for this specific build run, available in all build types
Warning

$SHORT_SHA is only set when a build is triggered by a source push event. If you run a build manually with gcloud builds submit, $SHORT_SHA will be empty. Your image tag will be malformed and the push step will fail with a confusing path error. Use $BUILD_ID for manual builds instead — it is always populated regardless of how the build was started.

Custom substitutions

Custom substitution names must start with an underscore. They let you configure the same cloudbuild.yaml for multiple environments by passing different values from different triggers:

substitutions:
  _REGION: europe-west2
  _ENV: prod

steps:
  - name: 'gcr.io/cloud-builders/docker'
    args:
      - 'build'
      - '-t'
      - '${_REGION}-docker.pkg.dev/$PROJECT_ID/api/api:$SHORT_SHA'
      - '.'

In practice, you would have a dev trigger and a prod trigger each pointing to the same cloudbuild.yaml, passing different values for _REGION and _ENV. This avoids maintaining separate build configs per environment.

Pushing to Artifact Registry

Every Docker image in Artifact Registry lives at a path with this structure:

REGION-docker.pkg.dev/PROJECT/REPOSITORY/IMAGE:TAG

Note

Breaking down a concrete example: europe-west2-docker.pkg.dev/my-app-prod/api/api-server:abc1234 maps to region europe-west2, project my-app-prod, repository api, image name api-server, and tag abc1234. The repository and image name are separate. One repository named api can hold multiple images: api/api-server, api/worker, api/migrations. The repository is a container for related images within a project and region, not a one-to-one mapping with a single image.

Create a Docker repository before your first push:

gcloud artifacts repositories create api \
  --repository-format=docker \
  --location=europe-west2 \
  --project=my-app-prod

Grant the Cloud Build service account permission to write to the repository:

CB_SA=$(gcloud builds get-default-service-account --project=my-app-prod)

gcloud artifacts repositories add-iam-policy-binding api \
  --location=europe-west2 \
  --project=my-app-prod \
  --member="serviceAccount:${CB_SA}" \
  --role="roles/artifactregistry.writer"

Once this is in place, Cloud Build handles authentication to Artifact Registry automatically for builds within the same project. If your pipeline needs to access credentials beyond standard registry access, for example credentials for a private base image or an external service, see Secrets in CI/CD Pipelines for the recommended approach using Secret Manager.

Common beginner mistakes

  1. Tagging images with only :latest. The latest tag is mutable. Every new push overwrites it. Without a SHA tag, you have no way to roll back to a specific version or determine which code was running in production at a given point in time. Always tag with $SHORT_SHA or another immutable identifier, and use :latest only as a convenience alias alongside it.

  2. Not understanding that most tags are mutable. A common misconception is that tags automatically increment like version numbers. They do not. A tag is just a label you attach to an image. Any tag you choose (:v1.0, :latest, :stable) can be reassigned to point to a different image at any time. Only the image digest (SHA256 hash) is truly immutable. Design your tagging strategy around immutable identifiers from the beginning.

  3. Single-stage Dockerfiles for compiled languages. A single-stage Go or Java Dockerfile includes the full compiler toolchain in the final image. This adds hundreds of megabytes and many CVE findings to an image that only needs the compiled binary to run. Multi-stage builds fix this at the Dockerfile level with no extra tooling required.

  4. Confusing Artifact Registry paths. Beginners often mix up the repository name and the image name, or forget the region prefix entirely. The full format is REGION-docker.pkg.dev/PROJECT/REPO/IMAGE:TAG. If you are seeing authentication errors or image-not-found errors, verify the full path carefully before investigating anything else.

  5. Omitting allowFailure: true on the cache pull step. On the first build, no :latest image exists in the registry yet. Without this flag, the pull step fails and the entire build exits before it builds anything. Always add allowFailure: true to the pull step when using this caching pattern.

  6. Assuming cache always exists. Caching with —cache-from is best-effort. If the pull step fails for any reason — first build, deleted image, temporary registry issue — the build continues without cache. Write your Dockerfile so that stable, slow-changing layers such as dependency installation come before frequently changing application code. A partial cache hit is better than none.

  7. Mixing local credential assumptions into the pipeline. If your Dockerfile or build script references environment variables, file paths, or configuration that only exist on your local machine, the build will fail in Cloud Build. Treat Cloud Build as a completely clean environment with no knowledge of your local setup or personal credentials.

  8. Not checking service account permissions before the first run. The most common first-run failure is a permission denied error when pushing to Artifact Registry. Check that the Cloud Build service account has roles/artifactregistry.writer on the target repository before you submit the first build. The secure CI/CD pipelines page covers service account permissions in more detail.

Frequently asked questions

Do I need Docker installed locally to use Cloud Build?

No. Cloud Build runs Docker in its own managed environment. You define the build steps in a cloudbuild.yaml file and Cloud Build handles executing them. You only need Docker locally if you want to test builds on your own machine before submitting them to Cloud Build.

Should I tag images with :latest or a commit SHA?

Use both. Tag every build with the commit SHA using $SHORT_SHA for traceability and rollback capability. Add a :latest tag as a convenience alias alongside it. Never rely on :latest alone in production. It is mutable and overwrites itself on every push, giving you no way to identify which code version is actually running.

What is the difference between Artifact Registry and the image itself?

Artifact Registry is the storage service where Docker images are kept. An image is a layered archive containing your application and its dependencies. When Cloud Build pushes an image, it uploads the image layers to Artifact Registry at a path that includes the region, project, repository name, image name, and tag.

Can Cloud Build build and push images automatically after every commit?

Yes. You connect Cloud Build to a source repository and configure a trigger that fires on each push to a branch. Cloud Build then runs your cloudbuild.yaml without any manual steps.

When is $SHORT_SHA unavailable?

$SHORT_SHA is only populated in builds triggered by a source code push event. For manually triggered builds or builds triggered via Pub/Sub, $SHORT_SHA is empty. Use $BUILD_ID as a fallback unique identifier, or define a custom substitution variable for version tagging.

Last verified: 25 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.