Skip to content

Module 2.6: GCP Artifact Registry

Complexity: [MEDIUM] | Time to Complete: 1h | Prerequisites: Module 2.1 (IAM & Resource Hierarchy)

This module sits at the intersection of security, developer experience, and cost management because every container deployment depends on a registry that teams often treat as invisible infrastructure until it breaks a build, blocks a deploy, or leaks an artifact.

After completing this module, you will be able to:

  • Configure Artifact Registry repositories for container images, language packages, and OS packages
  • Implement vulnerability scanning policies and understand Binary Authorization integration
  • Configure remote and virtual repositories for upstream caching and package aggregation
  • Secure Artifact Registry with IAM policies scoped to specific repositories

Hypothetical scenario: A widely used npm package in your dependency tree is compromised when an attacker publishes malicious versions to the public registry. Any CI/CD pipeline that runs npm install directly against the public npm registry during the exposure window can pull compromised code into production builds. Companies that pull packages directly from the public npm registry have no defense. Companies that use a private registry with upstream caching have a critical advantage: in many cases, the malicious versions are not cached because their pipelines pull the cached (clean) versions already stored in the private registry.

This incident represents a growing threat: supply chain attacks targeting public package registries. Whether you are pulling container images from Docker Hub, npm packages from the npm registry, or Python packages from PyPI, you are trusting code written by strangers. Artifact Registry is GCP’s managed solution for storing, managing, and securing software artifacts. It replaces the older Container Registry (GCR) and extends beyond Docker images to support npm, Maven, Python, Go, and OS packages.

In this module, you will learn how to create Artifact Registry repositories, push and pull container images, configure vulnerability scanning, set up IAM-based access control, and use remote repositories as upstream caches for public registries.

The Package Warehouse Analogy

Think of Artifact Registry as a secure warehouse for every kind of software artifact your organization ships. Container images sit on one shelf, npm packages on another, Maven JARs in climate-controlled storage, and Debian packages in labeled bins. Remote repositories are like a purchasing department that caches deliveries from external suppliers so your production line never waits on a third-party truck. Virtual repositories are the single loading dock where developers arrive with one badge swipe instead of hunting for the right aisle. IAM roles are the keycards that grant read-only access to floor workers, write access to the receiving dock, and admin access to the warehouse manager.


Container Registry (GCR) was GCP’s original image registry, built on top of Cloud Storage buckets. Artifact Registry is its successor with significant improvements. Understanding the migration path matters because many production pipelines still reference gcr.io URLs in Kubernetes manifests, Cloud Build configs, and Terraform modules written years ago.

Google deprecated Container Registry on May 15, 2023 and shut down write access on March 18, 2025. After the shutdown timeline completed, images stored in legacy GCR buckets became inaccessible unless you had already migrated. The good news is that Artifact Registry can host gcr.io repositories that transparently serve the same URLs your existing tooling expects, so a Kubernetes deployment referencing gcr.io/my-project/my-app:v1 continues to work after migration without rewriting every manifest on day one.

For new work, Google recommends pkg.dev URLs (REGION-docker.pkg.dev/PROJECT/REPO/IMAGE:TAG) because they support regional placement, per-repository IAM, remote/virtual repository modes, and cleanup policies that the legacy multi-regional GCR hostnames cannot offer. The automatic migration tool (gcloud artifacts docker upgrade migrate) creates Artifact Registry repositories for each GCR host in your project and redirects traffic, which is the fastest path for teams that need backward compatibility during a phased rollout.

Hypothetical scenario: A platform team discovers that 40% of their GKE workloads still pull from gcr.io URLs embedded in Helm charts maintained by different product squads. Rather than forcing a big-bang URL rewrite across hundreds of charts, they run the automatic migration to stand up gcr.io repositories on Artifact Registry, then schedule a second phase to migrate each squad to regional pkg.dev endpoints where co-location with GKE clusters in us-central1 eliminates cross-region pull latency.

FeatureContainer Registry (GCR)Artifact Registry
FormatsDocker images onlyDocker/OCI, Helm, Maven, npm, Python, Go, Apt, Yum, Kubeflow, Generic
Repository granularityOne registry per region, per projectMultiple named repositories per project
IAM granularityPer-image is complex (bucket-level ACLs)Per-repository IAM policies
Vulnerability scanningBasic (Container Analysis)Integrated automatic on-push + continuous rescanning
Remote repositoriesNot supportedUpstream caching for Docker Hub, npm, Maven, PyPI
Virtual repositoriesNot supportedAggregate multiple repos behind a single endpoint
Cleanup policiesManual scriptingBuilt-in tag and version cleanup policies
CMEK encryptionNot supportedCustomer-managed keys at repository creation
Immutable tagsNot supportedOptional --immutable-tags per repository
StatusShut down (March 2025)Active development, recommended

Artifact Registry supports Docker Image Manifest V2 (Schema 1 and 2), OCI image format specifications, and manifest lists for multi-architecture images. Helm charts packaged in OCI format store in Docker-format repositories alongside container images. Language-package formats (Maven, npm, Python, Go modules, Apt, Yum) each use dedicated repository types with format-specific client configuration, which lets a single GCP project host both the container images for your microservices and the internal npm packages those services depend on without operating separate registry infrastructure.

Terminal window
# Enable the Artifact Registry API
gcloud services enable artifactregistry.googleapis.com
# If you are still using GCR, migrate with:
gcloud artifacts docker upgrade migrate --projects=my-project

The migration command creates Artifact Registry repositories that mirror your existing GCR host layout and copies images asynchronously. During migration, both systems may serve traffic depending on your timeline, so plan the cutover during a maintenance window when you can validate that CI pipelines push successfully to the new endpoints and GKE clusters pull without ImagePullBackOff events or other unexpected deployment delays. After migration completes, update documentation and Terraform modules to reference pkg.dev URLs for new repositories even if legacy gcr.io URLs continue working through compatibility repositories.

Teams migrating from other cloud providers often compare Artifact Registry to Amazon ECR or Azure Container Registry. The GCP differentiators worth internalizing are unified multi-format support (not Docker-only), first-class remote and virtual repositories for upstream caching, and native integration with Binary Authorization and Artifact Analysis for supply chain security. The tradeoff is that Artifact Registry pricing is storage-based rather than per-image as some competitors charge, which makes cleanup policy discipline more important at scale for cost control.


Terminal window
# Create a Docker repository
gcloud artifacts repositories create docker-repo \
--repository-format=docker \
--location=us-central1 \
--description="Production Docker images" \
--immutable-tags
# The --immutable-tags flag prevents overwriting existing tags.
# Once you push myapp:v1.0, nobody can push a different image with the same tag.
# This is critical for reproducibility and security.
# List repositories
gcloud artifacts repositories list --location=us-central1
# Describe a repository
gcloud artifacts repositories describe docker-repo \
--location=us-central1

Repository names must be unique within a project and location combination, use lowercase letters and numbers with hyphens allowed, and appear in every image URL your teams reference. Choosing descriptive names like prod-app-images instead of generic docker prevents confusion when a project hosts a dozen repositories across environments and makes IAM audit logs readable when investigating who pushed to which store during a security review.

