Skip to content
Go back

regclient: Container Image Lifecycle Without a Daemon

By SumGuy 9 min read
regclient: Container Image Lifecycle Without a Daemon
Contents

Your Registry Is a Landfill. Fix It.

If you’ve been running containers for more than six months, you already know the feeling: open Harbor or ghcr.io, scroll the tag list, and discover 847 tags for a single service. Half of them are SHA digests from a CI run that failed at 2 AM on a Tuesday. The other half are latest, latest-2, latest-old, and whatever dev-test-final-FINAL3 is.

Skopeo is great, if you haven’t read that one yet, go do that first, it’s the prereq here, but it’s mostly a copy-and-inspect tool. It doesn’t have opinions about your tag retention policy. It won’t walk your registry and prune untagged manifests on a schedule. It won’t declaratively mirror three registries into one with a YAML file.

That’s where regclient comes in. It’s a Go-based toolkit with three binaries: regctl for interactive registry surgery, regbot for declarative lifecycle policies, and regsync for cross-registry mirroring. Think of Skopeo as the scalpel and regclient as the whole OR suite.

And yes, zero Docker daemon required. Just you, the OCI registry API, and a terminal.


The Three Tools

regctl, Interactive Registry Surgery

regctl is the CLI you’ll reach for when you need to inspect, copy, tag, or manipulate images right now. Install it:

Terminal window
# Linux amd64
curl -L https://github.com/regclient/regclient/releases/latest/download/regctl-linux-amd64 \
-o /usr/local/bin/regctl
chmod +x /usr/local/bin/regctl
# Or via Docker (no install needed)
docker run --rm ghcr.io/regclient/regctl:latest --help

Basic inspection, no daemon, no pull, just the manifest:

Terminal window
# Inspect image config without pulling
regctl image inspect ghcr.io/myorg/myapp:v1.2.3
# Get the digest for a tag
regctl image digest ghcr.io/myorg/myapp:latest
# Copy between registries
regctl image copy ghcr.io/myorg/myapp:v1.2.3 registry.internal/myapp:v1.2.3
# List all tags
regctl tag ls ghcr.io/myorg/myapp

The image inspect output is the full OCI config JSON, layers, env vars, entrypoints, labels. Same data you’d get from docker inspect after a pull, but without dragging gigabytes of layers onto your machine first.

Multi-arch manifest list inspection is where regctl gets genuinely useful:

Terminal window
# Inspect the manifest index (multi-arch)
regctl manifest get ghcr.io/myorg/myapp:v1.2.3
# Show per-platform digests
regctl manifest get --format '{{range .Manifests}}{{.Platform.OS}}/{{.Platform.Architecture}}: {{.Digest}}\n{{end}}' \
ghcr.io/myorg/myapp:v1.2.3

You get the full platform breakdown, linux/amd64, linux/arm64, linux/arm/v7, with individual digests. Try doing that with just docker manifest inspect and enjoy the wall of JSON you have to grep through.

Image Promotion in CI

Here’s a real CI pattern: build on every commit, promote to staging on merge to main, promote to prod on a semver tag. regclient handles the promotion step without rebuilding anything:

Terminal window
# Promote: copy the exact SHA from ci to staging (no rebuild)
DIGEST=$(regctl image digest ghcr.io/myorg/myapp:${COMMIT_SHA})
regctl image copy ghcr.io/myorg/myapp@${DIGEST} ghcr.io/myorg/myapp:staging
# Tag it for prod on release
regctl image copy ghcr.io/myorg/myapp@${DIGEST} ghcr.io/myorg/myapp:${RELEASE_TAG}
regctl image copy ghcr.io/myorg/myapp@${DIGEST} ghcr.io/myorg/myapp:latest

The key here is copying by digest, not by tag. You’re guaranteeing the exact same image bytes move from CI → staging → prod. No “it worked in staging” nonsense because the tag silently pointed somewhere else.


regbot, Scripted Lifecycle Policies

This is the one that makes the landfill problem go away. regbot takes a YAML config where each policy is a Lua script, and that scripting model is the thing to understand up front. There’s no magic action: keep count: 5 declarative shorthand; you write a little Lua that lists tags and decides what to delete. More verbose than a one-liner, but you can express literally any retention rule you can think of.

regbot.yml
version: 1
defaults:
parallel: 4
interval: 24h
creds:
- registry: ghcr.io
user: "{{ env \"GHCR_USER\" }}"
pass: "{{ env \"GHCR_TOKEN\" }}"
scripts:
- name: Prune myapp CI debris
timeout: 10m
script: |
-- Keep anything tagged prod/staging/latest, delete the rest
-- once it's older than the 5 most recent semver releases.
local repo = "ghcr.io/myorg/myapp"
local tags = tag.ls(repo)
table.sort(tags)
local kept = 0
for i = #tags, 1, -1 do
local t = tags[i]
if t == "prod" or t == "staging" or t == "latest" then
-- always keep these
elseif t:match("^v%d") and kept < 5 then
kept = kept + 1 -- keep newest 5 semver tags
else
log("deleting " .. repo .. ":" .. t)
tag.delete(repo .. ":" .. t)
end
end

Run it:

Terminal window
# Dry run first — always
regbot once --dry-run -c regbot.yml
# Live run
regbot once -c regbot.yml
# Or as a scheduled daemon (runs the schedule defined in YAML)
regbot server -c regbot.yml

The --dry-run flag deserves emphasis, it disables every write to the registry, so your tag.delete() calls just log what they would do. Run it at least once before you let regbot anywhere near prod. The “keep prod/staging/latest + keep the newest 5 semver tags” pattern above is the one you want for 90% of services, it keeps your important tags, keeps a small rollback window of versioned releases, and cleans up the CI debris automatically.


