Container Registry Management: Image Tagging, Vulnerability Scanning, Garbage Collection, and CI/CD

Published 17 min read
dockercontainer-registrysecurityci-cddevopsvulnerability-scanning

A container registry is the critical supply chain link between your CI/CD pipeline and your production infrastructure. Mismanaged registries accumulate thousands of unused images, harbor unscanned vulnerabilities, and lack access controls. This guide covers image tagging strategies that prevent deployment confusion, automated vulnerability scanning, garbage collection to control storage costs, access control for multi-team environments, and CI/CD integration patterns.

According to JFrog's 2026 DevSecOps report, the average organization stores 4,200 container images in their registry, of which 62% are unused. Unscanned images contain an average of 14 high-severity vulnerabilities. Organizations with automated registry management reduce security incidents by 71% and storage costs by 45%.

---

Image Tagging Strategy

Never use `latest` in production. A tag should uniquely identify the exact build and source code.

Recommended Tagging Pattern

<h1>Pattern: {app}-{semver}-{git-sha-short}</h1>

<h1>Example: api-1.4.2-a3f7b2c</h1>

<h1>Build and tag in CI/CD</h1>

VERSION=$(cat package.json | jq -r .version)

SHA=$(git rev-parse --short HEAD)

IMAGE="registry.arxentra.cloud/myapp/api"

<h1>Tag with version + SHA (primary, immutable)</h1>

docker build -t "${IMAGE}:${VERSION}-${SHA}" .

<h1>Tag with version (mutable, points to latest build of this version)</h1>

docker tag "${IMAGE}:${VERSION}-${SHA}" "${IMAGE}:${VERSION}"

<h1>Tag with branch for staging/preview</h1>

BRANCH=$(git branch --show-current | sed "s/\//-/g")

docker tag "${IMAGE}:${VERSION}-${SHA}" "${IMAGE}:${BRANCH}"

<h1>Push all tags</h1>

docker push "${IMAGE}:${VERSION}-${SHA}"

docker push "${IMAGE}:${VERSION}"

docker push "${IMAGE}:${BRANCH}"

Tag Type Reference

Tag TypeExampleMutabilityUse Case

Version + SHA`1.4.2-a3f7b2c`ImmutableProduction deployments
Semantic version`1.4.2`Mutable (latest build)Release tracking
Branch name`feature-auth`MutableStaging/preview
`latest``latest`MutableDevelopment only, never production
Date-based`2026-03-15`ImmutableNightly builds

---

Vulnerability Scanning

Scan every image before it reaches production. Block deployments of images with critical vulnerabilities.

Automated Scanning Pipeline

<h1>GitHub Actions vulnerability scanning</h1>

name: Container Security Scan

on:

push:

branches: [main]

pull_request:

jobs:

scan:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

- name: Build image

run: docker build -t myapp:${{ github.sha }} .

- name: Scan with Trivy

uses: aquasecurity/trivy-action@master

with:

image-ref: myapp:${{ github.sha }}

format: table

severity: HIGH,CRITICAL

exit-code: 1 # Fail the build

ignore-unfixed: true # Only fixable vulns

- name: Scan with Grype (secondary)

run: |

curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s

./bin/grype myapp:${{ github.sha }} --fail-on critical

Scan Policy Recommendations

SeverityActionTimeline

CriticalBlock deployment, fix immediatelyWithin 24 hours
HighBlock deployment in productionWithin 7 days
MediumAllow deployment, track in backlogWithin 30 days
LowLog and monitorNext scheduled update

---

Garbage Collection

Without garbage collection, registries grow indefinitely. A typical CI/CD pipeline creating 5 images per day accumulates 1,800+ images per year per application.

Retention Policy

<h1>registry-cleanup-policy.yaml</h1>

retention_rules:

# Keep all production-tagged images

- tag_pattern: "^[0-9]+\.[0-9]+\.[0-9]+$"

keep: all

reason: "Semantic versions are release candidates"

# Keep last 10 builds per branch

- tag_pattern: "^(main

develop
staging)$"

keep_last: 10

reason: "Branch tags for quick rollback"

# Delete feature branch images after 14 days

- tag_pattern: "^feature-.*"