Terminal window
# Create npm repository
gcloud artifacts repositories create npm-repo \
--repository-format=npm \
--location=us-central1 \
--description="Internal npm packages"
# Create Python (PyPI) repository
gcloud artifacts repositories create python-repo \
--repository-format=python \
--location=us-central1 \
--description="Internal Python packages"
# Create Maven repository
gcloud artifacts repositories create maven-repo \
--repository-format=maven \
--location=us-central1 \
--description="Internal Java artifacts"
# Create Apt repository (for Debian/Ubuntu packages)
gcloud artifacts repositories create apt-repo \
--repository-format=apt \
--location=us-central1 \
--description="Internal Debian packages"
# Create Yum repository (for RPM packages on RHEL/CentOS)
gcloud artifacts repositories create yum-repo \
--repository-format=yum \
--location=us-central1 \
--description="Internal RPM packages"
# Create Go module repository
gcloud artifacts repositories create go-repo \
--repository-format=go \
--location=us-central1 \
--description="Internal Go modules"

Each repository format enforces its own naming and authentication conventions. Docker and Helm (OCI) clients use gcloud auth configure-docker; npm clients point .npmrc at the Artifact Registry npm endpoint; Maven and Gradle builds add a repository URL to pom.xml or build.gradle; Python clients configure pip or twine with the Artifact Registry PyPI URL. Choosing one repository per format per environment (for example, prod-docker, staging-docker) keeps IAM bindings and cleanup policies scoped to the blast radius you actually want, rather than mixing production and experimental artifacts in a single bucket-like namespace.

Artifact Registry repositories are regional or multi-regional resources, and location choice affects latency, cost, and compliance simultaneously. Co-locating a Docker repository in us-central1 with GKE clusters and Cloud Run services in the same region eliminates cross-region egress on every pod restart or scale-out event, which is the pattern most cost-conscious platform teams adopt for production workloads. Multi-regional repositories trade slightly higher storage cost for geographic redundancy when the same image must be pulled from clusters in different continents without maintaining duplicate repos per region.

Hypothetical scenario: A fintech team stores container images in europe-west1 but runs primary GKE in us-central1 for historical reasons. Every node that pulls an image pays cross-region egress on each layer download, and cold-start latency for Cloud Run revisions increases because the image travels an ocean before the container starts. Moving the production repository to us-central1 and keeping a read-only replication workflow for the European DR cluster cuts monthly networking costs and aligns with data-residency discussions without changing application code.


While container images dominate Artifact Registry conversations, the same service hosts the language packages that those containers install during docker build. Centralizing npm, Maven, Python, and Go modules alongside the images that consume them gives security teams a single audit surface and gives developers consistent authentication across every artifact type.

npm repositories use a dedicated endpoint format: https://REGION-npm.pkg.dev/PROJECT/REPO/. After creating an npm-format repository, configure your .npmrc to point at that URL and authenticate with the npm credential helper or a short-lived access token from npx google-artifactregistry-auth. Internal packages publish with npm publish; CI pipelines that previously pulled directly from the public npm registry can instead pull through a remote npm repository that caches upstream packages locally, which mitigates both rate limiting and the class of supply chain attack illustrated in this module’s opening incident.

Maven-format repositories integrate with standard Java build tooling through a repository URL added to pom.xml or settings.xml. Gradle projects reference the same URL in build.gradle or build.gradle.kts. Snapshot and release versioning behave as they would on Nexus or Artifactory, which makes Artifact Registry a viable replacement when your Java microservices already run on GKE and you want artifact storage in the same IAM boundary as your container images. Remote repositories can cache Maven Central so builds do not reach out to the public internet on every dependency resolution.

Python-format repositories expose a PyPI-compatible index URL. Configure pip with --index-url or add the URL to pip.conf for persistent use. twine upload publishes internal wheels and sdists; pip install resolves dependencies from the same repository. Pairing a standard Python repository for internal packages with a remote PyPI cache gives data science teams the same upstream-buffering benefit that platform teams get from Docker Hub remote repos, which matters when a compromised package appears on PyPI after your nightly training job already cached the clean version.

Apt and Yum repositories store .deb and .rpm packages for golden-image builds and configuration management. Platform teams that bake custom OS packages into VM images or Dockerfile RUN apt-get install steps can host those packages privately instead of mirroring them on internal file servers with ad hoc authentication. The same IAM roles apply: CI systems that publish packages receive writer access, while production build pipelines receive reader access scoped to the specific repository.


Pulling and pushing container images is the most common Artifact Registry workflow, but authentication differs depending on whether a human developer, a CI/CD pipeline, or a GKE node is performing the operation. Getting authentication wrong produces the frustrating denied: Permission denied error that masks whether the problem is missing Docker credentials, insufficient IAM, or a nonexistent repository.

Terminal window
# Configure Docker to authenticate with Artifact Registry
gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
# This adds a credential helper to your Docker config (~/.docker/config.json)
# For other regions, specify them:
gcloud auth configure-docker us-central1-docker.pkg.dev,europe-west1-docker.pkg.dev

The gcloud auth configure-docker command installs a credential helper that exchanges your user or service account credentials for short-lived OAuth tokens on each docker push or docker pull. For CI/CD pipelines, bind roles/artifactregistry.writer to a dedicated service account and authenticate Cloud Build or your external runner with that identity rather than storing long-lived JSON keys. Cloud Build’s default service account already has push access within the same project when you grant the writer role on the target repository.

For GKE deployments, image pulls authenticate through the node service account, not the Kubernetes ServiceAccount attached to individual pods. Grant roles/artifactregistry.reader on the repository to whichever IAM identity your node pool uses—typically the Compute Engine default service account (PROJECT_NUMBER-compute@developer.gserviceaccount.com) or a custom node service account you configured at cluster creation. If your Artifact Registry repository lives in a different GCP project than your GKE cluster, you must grant reader access cross-project; same-project deployments often work with defaults once the reader role is bound.

Workload Identity Federation lets external CI systems (GitHub Actions, GitLab CI, on-premises Jenkins) impersonate a GCP service account without downloading key files. The external identity provider exchanges OIDC tokens for GCP credentials, and the impersonated service account receives repository-scoped IAM roles. This pattern eliminates the key-rotation burden that makes service account JSON keys a recurring audit finding.