regsync, Cross-Registry Mirroring

regsync solves the problem of: “I want a local mirror of upstream images so my cluster doesn’t pound Docker Hub rate limits, and I want it to stay current automatically.”

regsync.yml
version: 1
creds:
- registry: registry.internal
hostname: registry.internal
tls: disabled
sync:
- source: library/nginx
target: registry.internal/mirror/nginx
type: repository
tags:
allow:
- "^1\\.2[0-9]" # Only nginx 1.20+
- "latest"
deny:
- ".*-alpine$" # Skip alpine variants (we use debian)
- source: ghcr.io/myorg/myapp
target: registry.internal/prod/myapp
type: repository
tags:
allow:
- "^v[0-9]" # Only semver tags

Run it on a cron or as a daemon:

Terminal window
# One-shot sync
regsync once -c regsync.yml
# Daemon mode — syncs on the schedule defined in the YAML
regsync server -c regsync.yml

The tags.allow / tags.deny filters use Go regexes. They’re your rate-limit shield, you’re not pulling every tag from Docker Hub, just the ones your org actually uses.


Handling OCI Artifacts

Modern registries store more than just container images, OCI artifacts include Helm charts, SBOMs, Cosign signatures, and WASM modules. regctl handles them natively:

Terminal window
# List referrers (signatures, SBOMs) attached to an image
regctl artifact list ghcr.io/myorg/myapp:v1.2.3
# Copy image + all attached artifacts
regctl image copy --referrers \
ghcr.io/myorg/myapp:v1.2.3 \
registry.internal/myapp:v1.2.3

The --referrers flag is the key one. If you’re using Cosign to sign your images, a plain regctl image copy without --referrers will leave the signatures behind. Your Sigstore policy enforcement on the destination registry will fail, and you’ll spend 20 minutes convinced your signing workflow is broken when it’s actually just the copy step.


regclient vs Skopeo vs Crane

Honest comparison, no hype:

ToolStrengthsWeaknesses
SkopeoUniversal copy/inspect, great distros supportNo lifecycle policies, no scheduling
regctlManifest surgery, multi-arch, OCI artifactsMore complex, less distro packaging
Crane (Google)Minimal, fast, good Go libraryNo regbot/regsync equivalents

Skopeo is the general-purpose utility. It’s in every package manager, it copies between virtually every registry format including docker-daemon and dir/. It’s the right default tool for “I need to copy this image once.”

regclient (regctl/regbot/regsync) is the registry operations toolkit. If you need lifecycle policies, scheduled mirroring, or you’re building registry automation workflows, it’s the better fit. The scriptable regbot and YAML-driven regsync configs are something Skopeo doesn’t have at all.

Crane (from Google’s go-containerregistry) sits in the middle, it’s a solid CLI and an even better Go library for building registry tooling. If you’re writing Go code that needs to talk to registries, crane / go-containerregistry is the library you want. As a standalone CLI it’s more minimal than either Skopeo or regclient.

Pick the one that fits the job. They’re not mutually exclusive, regbot for scheduled cleanup, Skopeo in your CI scripts for one-off copies, Crane if you’re writing registry automation in Go.


Authentication Setup

All three tools read credentials from the same config file:

Terminal window
# Log in (stores creds in ~/.config/regclient/config.json)
regctl registry login ghcr.io
# Or set per-registry in config
regctl registry set registry.internal --tls disabled
# Check what registries are configured
regctl registry config

For CI, pass creds via environment variables in the YAML ({{ env "GHCR_TOKEN" }} as shown in the regbot config above) or via --user/--pass flags. Don’t hardcode credentials in YAML files that get committed. Your future self and your security team will both thank you.


Pruning Untagged Manifests in Harbor

Harbor doesn’t automatically garbage-collect untagged manifests, you end up with a graveyard of anonymous SHA digests consuming storage. regbot can’t delete bytes directly (the registry API only exposes tags and manifests, not storage), but it’s great at the half you actually script: nuking the stale tags that keep those manifests alive. Drop the tags, then let Harbor’s GC reclaim the now-dangling manifests:

harbor-cleanup.yml
version: 1
creds:
- registry: harbor.internal
user: "{{ env \"HARBOR_USER\" }}"
pass: "{{ env \"HARBOR_PASS\" }}"
scripts:
- name: Prune stale dev tags
timeout: 15m
script: |
-- Harbor leaves untagged manifests behind; regbot scripts work on
-- tags, so the practical move is to drop the stale tags that point
-- at them and let Harbor's GC reclaim the now-dangling manifests.
local repo = "harbor.internal/project/myapp"
for _, t in ipairs(tag.ls(repo)) do
if t:match("^dev%-") then
log("deleting " .. repo .. ":" .. t)
tag.delete(repo .. ":" .. t)
end
end

Run this nightly in a cron job (paired with Harbor’s own GC) and your storage growth goes from “buy more disks” to “actually manageable.”


The Bottom Line

If your registry tag list looks like a crime scene, regclient is the forensic cleanup crew. regctl for hands-on inspection and promotion, regbot for scripted retention policies that run themselves, regsync for mirroring that doesn’t hammer upstream rate limits.

Start with regbot once --dry-run pointed at your worst-offending repo. You’ll immediately see how many tags are deletable. Then automate it. Your 2 AM self, the one who can’t find which of the 400 CI tags is actually running in prod, will appreciate it.

The Docker daemon hasn’t been required for registry operations for years. Act like it.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
KV Cache Quantization: Free LLM Context, Almost

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts