Container Registry, OCI Images, Cleanup, Authentication, Vulnerability Scanning, and Retention: Guided Hands-On Workflow and Core Operations
Inspect a disposable registry, publish or safely simulate one tiny OCI image, prove tag-to-digest identity, inspect metadata, exercise scanning or a fixture, and clean up without confusing tag deletion with garbage collection.
Learning objectives
- Inspect source/project/registry state before publishing.
- Publish or safely simulate one tiny disposable OCI image without requiring privileged infrastructure.
- Compare tag and digest references and capture registry metadata with glab/API.
- Run current container scanning or a clearly labeled fixture and verify image/report identity.
- Delete only verified disposable registry state after downstream-impact analysis.
1. Disposable scenario and two execution lanes
Create or reuse a disposable GitLab Free project such as
learner-example/ch23-registry-lab. The live lane
publishes one tiny image only if your project has an enabled
registry and you have suitable local/runner container tooling. The
fallback lane uses the exact same metadata/digest reasoning with an
OCI fixture and read-only registry output, so completion never
depends on paid compute, privileged Docker-in-Docker, or cloud
infrastructure.
2. Preflight: prove project, Git SHA, registry variables, and existing registry state
Before publishing, record the source state and inspect the registry:
git status --short --branch
git rev-parse HEAD
glab repo view --output json | jq '{path_with_namespace,default_branch,web_url}'
glab container-registry repository list --output json
In CI, check only presence of registry variables; never print the password:
test -n "$CI_REGISTRY"
test -n "$CI_REGISTRY_IMAGE"
test -n "$CI_REGISTRY_USER"
test -n "$CI_REGISTRY_PASSWORD"
printf 'registry=%s image_base=%s\n' "$CI_REGISTRY" "$CI_REGISTRY_IMAGE"
The image base should be the project registry path. Record the
project ID and expected nested repository
$CI_REGISTRY_IMAGE/ch23-lab.
3. Predict identity before mutation
Write evidence/ch23/predictions.md before any push.
Predict:
- source commit SHA and pipeline source;
-
repository path
<CI_REGISTRY_IMAGE>/ch23-lab; - immutable tag based on full commit SHA (or a short lab-only tag plus recorded commit);
- that a later human tag can move while the recorded digest must not;
- which actor may push/delete and which credential lifetime applies;
- what cleanup will delete (tag/repository metadata) versus what storage reclamation may happen later.
4. Live lane: publish one tiny image only when safe tooling exists
If an existing CI runner already provides safe container tooling, or
if you run this inside a disposable local container environment, you
may publish an intentionally tiny image. The following login pattern
is specifically for a CI job, where GitLab provides
the job-only registry variables and the password stays off the
command line. For a local publish outside CI, use a separately
created disposable least-privilege registry credential through
--password-stdin, then revoke it; do not expect
job-only CI_REGISTRY_* credentials to exist locally.
printf '%s' "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" --username "$CI_REGISTRY_USER" --password-stdin
IMAGE="$CI_REGISTRY_IMAGE/ch23-lab"
TAG="$CI_COMMIT_SHA"
# Build with your already-approved non-privileged/local builder, then:
docker tag ch23-local:lab "$IMAGE:$TAG"
docker push "$IMAGE:$TAG"
docker logout "$CI_REGISTRY"
Do not enable a privileged runner solely for this chapter. If the current runner lacks a safe builder, use the fixture lane below or publish from your disposable local container environment.
5. No-runner/no-registry fallback: an OCI fixture still proves the identity model
Create a local evidence fixture representing two observations of the same tag at different times:
{
"repository": "registry.example.invalid/learner/ch23-lab",
"observations": [
{"tag":"latest","digest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","source_sha":"1111111111111111111111111111111111111111"},
{"tag":"latest","digest":"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","source_sha":"2222222222222222222222222222222222222222"}
]
}
Both rows have tag latest; only the digest
distinguishes exact content. Use this fixture for the scan/retention
exercises when live registry compute is unavailable.
6. Inspect repository and tag metadata after publish
Once the live repository exists, use current glab commands:
glab container-registry repository list --include-tags-count --output json > evidence/ch23/repositories.json
REPO_ID="$(jq -r 'map(select(.path | endswith("/ch23-lab")))[0].id // empty' evidence/ch23/repositories.json)"
test -n "$REPO_ID"
glab container-registry tag list "$REPO_ID" --details --per-page 100 --output json > evidence/ch23/tags.json
jq 'map({name,digest,total_size,created_at})' evidence/ch23/tags.json
Record the digest returned for the commit-derived tag. The repository ID is a GitLab metadata identifier; it is not the OCI digest.
7. Compare tag pull and digest pull
With safe local tooling, first pull by tag, then by the recorded digest. The expected image identity is the digest returned by the registry metadata:
IMAGE="$CI_REGISTRY_IMAGE/ch23-lab"
TAG="<recorded-tag>"
DIGEST="sha256:<recorded-digest>"
docker pull "$IMAGE:$TAG"
docker pull "$IMAGE@$DIGEST"
docker image inspect "$IMAGE@$DIGEST" --format '{{json .RepoDigests}}'
If a later tag inspection points elsewhere, the digest reference still identifies the original manifest. For multi-platform images, distinguish an index digest from a selected platform-manifest digest.
8. Run current container scanning only when runner/image prerequisites are available
The current GitLab-managed container scanning template requires a
test stage and a suitable runner. A minimal
configuration looks like:
stages: [test]
include:
- template: Jobs/Container-Scanning.gitlab-ci.yml
container_scanning:
variables:
CS_IMAGE: "$CI_REGISTRY_IMAGE/ch23-lab:$CI_COMMIT_SHA"
After the job, preserve
gl-container-scanning-report.json and the CycloneDX
SBOM if produced. If no compatible runner/compute is available, use
a clearly labeled synthetic report fixture and still practice
binding report metadata to the expected image reference/digest. Do
not claim the fixture is a real vulnerability assessment.
9. Verify that scan evidence belongs to the intended image
Before interpreting severity counts, compare the scan job’s
CS_IMAGE, pipeline SHA, registry tag metadata, and
recorded digest. The dangerous failure is a perfectly valid scan of
the wrong image. Preserve the report checksum as evidence:
sha256sum gl-container-scanning-report.json 2>/dev/null || echo "fixture/report not present in this lane"
A scan result should never be detached from the image identity it analyzed.
10. Simulate cleanup policy before enabling it
For a disposable project, design a rule such as “keep
latest, keep release tags, keep the newest five commit
tags, remove matching development tags older than 30 days.” Before
enabling it, classify a fixture table manually:
| Tag | Age | Keep pattern? | Protected/immutable? | Predicted action |
|---|---|---|---|---|
latest |
120d | special | no | Keep — cleanup policy always preserves latest. |
v1.4.0 |
80d | ^v |
optional | Keep. |
dev-a1b2 |
45d | no | no | Candidate for removal if regex/age match. |
dev-c3d4 |
2d | no | no | Keep — too recent. |
Do not use .* first against a valuable registry. A
cleanup policy can require multiple runs on GitLab.com;
disappearance from UI and physical storage reclamation are different
observations.
11. Challenge: choose the correct control
You need a production deployment to keep rolling back to the exact
release even if humans retag stable. Which control is
primary?
Answer before revealing: record/deploy by digest. Tag protection/immutability can strengthen governance, but digest pinning is the content identity. Cleanup must preserve referenced digests/tags long enough for rollback policy.
12. Cleanup only after proving no downstream references
List the exact tag and record its digest before deletion. Then, in the disposable project only:
REPO_ID="<verified-repository-id>"
TAG="<verified-lab-tag>"
glab container-registry tag view "$REPO_ID" "$TAG" --output json > evidence/ch23/tag-before-delete.json
# Destructive: delete only the verified disposable tag.
glab container-registry tag delete "$REPO_ID" "$TAG" --yes
if glab container-registry tag view "$REPO_ID" "$TAG" --output json >/dev/null 2>&1; then
echo "tag still exists" >&2
exit 1
fi
echo "verified tag removal"
Deleting the tag does not mean administrators should immediately run garbage collection. If the whole repository is disposable and you choose to delete it, re-list it, verify no consumer references, use the repository-delete command with an explicit destructive warning, and verify absence.
Knowledge check
Why is
docker login -p "$CI_REGISTRY_PASSWORD" inferior to
--password-stdin?
Command-line secrets can be exposed through shell/process history and diagnostics. Standard input avoids placing the password directly in the command arguments.
What should you record before deleting a lab tag?
Owning project/repository ID and path, exact tag, current digest, source/pipeline provenance, and evidence that no downstream deployment/consumer relies on it.
A scan report exists, but the tag moved after the scan. What do you verify first?
Which digest/image reference the scan job actually analyzed, then compare it with the deployment digest.
Why might storage not drop immediately after tag deletion?
Layers/manifests can remain referenced or await registry garbage collection/storage lifecycle processing.
What is the free-compatible fallback if you lack safe container-build compute?
Use the provided OCI/tag/digest and scan-report fixtures plus read-only GitLab metadata. Do not enable privileged infrastructure just for the lab.
Summary
You inspected before mutation, separated tag names from digest identity, used short-lived CI authentication, bound scan evidence to an image, designed retention before cleanup, and verified destructive tag removal. The next lesson turns these mechanics into production architecture choices.
Official references
Primary sources used for the current GitLab 19.3 behavior taught in this lesson:
- GitLab Docs — Container Registry
- GitLab Docs — Authenticate with the Container Registry
- GitLab Docs — Reduce Container Registry storage
- GitLab Docs — Protected container repositories
- GitLab Docs — Protected container tags
- GitLab Docs — Immutable container tags
- GitLab Docs — Container Registry API
- GitLab Docs — Container scanning
- GitLab Docs — Container Registry metadata database
- GitLab Docs — Predefined CI/CD variables
- GitLab CLI — container-registry repository list
- GitLab CLI — container-registry tag list
- GitLab CLI — container-registry tag view
- GitLab CLI — container-registry tag delete
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.