max_age_days: 14

reason: "Feature branches are short-lived"

# Delete untagged manifests after 7 days

- untagged: true

max_age_days: 7

reason: "Orphaned layers from replaced tags"

<h1>Automated cleanup script</h1>

#!/bin/bash

REGISTRY="registry.arxentra.cloud"

REPO="myapp/api"

<h1>Delete feature branch images older than 14 days</h1>

CUTOFF=$(date -d "14 days ago" +%s)

for tag in $(crane ls "${REGISTRY}/${REPO}" | grep "^feature-"); do

created=$(crane manifest "${REGISTRY}/${REPO}:${tag}" | \

jq -r ".config.created // empty" | xargs -I{} date -d {} +%s)

if [ -n "$created" ] && [ "$created" -lt "$CUTOFF" ]; then

echo "Deleting ${tag} (created $(date -d @${created}))"

crane delete "${REGISTRY}/${REPO}:${tag}"

fi

done

---

Access Control

Registry access should follow the principle of least privilege.

RolePullPushDeleteAdmin

CI/CD pipelineYesYesNoNo
Developer (staging)YesYesNoNo
Developer (production)YesNoNoNo
Production clusterYesNoNoNo
Registry adminYesYesYesYes

<h1>Create read-only token for production cluster</h1>

docker login registry.arxentra.cloud \

--username prod-reader \

--password-stdin < /secrets/registry-token

<h1>Create push token for CI/CD</h1>

docker login registry.arxentra.cloud \

--username ci-pusher \

--password-stdin < /secrets/ci-token

Arxentra provides team-based registry access control. See Services Overview for container registry plans.

---

CI/CD Integration

Complete Build-Scan-Push Pipeline

<h1>.github/workflows/container.yml</h1>

name: Build, Scan, Push

on:

push:

branches: [main]

env:

REGISTRY: registry.arxentra.cloud

IMAGE: myapp/api

jobs:

build-and-push:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

- name: Set image metadata

id: meta

run: |

VERSION=$(cat package.json | jq -r .version)

SHA=$(echo ${{ github.sha }} | head -c 7)

echo "tag=${VERSION}-${SHA}" >> $GITHUB_OUTPUT

- name: Build

run: docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ steps.meta.outputs.tag }} .

- name: Scan

uses: aquasecurity/trivy-action@master

with:

image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ steps.meta.outputs.tag }}

exit-code: 1

severity: CRITICAL

- name: Push

run: |

echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ${{ env.REGISTRY }} -u ci --password-stdin

docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ steps.meta.outputs.tag }}

---

Storage Cost Optimization

StrategyStorage SavingsImplementation

Multi-stage builds60-90% per imageDockerfile refactor
Layer deduplication20-40% totalUse shared base images
Garbage collection30-50% totalAutomated cleanup script
Compressed push15-25% transferEnabled by default
.dockerignore10-30% per imageExclude dev files

Check Pricing for registry storage costs and included storage by plan.

---

Frequently Asked Questions

Should I run my own registry or use a managed service?

Use a managed service unless you have specific compliance requirements that prevent it. Self-hosted registries (Harbor, Quay) require storage management, TLS, authentication, garbage collection, and high availability. Managed registries handle all of this. The operational cost of self-hosting exceeds managed service pricing for most teams.

How do I handle multi-architecture images (ARM and AMD64)?

Use Docker buildx to create multi-architecture manifests. CI/CD builds both architectures and pushes a manifest list. The registry serves the correct architecture automatically based on the pulling client.

What is the best way to share base images across teams?

Create a shared base image repository maintained by the platform team. Publish base images with security hardening, standard tooling, and health check utilities. Application teams extend these base images. This reduces duplication and ensures security standards. See Documentation for team collaboration patterns.

How do I migrate images between registries?

Use `crane copy` (from Google's go-containerregistry) or `skopeo copy`. Both copy images between registries without pulling to local Docker. This is faster and works in CI/CD environments without Docker installed.

What happens if my registry goes down?

Running containers are not affected - they already have the image. New deployments and scaling operations will fail. Mitigate by caching images on nodes (Kubernetes does this by default), using a registry mirror, or pulling images to a local cache as part of your deployment pipeline.