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 Type | Example | Mutability | Use Case |
| Version + SHA | `1.4.2-a3f7b2c` | Immutable | Production deployments |
| Semantic version | `1.4.2` | Mutable (latest build) | Release tracking |
| Branch name | `feature-auth` | Mutable | Staging/preview |
| `latest` | `latest` | Mutable | Development only, never production |
| Date-based | `2026-03-15` | Immutable | Nightly 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
| Severity | Action | Timeline |
| Critical | Block deployment, fix immediately | Within 24 hours |
| High | Block deployment in production | Within 7 days |
| Medium | Allow deployment, track in backlog | Within 30 days |
| Low | Log and monitor | Next 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.
| Role | Pull | Push | Delete | Admin |
| CI/CD pipeline | Yes | Yes | No | No |
| Developer (staging) | Yes | Yes | No | No |
| Developer (production) | Yes | No | No | No |
| Production cluster | Yes | No | No | No |
| Registry admin | Yes | Yes | Yes | Yes |
<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
| Strategy | Storage Savings | Implementation |
| Multi-stage builds | 60-90% per image | Dockerfile refactor |
| Layer deduplication | 20-40% total | Use shared base images |
| Garbage collection | 30-50% total | Automated cleanup script |
| Compressed push | 15-25% transfer | Enabled by default |
| .dockerignore | 10-30% per image | Exclude 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.