Terminal window
# Example: grant a Cloud Build service account push access (same project)
gcloud artifacts repositories add-iam-policy-binding docker-repo \
--location=us-central1 \
--member="serviceAccount:PROJECT_NUMBER@cloudbuild.gserviceaccount.com" \
--role="roles/artifactregistry.writer"
Terminal window
# Image URL format:
# REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/IMAGE:TAG
export PROJECT_ID=$(gcloud config get-value project)
export REGION=us-central1
export REPO=docker-repo
# Build an image locally
docker build -t my-api:v1.2.0 .
# Tag it for Artifact Registry
docker tag my-api:v1.2.0 \
${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/my-api:v1.2.0
# Push to Artifact Registry
docker push ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/my-api:v1.2.0
# Also tag as latest
docker tag my-api:v1.2.0 \
${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/my-api:latest
docker push ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/my-api:latest
# List images in the repository
gcloud artifacts docker images list \
${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}
# List tags for a specific image
gcloud artifacts docker tags list \
${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/my-api
# Delete an image by digest
gcloud artifacts docker images delete \
${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/my-api@sha256:abc123... \
--quiet
Terminal window
# Pull from Artifact Registry
docker pull ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/my-api:v1.2.0
# Use in Kubernetes deployment
cat <<'YAML'
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-api
spec:
replicas: 3
selector:
matchLabels:
app: my-api
template:
metadata:
labels:
app: my-api
spec:
containers:
- name: my-api
image: us-central1-docker.pkg.dev/my-project/docker-repo/my-api:v1.2.0
ports:
- containerPort: 8080
YAML

Modern deployment targets rarely standardize on a single CPU architecture. GKE clusters may mix x86 nodes with Tau T2A ARM instances for cost optimization, and developer laptops on Apple Silicon build linux/arm64 images locally. Artifact Registry supports manifest lists and OCI image indexes that let you push both linux/amd64 and linux/arm64 variants under a single tag; the container runtime selects the correct architecture automatically at pull time.

Build multi-arch images with Docker Buildx and push them in one command. The example below tags both architectures under a single version label so GKE nodes pull the correct variant automatically without separate deployment manifests per architecture family.

Terminal window
# Create and use a buildx builder (one-time setup)
docker buildx create --name multiarch --use
# Build and push multi-arch image to Artifact Registry
docker buildx build --platform linux/amd64,linux/arm64 \
-t ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/my-api:v1.2.0 \
--push .

Helm 3.8+ supports OCI registries for chart storage, and Artifact Registry accepts Helm charts in Docker-format repositories because charts are packaged as OCI artifacts. Push a chart with helm push my-chart-1.0.0.tgz oci://REGION-docker.pkg.dev/PROJECT/REPO and install with helm install my-release oci://REGION-docker.pkg.dev/PROJECT/REPO/my-chart --version 1.0.0. Storing charts alongside the container images they deploy keeps the release artifact bundle in one IAM-scoped repository, which simplifies audit questions about which chart version deployed which image digest.

When you reference images by digest rather than tag in Kubernetes manifests (image: repo/my-app@sha256:abc123...), you eliminate tag-mutation risk entirely. Immutable tags are a softer guarantee—they prevent overwrites but still allow two different builds to reuse the same tag name if someone deletes and recreates the tag through policy changes—so digest pinning remains the gold standard for production deployments that must prove exactly which bits ran in a given incident investigation.


Artifact Registry integrates with Artifact Analysis (formerly Container Analysis) to automatically scan container images for known vulnerabilities in OS packages and language-level dependencies. Scanning is a paid capability—each initial scan costs $0.26 per image digest, and rescans of the same digest are free—so enabling the Container Scanning API on a project where CI pushes hundreds of unique images daily can produce a noticeable line item unless you design scanning gates deliberately.

Terminal window
# Enable the Container Scanning API (required for on-push scan results)
gcloud services enable containeranalysis.googleapis.com \
containerscanning.googleapis.com
# Scanning happens automatically when you push an image after the API is enabled.
# You can also trigger an on-demand scan:
gcloud artifacts docker images scan \
${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/my-api:v1.2.0 \
--location=us-central1

Automatic scanning triggers on the first push of each unique image digest. Because images are identified by digest, retagging an existing digest does not incur another scan charge. Continuous scanning re-evaluates stored images when new CVE data arrives, which means a image that passed inspection last week may surface new HIGH findings today without any code change on your side—exactly the behavior you want for supply chain monitoring, but surprising if your deployment pipeline treats “scanned once” as “safe forever.”

Artifact Analysis inspects both the operating-system packages in your base image layer and application-level dependencies where supported. The severity table below maps CVSS scores to recommended response timelines; production teams typically wire CRITICAL and HIGH findings into CI gates or Binary Authorization policies rather than relying on developers to check scan results manually.

Terminal window
# List vulnerabilities for a specific image
gcloud artifacts docker images list \
${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO} \
--show-occurrences \
--format="table(package, version, createTime, updateTime)"
# Get detailed vulnerability report
gcloud artifacts vulnerabilities list \
${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/my-api:v1.2.0 \
--format="table(vulnerability.shortDescription, vulnerability.cvssScore, vulnerability.severity, vulnerability.packageIssue[0].affectedPackage)"

Scan results appear in the Google Cloud console under Artifact Registry, in the gcloud output above, and through the Container Analysis API for integration with SIEM or ticketing systems. Each finding links to a CVE identifier, affected package name, installed version, and fixed version when available. Platform teams typically define a policy document—often enforced through Binary Authorization or a CI script—that blocks promotion of images containing CRITICAL vulnerabilities and requires a ticket reference for HIGH findings that cannot be patched immediately because the upstream fix is not yet released.

Building a vulnerability scanning policy means deciding three things: which severities block deployment, who can grant exceptions, and how continuous rescanning alerts reach on-call engineers when a previously clean stored digest gains new CRITICAL findings overnight. The first decision belongs to security and product leadership; the second needs an audit trail; the third requires Pub/Sub notifications or Security Command Center integration so findings do not sit unread in a console tab.

Hypothetical scenario: The Silent Scan Failure

Hypothetical scenario: A platform team enabled vulnerability scanning on their production Docker repository but never wired scan results into the deployment pipeline. Developers assumed green CI checks meant secure images. When a CRITICAL OpenSSL CVE dropped on a Tuesday evening, continuous scanning flagged twelve production images within hours—but nobody noticed until a external researcher emailed the security team on Friday. The fix required emergency rebuilds of every affected image, attestations for Binary Authorization, and a retroactive policy mandating that CI fail on CRITICAL findings before images reach the production repository. The scanning feature worked exactly as designed; the failure was organizational, not technical.

SeverityCVSS ScoreAction
CRITICAL9.0 - 10.0Block deployment, fix immediately
HIGH7.0 - 8.9Fix before next release
MEDIUM4.0 - 6.9Fix within sprint
LOW0.1 - 3.9Track and fix at convenience
MINIMAL0.0Informational

Stop and think: If vulnerability scanning is automatic, what prevents a developer from deploying an image that was scanned but found to have CRITICAL vulnerabilities?

For production environments, you can enforce that only scanned and approved images are deployed to GKE. Binary Authorization evaluates attestations attached to images before GKE admits them into the cluster, closing the gap between “we scanned it” and “we deployed it anyway.”

Terminal window
# Enable Binary Authorization
gcloud services enable binaryauthorization.googleapis.com
# The full Binary Authorization setup is beyond this module,
# but the key concept is:
# 1. Images are pushed to Artifact Registry
# 2. Vulnerability scanning runs automatically
# 3. An attestor signs images that pass the scan
# 4. GKE clusters only run images with valid attestations

A typical production pipeline pushes the image, waits for scan completion, runs a policy check against allowed severities, and only then creates an attestation that Binary Authorization verifies at deploy time. Without this enforcement layer, automatic scanning is informational—developers see CRITICAL CVEs in the console but nothing prevents kubectl apply from running the vulnerable image in production.


Artifact Registry supports fine-grained IAM at the repository level, which is one of the primary reasons teams migrate from legacy GCR bucket ACLs. Project-level roles like roles/artifactregistry.admin grant control over every repository in the project; repository-level bindings limit CI/CD push access to docker-repo without exposing npm-repo or production-only image stores.

RolePermissionsTypical User
roles/artifactregistry.readerPull images, list reposGKE nodes, Cloud Run, developers
roles/artifactregistry.writerPull + push imagesCI/CD pipelines
roles/artifactregistry.repoAdminFull control over a repoPlatform engineers
roles/artifactregistry.adminFull control over all reposRegistry administrators

The Artifact Registry service agent (service-PROJECT_NUMBER@gcp-sa-artifactregistry.iam.gserviceaccount.com) requires KMS permissions when you use customer-managed encryption keys. Cloud Run, Cloud Build, and GKE each have their own service agents documented in the access-control guide—grant the minimum repository role to the agent that actually pulls or pushes, not a project-wide admin role “just to make it work.”

Terminal window
# Grant a CI/CD service account push access to a specific repository
gcloud artifacts repositories add-iam-policy-binding docker-repo \
--location=us-central1 \
--member="serviceAccount:ci-pipeline@my-project.iam.gserviceaccount.com" \
--role="roles/artifactregistry.writer"
# Grant GKE node service account read access
gcloud artifacts repositories add-iam-policy-binding docker-repo \
--location=us-central1 \
--member="serviceAccount:gke-nodes@my-project.iam.gserviceaccount.com" \
--role="roles/artifactregistry.reader"
# Grant a developer group read access (for pulling images locally)
gcloud artifacts repositories add-iam-policy-binding docker-repo \
--location=us-central1 \
--member="group:developers@example.com" \
--role="roles/artifactregistry.reader"
# View repository IAM policy
gcloud artifacts repositories get-iam-policy docker-repo \
--location=us-central1

Cross-project access is a frequent source of ImagePullBackOff errors in GKE. When your cluster runs in project app-prod but images live in project shared-artifacts, you must grant roles/artifactregistry.reader on the repository in shared-artifacts to the node service account from app-prod. Project-level IAM bindings work but violate least privilege because they grant access to every repository in the project; repository-level bindings limit the blast radius if a node is compromised.

Cloud Run and Cloud Functions pull images through the Cloud Run service agent (service-PROJECT_NUMBER@serverless-robot-prod.iam.gserviceaccount.com), which needs reader access on the repository hosting the container image specified in the service definition. App Engine flexible environment uses its own service account (PROJECT_ID@appspot.gserviceaccount.com). Each compute service has a documented service agent in the access control guide, and granting the wrong agent is a common copy-paste mistake during initial setup.

For human developers who need local pull access, bind roles/artifactregistry.reader to a Google Group rather than individual users so onboarding and offboarding happen through group membership. Developers who also need push access for manual hotfix builds receive writer on staging repositories only; production repositories receive pushes exclusively from CI service accounts with Binary Authorization attestations downstream.


Most teams do not push container images manually from laptops; Cloud Build or another CI system builds, tags, and pushes on every merge. Artifact Registry integrates natively with Cloud Build through the gcloud builds submit workflow and through cloudbuild.yaml steps that reference Artifact Registry image URLs directly.

A typical Cloud Build pipeline enables the Artifact Registry and Container Analysis APIs, builds a Docker image with Kaniko or Docker builder, tags it with both a git commit SHA and a semver release tag, pushes to a regional repository, waits for vulnerability scanning to complete, and only then deploys to GKE or Cloud Run. Keeping the build and registry in the same region avoids egress charges and reduces push latency during large layer uploads.

# cloudbuild.yaml excerpt — build and push to Artifact Registry
steps:
- name: 'gcr.io/cloud-builders/docker'
args:
- 'build'
- '-t'
- '${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_REPO}/${_IMAGE}:${SHORT_SHA}'
- '-t'
- '${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_REPO}/${_IMAGE}:${_TAG}'
- '.'
- name: 'gcr.io/cloud-builders/docker'
args:
- 'push'
- '--all-tags'
- '${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_REPO}/${_IMAGE}'
substitutions:
_REGION: us-central1
_REPO: app-images
_IMAGE: my-api
_TAG: latest

Cloud Build’s service account needs roles/artifactregistry.writer on the target repository. If builds pull base images through a remote Docker Hub cache, the build service account also needs reader access on the remote repository. Separating build-time permissions (writer on staging repo) from deploy-time permissions (reader on production repo, attestation signer) enforces the same promotion workflow that Binary Authorization expects downstream.

When Cloud Build runs inside a private pool or VPC-SC perimeter, verify that Artifact Registry endpoints are allowed through your service perimeter. VPC Service Controls documentation for Artifact Registry covers restricted-network access from perimeters, which matters for regulated industries that cannot reach Google APIs over the public internet without explicit perimeter configuration.


Remote repositories act as caching proxies for public registries. When you pull an artifact through a remote repository, Artifact Registry fetches it from the upstream on a cache miss and stores a local copy. Subsequent pulls serve from your regional cache, which improves build speed, reduces exposure to upstream outages, and creates a buffer against supply chain attacks where a compromised package appears on a public registry after your pipeline already cached a clean version.

Remote repositories support upstream sources including Docker Hub, Maven Central, npmjs, PyPI, and others depending on repository format. Remote repos are read-through caches: you cannot push artifacts into them, only pull. Storage consumed by cached upstream artifacts counts toward your Artifact Registry storage bill, which is why pairing remote repos with cleanup policies matters for long-running CI pipelines that pull many unique tag combinations.

flowchart LR
CI["Your CI/CD<br>Pipeline"]
subgraph AR ["Artifact Registry (Remote Repo)"]
direction TB
Cache[("Cache (local)")]
end
Hub["Docker Hub<br>(upstream)"]
CI -- "Cache Miss" --> AR
AR -. "Pulls from cache<br>(if available)" .-> CI
AR --> Hub

Pause and predict: If a public registry goes down for maintenance, what happens to a CI/CD pipeline that pulls images through an Artifact Registry remote repository?

Terminal window
# Create a remote repository that caches Docker Hub
gcloud artifacts repositories create dockerhub-cache \
--repository-format=docker \
--location=us-central1 \
--description="Docker Hub upstream cache" \
--mode=remote-repository \
--remote-repo-config-desc="Docker Hub" \
--remote-docker-repo=DOCKER-HUB
# Create a remote repository for npm
gcloud artifacts repositories create npm-cache \
--repository-format=npm \
--location=us-central1 \
--description="npm registry upstream cache" \
--mode=remote-repository \
--remote-repo-config-desc="npm Registry" \
--remote-npm-repo=NPMJS
# Create a remote repository for PyPI
gcloud artifacts repositories create pypi-cache \
--repository-format=python \
--location=us-central1 \
--description="PyPI upstream cache" \
--mode=remote-repository \
--remote-repo-config-desc="PyPI" \
--remote-python-repo=PYPI
# Pull an image through the remote repository
# Note: Docker Hub official images require the 'library/' namespace prefix
docker pull us-central1-docker.pkg.dev/${PROJECT_ID}/dockerhub-cache/library/nginx:1.25
# Subsequent pulls of the same image come from your local cache

Remote repositories do not prefetch the entire upstream registry—they cache artifacts on first pull, which means the initial CI run after creating a remote repo still hits the public registry and is subject to upstream rate limits until the cache warms. Docker Hub official images require the library/ namespace prefix when pulling through Artifact Registry, as shown in the nginx example above, because Docker Hub namespaces official images under library/ even when users omit it in direct Hub pulls.

For npm, Maven, and Python remote repos, the upstream configuration uses enumerated constants like NPMJS, MAVEN-CENTRAL, and PYPI in the gcloud command. Remote repositories support authenticated upstream access for private Docker Hub organizations and other protected registries by storing credentials in a Secret Manager secret referenced in the remote repository configuration—see the remote repository guide.

Hypothetical scenario: Your CI pipeline builds 200 times per day and each build pulls node:20-bookworm-slim from Docker Hub. Without a remote cache, you hit Docker Hub rate limits within the first hour and builds fail unpredictably. After creating a remote repository and updating CI to pull through us-central1-docker.pkg.dev/PROJECT/dockerhub-cache/library/node:20-bookworm-slim, the first build populates the cache and the remaining 199 builds pull from Artifact Registry at in-region speeds with no upstream dependency.

Virtual repositories provide a single endpoint that aggregates multiple upstream repositories (both standard and remote). Priority values determine lookup order: higher priority wins—when multiple upstreams contain an artifact with the same name, the upstream with the highest priority value is served. To defend against dependency confusion, your internal standard repository must have a higher priority than the public remote cache so internal packages win over identically named public packages.

Terminal window
# Create a virtual repository that combines your internal repo and Docker Hub cache
gcloud artifacts repositories create docker-virtual \
--repository-format=docker \
--location=us-central1 \
--description="Virtual Docker repository" \
--mode=virtual-repository \
--upstream-policy-file=upstream-policy.json
[
{
"id": "internal",
"repository": "projects/my-project/locations/us-central1/repositories/docker-repo",
"priority": 200
},
{
"id": "dockerhub",
"repository": "projects/my-project/locations/us-central1/repositories/dockerhub-cache",
"priority": 100
}
]

With this configuration, pulls from the virtual repository check upstreams by priority (highest first). The internal standard repository at priority 200 wins when both it and the Docker Hub cache contain an artifact with the same name; the remote cache at priority 100 is consulted only when the internal repo has no match. Developers configure a single registry URL in Docker, npm, or pip settings, which reduces misconfiguration and makes dependency-confusion defenses easier because internal package names resolve to internal artifacts before the public cache is consulted.

When designing upstream priority, remember that higher numbers win. A common mistake sets the remote cache at priority 200 and the internal repo at priority 100, which inverts the intended behavior and serves public packages even when an internal artifact exists. Document priority ordering in your platform runbook so teams configuring virtual repositories do not accidentally expose themselves to dependency confusion attacks through inverted priority values.


Cleanup policies automatically delete old images to prevent storage cost growth. Artifact Registry evaluates policies through a periodic background job, so changes take effect within approximately one day. Use --dry-run mode first to preview which artifacts would be deleted before enabling destructive rules on a production repository.

Untagged images accumulate silently when CI pipelines push both digest-pinned builds and floating tags like latest or branch names. Each push of my-app:main creates a new digest while the previous digest becomes untagged but still billable storage. A delete policy targeting untagged images older than 30 days, combined with a keep policy retaining the ten most recent v-prefixed release tags, balances cost control against the ability to roll back to a recent production build.

Terminal window
# Create a cleanup policy that deletes untagged images older than 30 days
gcloud artifacts repositories set-cleanup-policies docker-repo \
--location=us-central1 \
--policy=cleanup-policy.json
[
{
"name": "delete-untagged",
"action": {"type": "Delete"},
"condition": {
"tagState": "untagged",
"olderThan": "2592000s"
}
},
{
"name": "keep-recent-tagged",
"action": {"type": "Keep"},
"condition": {
"tagState": "tagged",
"tagPrefixes": ["v"]
},
"mostRecentVersions": {
"keepCount": 10
}
}
]

Encryption with Customer-Managed Keys (CMEK)

Section titled “Encryption with Customer-Managed Keys (CMEK)”

By default, Artifact Registry encrypts all stored artifacts with Google-managed encryption keys. For regulated workloads that require control over key rotation, geographic location, and audit trails, you can assign a Cloud KMS customer-managed encryption key (CMEK) when creating a repository. CMEK is set at repository creation time—you cannot convert an existing repository from Google-managed to CMEK encryption later, which means planning encryption requirements before the first push avoids a painful re-upload migration.

Terminal window
# Grant the Artifact Registry service agent access to the KMS key
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)")
gcloud kms keys add-iam-policy-binding my-ar-key \
--location=us-central1 \
--keyring=my-keyring \
--member="serviceAccount:service-${PROJECT_NUMBER}@gcp-sa-artifactregistry.iam.gserviceaccount.com" \
--role="roles/cloudkms.cryptoKeyEncrypterDecrypter"
# Create a CMEK-encrypted Docker repository
gcloud artifacts repositories create prod-images-cmek \
--repository-format=docker \
--location=us-central1 \
--kms-key=projects/${PROJECT_ID}/locations/us-central1/keyRings/my-keyring/cryptoKeys/my-ar-key \
--immutable-tags

Organization policy constraints such as constraints/gcp.restrictCmekCryptoKeyProjects can require that all new Artifact Registry repositories use keys from an approved KMS project. Cloud KMS Autokey generates keys on demand during repository creation. This reduces manual key-provisioning steps for platform teams operating dozens of repositories across environments.


Understanding the Artifact Registry Cost Model

Section titled “Understanding the Artifact Registry Cost Model”

Artifact Registry billing has three independent components—storage, data transfer, and vulnerability scanning—and teams that monitor only storage often miss the scanning line item that grows with CI velocity.

Artifact Registry storage costs $0.10 per GB per month for usage above the 0.5 GB free tier (aggregated across all projects attached to a billing account). Regional and multi-regional repositories use the same storage rate. Remote repository caches count toward storage because every upstream artifact you pull through the cache persists locally until a cleanup policy or manual deletion removes it.

At moderate scale—say 200 GB of container images and cached upstream packages across production and staging repositories—storage alone runs roughly 20/monthafterthefreetier.Thatsoundsmodestuntilyourealizeasingleteampushingunique:sha20/month after the free tier. That sounds modest until you realize a single team pushing unique `:sha-{GIT_COMMIT}` tags on every build without cleanup policies can add 5–10 GB per week of mostly-untagged digests that nobody pulls again.

Pulling images from Artifact Registry to GKE nodes, Cloud Run, or Compute Engine within the same region incurs no egress charge—this is the co-location pattern that makes regional pkg.dev repositories cheaper than pulling from a multi-regional gcr.io host across regions. Cross-region pulls, pulls to on-premises infrastructure, and pulls to other cloud providers incur standard GCP network egress rates. Remote repositories reduce egress to public registries because the first pull from Docker Hub crosses the internet boundary once; subsequent CI runs pull from your regional cache over Google’s network.

Each unique image digest scanned costs 0.26onfirstpush;rescansofthesamedigestarefree.Apipelinethatbuilds50distinctimagesperdayacrossfivemicroservicesgeneratesroughly1,500initialscanspermonth,orabout0.26** on first push; rescans of the same digest are free. A pipeline that builds 50 distinct images per day across five microservices generates roughly 1,500 initial scans per month, or about **390/month in scanning alone. Mitigations include scanning only release-candidate tags (not every feature-branch build), using a dedicated scanning project, and relying on continuous rescanning of stored digests rather than pushing duplicate builds with identical content under new tags.

KnobWhat It ReducesTradeoff
Cleanup policies for untagged digestsStorage growth from CI churnMust retain enough tagged history for rollbacks
Regional repos co-located with GKECross-region egressLess geographic redundancy for image pulls
Remote repos for public base imagesDocker Hub egress + rate limitsCached upstream artifacts still bill for storage
Scan only release tagsScanning chargesFeature branches deploy without fresh scan data
Immutable tags + digest pinningAccidental re-push/rescan cyclesRequires pipeline discipline on tag naming

Hypothetical scenario: A data platform team enables vulnerability scanning project-wide, then a monorepo CI job starts building 200 Docker variants nightly—one per combination of base image, Python version, and CUDA driver. Within a month, storage crosses 2 TB (200/month)andscanningadds200/month) and scanning adds 1,300/month because each variant is a new digest. The fix is not disabling scanning; it is restructuring the build matrix to reuse base layers, applying cleanup policies to ephemeral tags, and gating scans on merge-to-main rather than every pull request build.


PatternWhen to UseWhy It WorksScaling Note
Regional repo per environmentSeparate prod, staging, and dev Docker repos in the same region as GKEIAM and cleanup policies differ per environment; in-region pulls avoid egressAdd repos per team only when IAM isolation requires it—repo sprawl complicates virtual upstream configs
Remote cache + virtual repoTeams consume both internal and public packagesSingle endpoint for developers; internal repo at higher priority wins over public cacheMonitor remote cache storage; high-cardinality public tag pulls fill cache quickly
Immutable tags on production reposAny repo receiving release artifactsPrevents tag mutation attacks and accidental overwritesPair with digest references in Kubernetes manifests for strongest guarantees
Repo-scoped CI service accountsEvery pipeline that pushes imagesWriter role on one repo cannot poison unrelated npm or staging reposCreate one SA per pipeline class, not one shared ci@ with admin
Cleanup: delete untagged, keep N releasesHigh-churn CI reposStops silent storage growth from floating branch tagsTest with --dry-run; policies take ~24h to apply
Binary Authorization gateProduction GKE clustersCloses the gap between scanning and deploymentRequires attestor setup; start with audit mode before enforce
Anti-PatternWhat Goes WrongWhy Teams Fall Into ItBetter Alternative
Project-wide artifactregistry.admin for CICompromised pipeline can delete or poison every repoAdmin avoids debugging permission errorsartifactregistry.writer scoped to target repo
Scanning every branch buildScanning bill scales with commit frequency”Enable scanning everywhere” sounds secureScan release tags; use continuous rescan for stored digests
Remote repo without cleanupCache grows unbounded with unique upstream tagsRemote repos feel “free” because pulls are fastApply delete policies to cached artifacts or monitor storage
Cross-region image pullsEgress charges and slower pod startupReusing us multi-region URLs after migrating from GCRRegional pkg.dev in the same region as compute
Relying on latest in productionRollbacks impossible; tag mutation riskConvenient in dev, copied to prod manifestsSemver or git-SHA tags; immutable tags enabled
Skipping GCR migrationPull failures after shutdown timeline”It still works today” deferralRun gcloud artifacts docker upgrade migrate or move to pkg.dev

Decision Framework: Repository Architecture Choices

Section titled “Decision Framework: Repository Architecture Choices”

Use this framework when designing Artifact Registry layout for a new platform or migrating from legacy GCR. The three most common decision points are migration strategy, upstream caching model, and scanning enforcement depth.

flowchart TD
START["Start: Plan Artifact Registry layout"] --> Q1{"Still on legacy GCR buckets?"}
Q1 -- "Yes" --> MIG["Run automatic migration<br/>or create gcr.io repos on AR"]
Q1 -- "No / greenfield" --> Q2{"Need public upstream packages?"}
MIG --> Q2
Q2 -- "No, internal only" --> STD["Standard repo per format/env<br/>+ cleanup policies + immutable tags"]
Q2 -- "Yes" --> Q3{"Single endpoint for devs?"}
Q3 -- "Yes" --> VIRT["Standard + Remote repos<br/>aggregated via Virtual repo"]
Q3 -- "No, separate configs OK" --> REM["Remote cache repo only<br/>point CI directly"]
STD --> Q4{"Production GKE?"}
VIRT --> Q4
REM --> Q4
Q4 -- "Yes" --> SCAN["Enable scanning + Binary Authorization<br/>on prod repos"]
Q4 -- "No" --> DEV["Scan optional;<br/>focus on IAM + cleanup"]
DecisionOption AOption BChoose A WhenChoose B When
Migrationgcr.io repos on Artifact RegistryNew pkg.dev regional reposNeed zero-downtime URL compatibility during phased migrationGreenfield or ready for manifest URL updates
Upstream accessRemote repository onlyVirtual repo (internal + remote)CI pulls public images; devs rarely mix sourcesDevs need one .npmrc / Docker config for internal + public
Scanning gateScan all pushesScan release tags onlyLow push volume; strict complianceHigh-churn CI; cost-sensitive; use continuous rescan
EncryptionGoogle-managed keysCMEK at repo creationDefault security sufficientRegulatory requirement for customer key control
Tag strategyImmutable semver tagsFloating branch tags + cleanupProduction deploymentsEphemeral dev/staging repos with aggressive cleanup

If your CI pushes fewer than 20 unique production-bound images per week, enable automatic scanning on every push. Wire CRITICAL findings to block deploy. If CI pushes hundreds of feature-branch images daily, scan on merge to main instead. Promote only tagged release artifacts to the production repository where Binary Authorization enforces attestations. The cost difference between these models can be an order of magnitude at scale. Security outcomes stay similar when continuous rescanning covers stored release digests.


Operational Monitoring and Troubleshooting

Section titled “Operational Monitoring and Troubleshooting”

Artifact Registry problems surface in three places: CI logs during push/pull, GKE events as ImagePullBackOff, and billing reports showing unexpected storage or scanning growth. Knowing which diagnostic path to follow saves hours of misdirected debugging.

When docker push returns denied: Permission denied, check credentials first (gcloud auth configure-docker), then repository existence and region, then IAM bindings on the repository—not the project. Artifact Registry intentionally returns generic permission errors for nonexistent repos to avoid leaking resource names to unauthorized callers.

When GKE pods enter ImagePullBackOff, verify the image URL format (REGION-docker.pkg.dev/PROJECT/REPO/IMAGE:TAG), confirm the repository exists in the stated region, and grant roles/artifactregistry.reader to the node service account on that repository. Cross-project pulls require explicit cross-project IAM; same-project pulls with the default Compute Engine service account still need the reader role bound after least-privilege hardening removes legacy broad storage permissions.

Storage growth without traffic growth almost always means untagged digests accumulating from CI. Run gcloud artifacts docker images list with --include-tags to identify images with empty tag columns, then implement cleanup policies in dry-run mode before enabling deletion. Scanning cost spikes correlate with unique digest count—audit CI for pipelines that push :latest or :sha-$CI_COMMIT on every branch build without deduplication.

Audit logs for Artifact Registry record push, pull, and delete operations when data access logging is enabled. Enable the DATA_WRITE audit log type for artifactregistry.googleapis.com to trace who pushed a malicious image or who deleted a release tag during an incident investigation. Cleanup policy dry runs also appear in audit logs, which helps validate that a new delete rule will not remove production release tags before the policy goes live.

Multi-team platforms often ask whether to share one Docker repository or isolate per team. The recommended pattern creates one shared remote repository for Docker Hub caching (all teams need reader access), separate standard repositories per team for internal images (writer IAM scoped to each team’s CI service account), and optional virtual repositories per team that prioritize internal images over the shared cache. Shared caching avoids duplicate storage of node:20-bookworm-slim across fifteen teams, while separate standard repositories prevent Team A’s compromised pipeline from pushing malicious images into Team B’s release namespace.


  1. Artifact Registry is the managed successor to Container Registry and supports Docker/OCI images, language packages, and OS packages across regional repositories with per-repository IAM.

  2. The --immutable-tags flag prevents tag mutation attacks. Without it, someone with push access can overwrite myapp:v1.0 with a completely different image. This is a real attack vector---an attacker who compromises a CI/CD pipeline can replace a known-good image tag with a malicious one. Immutable tags guarantee that v1.0 always refers to the exact same image digest.

  3. Remote repositories save egress and rate-limit exposure for teams that pull heavily from public registries. Instead of every CI/CD run pulling node:20-slim from Docker Hub (incurring egress costs and being subject to Docker Hub rate limits), the image is cached locally after the first pull. Google hosts mirror.gcr.io as a pull-through cache for frequently requested Docker Hub images on Artifact Registry infrastructure, which GKE documentation references for base image pulls when you do not operate your own remote repository.

  4. Artifact Registry supports multi-architecture images (manifest lists) out of the box. You can push both linux/amd64 and linux/arm64 variants under the same tag, and Docker or Kubernetes will automatically pull the correct architecture for the host platform. This is increasingly important as ARM-based instances (like GCP’s Tau T2A) become popular for cost savings.


MistakeWhy It HappensHow to Fix It
Still using Container Registry (gcr.io)Old tutorials and existing pipelines reference itMigrate to Artifact Registry; GCR is deprecated
Not enabling vulnerability scanningScanning is not enabled by default on all reposEnable it on every Docker repository
Using latest tag in productionConvenient during developmentAlways use specific version tags; enable --immutable-tags
Granting artifactregistry.admin to CI/CDShortcut to avoid permission errorsCI/CD only needs artifactregistry.writer for push
Not setting up cleanup policiesStorage seems cheap initiallyImplement cleanup policies; untagged images accumulate fast
Pulling public images directly in CI/CDWorks fine until Docker Hub rate-limits youSet up remote repositories as upstream caches
Forgetting to configure Docker authdocker push fails with “denied”Run gcloud auth configure-docker REGION-docker.pkg.dev
Not using immutable tagsDefault behavior allows tag overwritingEnable --immutable-tags on all production repositories

1. Your organization has multiple teams publishing internal npm packages, while also relying heavily on public packages from the npm registry. Developers are complaining that they have to configure multiple registry URLs in their `.npmrc` files, and CI builds occasionally fail due to rate limits on the public npm registry. How can you use Artifact Registry's repository types to solve these issues simultaneously?

You should create a standard repository for the internal packages and a remote repository configured to cache the public npm registry. Then, you can create a virtual repository that aggregates both the standard and remote repositories behind a single endpoint. This solves the developers’ configuration issue because they only need to point their .npmrc to the single virtual repository URL. It also solves the CI rate-limiting issue because the remote repository caches the public npm packages locally upon the first pull, serving all subsequent CI requests directly from GCP’s internal network without hitting the public registry.

2. A deployment of your `frontend:v2.1` image worked flawlessly in the staging environment yesterday. Today, the exact same deployment YAML (referencing `frontend:v2.1`) was deployed to production, but the application crashed on startup with a missing dependency error. Assuming no environmental differences, what security and reliability risk likely occurred, and how could it have been prevented in Artifact Registry?

The likely cause is that a developer or a compromised pipeline overwrote the frontend:v2.1 image tag with a new, defective image digest after the staging deployment. This is a classic tag mutation issue, which poses a severe security and reliability risk because a known-good tag can be silently swapped with malicious or broken code. You could have prevented this by enabling the --immutable-tags flag on the production Docker repository. When this flag is enabled, Artifact Registry permanently locks the tag to the original image digest, ensuring that once v2.1 is pushed, it can never be altered or overwritten.

3. You are configuring a new GKE cluster that will run workloads using container images stored in a private Artifact Registry repository within the same GCP project. You want to follow the principle of least privilege. When configuring IAM for the GKE nodes, which specific role should you grant to the node's service account to ensure it can run the containers without exposing the repository to unnecessary risks?

You should grant the roles/artifactregistry.reader role to the GKE node’s service account (or the Workload Identity service account), scoped specifically to the target repository rather than the entire project. This role provides the exact permissions needed to pull and download container images, but explicitly denies the ability to push, overwrite, or delete artifacts. Granting a broader role like artifactregistry.writer or artifactregistry.admin to a compute node violates the principle of least privilege, as a compromised node could then poison the repository by pushing malicious images. By scoping the reader role only to the necessary repository, you limit the blast radius if the node is ever compromised.

4. A critical zero-day vulnerability is discovered in a popular open-source Python library, and malicious actors have managed to upload a compromised version of the package to PyPI. Your CI/CD pipelines automatically build new container images every night using `pip install`. If your organization uses an Artifact Registry remote repository for PyPI, how does this architecture protect your nightly builds from pulling the compromised package?

Remote repositories act as a caching proxy, meaning they store a local copy of any package the first time it is pulled from the upstream registry. Because your pipelines have likely pulled the clean, older version of the Python library previously, that clean version is already cached in your Artifact Registry. When the nightly build runs, it pulls the package from the local cache rather than reaching out to PyPI, completely bypassing the newly compromised version. This caching mechanism provides a critical buffer, allowing your security team time to pin the dependency to a known-safe version before the cache is ever invalidated or updated.

5. A new developer joins your team and is trying to push their first Docker image to the company's Artifact Registry repository (`us-central1-docker.pkg.dev/my-project/docker-repo/app:v1`). The developer runs `docker push` but receives a "denied: Permission denied" error. They confirm they are logged into `gcloud` with their corporate account. What are the three most likely configuration issues or missing steps causing this failure?

The first likely cause is that the developer has not configured Docker to authenticate with Artifact Registry; they must run gcloud auth configure-docker us-central1-docker.pkg.dev to inject the credential helper into their Docker configuration. The second common cause is that the developer’s identity has not been granted the roles/artifactregistry.writer role on that specific repository, leaving them without the necessary permissions to push. Finally, they may be trying to push to a repository that doesn’t exist or isn’t in the correct region, which Artifact Registry often masks behind a generic permission denied error to prevent information disclosure. Troubleshooting these three areas resolves almost all initial push access issues.

6. Your company uses a virtual repository to consolidate access. It is configured with an internal standard repository at priority 200, and a remote repository caching Docker Hub at priority 100. A developer accidentally names their internal helper script `ubuntu` and publishes it as a container image to the internal repository. When a CI pipeline runs `docker pull /ubuntu:latest`, what exactly happens and what security benefit does this priority configuration provide?

When the docker pull command is executed, the virtual repository evaluates its upstreams by priority, with higher values winning. It checks the internal standard repository (priority 200) first among matching upstreams, finds the internally published ubuntu:latest image, and returns it without consulting the remote Docker Hub cache (priority 100). This priority configuration provides a critical security benefit by preventing dependency confusion attacks. If an attacker publishes a malicious package to a public registry with the exact same name as an internal package, the virtual repository serves the internal version because it carries the higher priority—not the public copy.

7. Your organization enables automatic vulnerability scanning on all Docker repositories and integrates Binary Authorization on production GKE clusters. A developer pushes a new image, the scan completes with two CRITICAL CVEs in the base image, and the developer deploys to production anyway using an existing deployment YAML. What should have prevented this deployment, and what vulnerability scanning policy components were likely missing?

Binary Authorization should have blocked the deployment because the image lacks a valid attestation from an authorized attestor confirming it passed the organization’s scan policy. Automatic vulnerability scanning alone is informational—it produces findings in the console but does not prevent kubectl apply unless you wire scan results into attestations that Binary Authorization enforces at the GKE admission layer. The missing policy components are likely a CI gate that fails builds on CRITICAL findings before attestation, an attestor configured to sign only policy-compliant images, and a Binary Authorization policy on the GKE cluster set to enforce rather than audit. Together, these three layers convert scan data into an actual deployment control rather than a dashboard warning.

8. A cost review reveals that Artifact Registry storage grew from 50 GB to 800 GB in three months despite stable production traffic. Investigation shows CI pushes `:build-${CI_PIPELINE_ID}` tags on every commit but only promotes `v*` tags to production. What combination of Artifact Registry features addresses the root cause without affecting production rollback capability?

The root cause is untagged or ephemeral CI tags accumulating as orphaned digests that still consume storage. Implement cleanup policies with two rules: a Delete action for untagged images older than a defined window (for example, 30 days), and a Keep action retaining the ten most recent versions matching the v tag prefix for production rollbacks. Test policies in dry-run mode first because Artifact Registry applies cleanup through a periodic background job. Optionally restrict CI to push ephemeral tags to a separate non-production repository with aggressive cleanup, while only release tags land in the production repo protected by immutable tags.


Hands-On Exercise: Container Registry with Scanning and Caching

Section titled “Hands-On Exercise: Container Registry with Scanning and Caching”

Create an Artifact Registry setup with a standard Docker repository, enable vulnerability scanning, and configure an upstream Docker Hub cache. This exercise mirrors the production pattern most GCP teams adopt: internal images live in a standard repo with immutable tags, public base images flow through a remote cache, and CI service accounts receive the minimum IAM roles needed to push and pull without project-wide admin access.

  • gcloud CLI installed and authenticated
  • Docker installed locally
  • A GCP project with billing enabled
Solution
Terminal window
export PROJECT_ID=$(gcloud config get-value project)
export REGION=us-central1
# Enable APIs
gcloud services enable artifactregistry.googleapis.com \
containeranalysis.googleapis.com \
containerscanning.googleapis.com
# Create a standard Docker repository with immutable tags
gcloud artifacts repositories create app-images \
--repository-format=docker \
--location=$REGION \
--description="Application container images" \
--immutable-tags
# Create a remote repository caching Docker Hub
gcloud artifacts repositories create dockerhub-mirror \
--repository-format=docker \
--location=$REGION \
--description="Docker Hub cache" \
--mode=remote-repository \
--remote-repo-config-desc="Docker Hub" \
--remote-docker-repo=DOCKER-HUB
# Configure Docker authentication
gcloud auth configure-docker ${REGION}-docker.pkg.dev --quiet
# Verify repositories
gcloud artifacts repositories list --location=$REGION
Solution
Terminal window
# Create a simple Dockerfile
mkdir -p /tmp/ar-lab && cd /tmp/ar-lab
cat > Dockerfile << 'EOF'
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
COPY index.html /var/www/html/
CMD ["python3", "-m", "http.server", "8080", "--directory", "/var/www/html"]
EOF
cat > index.html << 'EOF'
<h1>Artifact Registry Lab</h1>
<p>Image built and pushed successfully.</p>
EOF
# Build the image
docker build -t my-web:v1.0.0 .
# Tag for Artifact Registry
docker tag my-web:v1.0.0 \
${REGION}-docker.pkg.dev/${PROJECT_ID}/app-images/my-web:v1.0.0
# Push
docker push ${REGION}-docker.pkg.dev/${PROJECT_ID}/app-images/my-web:v1.0.0
# List images in the repository
gcloud artifacts docker images list \
${REGION}-docker.pkg.dev/${PROJECT_ID}/app-images
Solution
Terminal window
# Wait for the scan to complete (usually takes 1-3 minutes after push)
echo "Waiting for vulnerability scan to complete..."
sleep 60
# List vulnerabilities
gcloud artifacts docker images describe \
${REGION}-docker.pkg.dev/${PROJECT_ID}/app-images/my-web:v1.0.0 \
--show-all-metadata
# If the above does not show vulnerabilities yet, check scan status:
gcloud artifacts docker images list \
${REGION}-docker.pkg.dev/${PROJECT_ID}/app-images \
--include-tags \
--format="table(package, version, createTime, tags)"
Solution
Terminal window
# Pull nginx through your Docker Hub cache (not directly from Docker Hub)
docker pull ${REGION}-docker.pkg.dev/${PROJECT_ID}/dockerhub-mirror/library/nginx:1.25-alpine
# Verify it was cached
gcloud artifacts docker images list \
${REGION}-docker.pkg.dev/${PROJECT_ID}/dockerhub-mirror
# Second pull will be faster (served from cache)
docker pull ${REGION}-docker.pkg.dev/${PROJECT_ID}/dockerhub-mirror/library/nginx:1.25-alpine
Solution
Terminal window
# View current IAM policy
gcloud artifacts repositories get-iam-policy app-images \
--location=$REGION
# Simulate granting a CI/CD SA write access (create SA first if needed)
gcloud iam service-accounts create ci-pipeline \
--display-name="CI Pipeline SA" 2>/dev/null || true
gcloud artifacts repositories add-iam-policy-binding app-images \
--location=$REGION \
--member="serviceAccount:ci-pipeline@${PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/artifactregistry.writer"
# Verify
gcloud artifacts repositories get-iam-policy app-images \
--location=$REGION \
--format="table(bindings.role, bindings.members)"
Solution
Terminal window
# Delete images
gcloud artifacts docker images delete \
${REGION}-docker.pkg.dev/${PROJECT_ID}/app-images/my-web:v1.0.0 \
--quiet --delete-tags 2>/dev/null || true
# Delete repositories
gcloud artifacts repositories delete app-images \
--location=$REGION --quiet
gcloud artifacts repositories delete dockerhub-mirror \
--location=$REGION --quiet
# Delete service account
gcloud iam service-accounts delete \
ci-pipeline@${PROJECT_ID}.iam.gserviceaccount.com --quiet 2>/dev/null || true
# Clean up local files
rm -rf /tmp/ar-lab
echo "Cleanup complete."
  • Standard Docker repository created with immutable tags
  • Container image built, tagged, and pushed successfully
  • Vulnerability scan results viewable
  • Docker Hub remote repository (cache) created and functional
  • Repository-level IAM configured for CI/CD service account
  • All resources cleaned up

Next up: Module 2.7: Cloud Run (Serverless Containers) --- Deploy stateless containers without managing infrastructure, master revisions and traffic splitting for blue/green deployments, and connect Cloud Run to your VPC for private backend access.