Use keyctl (keyutils) to cache the master passphrase in the kernel keyring with a configurable TTL (default 5 min). New unlock/lock subcommands for manual cache control. keyctl is optional — silently skipped if not installed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
hubmanager
A Bash CLI tool to manage Docker Registry images remotely. Supports Docker Hub and any self-hosted Docker Registry v2 API, with flexible authentication.
Requirements
- Bash 4.0+
- curl
- jq
- openssl (optional — required only when using encrypted config values)
- keyctl (optional — from
keyutils; enables passphrase caching across invocations)
Installation
# System-wide
sudo install -m 755 hubmanager /usr/local/bin/hubmanager
# User-local
install -m 755 hubmanager ~/bin/hubmanager
Quick Start
# Test your credentials and save them
hubmanager login --registry https://registry.example.com \
--user admin --password secret --save
# Save with password encrypted at rest
hubmanager login --registry https://registry.example.com \
--user admin --password secret --save --encrypt
# List all repositories
hubmanager list
# List tags for an image
hubmanager tags myuser/myapp
# Inspect an image
hubmanager inspect myuser/myapp:latest
# Delete an old tag
hubmanager delete myuser/myapp:v1.0.0
# Prune old tags (keep the 3 most recent)
hubmanager prune myuser/myapp --keep 3 --dry-run
Configuration
Credentials and registry settings are stored in ~/.config/hubmanager.conf.
The file uses a simple KEY=VALUE format:
# ~/.config/hubmanager.conf
# chmod 600 ~/.config/hubmanager.conf
# Default registry and credentials
REGISTRY=https://registry.example.com
USERNAME=admin
PASSWORD=mysecretpassword
# Named registry aliases (use with --registry <alias>)
REGISTRY_STAGING_URL=https://staging-registry.example.com
REGISTRY_STAGING_USERNAME=deploy
REGISTRY_STAGING_PASSWORD=deploytoken
REGISTRY_HUB_URL=https://registry-1.docker.io
REGISTRY_HUB_USERNAME=myuser
REGISTRY_HUB_PASSWORD=myhubtoken
The file must be readable only by the owner (
chmod 600). hubmanager will warn if permissions are too open.
Named aliases let you switch registries with a short name:
hubmanager --registry staging list
hubmanager --registry hub tags myuser/myapp
Encrypted credentials
Use login --save --encrypt to store the password encrypted with AES-256-CBC.
A master passphrase is prompted at save time (with confirmation) and on every subsequent
invocation that reads the config file. Requires openssl.
hubmanager login --registry https://registry.example.com \
--user admin --password secret --save --encrypt
# New master passphrase: ****
# Confirm master passphrase: ****
# Login Succeeded — bearer auth, registry: https://registry.example.com
# Credentials saved to /home/user/.config/hubmanager.conf
# Password stored encrypted (AES-256-CBC). Master passphrase required on each use.
The config file stores an enc: prefixed ciphertext:
REGISTRY=https://registry.example.com
USERNAME=admin
PASSWORD=enc:U2FsdGVkX1+...base64ciphertext...
The enc: prefix also works for named alias passwords (REGISTRY_<ALIAS>_PASSWORD).
On every command that reads the config, the master passphrase is prompted once and cached
for the duration of the session.
Passphrase caching across invocations
If keyctl (from the keyutils package) is installed, the master passphrase is
automatically cached in the Linux kernel keyring for 5 minutes. Subsequent commands
within that window will not re-prompt.
# First command prompts for passphrase, caches it for 5 min
hubmanager list
# Runs without prompting (within cache window)
hubmanager tags myuser/myapp
# Pre-cache before a scripted batch
hubmanager unlock
hubmanager list && hubmanager tags myuser/myapp && hubmanager inspect myuser/myapp:latest
# Clear cache immediately
hubmanager lock
# Custom timeout (10 minutes)
hubmanager unlock --cache-timeout 600
If keyctl is not installed, passphrase caching is silently skipped — each invocation
prompts as before.
Global Options
hubmanager [OPTIONS] <command> [COMMAND OPTIONS]
-r, --registry <url> Registry base URL
Default: https://registry-1.docker.io
-u, --user <username> Username (overrides config file)
-p, --password <pass> Password or token (overrides config file)
--config <file> Config file path (default: ~/.config/hubmanager.conf)
--cache-timeout <s> Passphrase cache TTL in seconds (default: 300)
--json Output raw JSON (pipe-friendly)
--no-color Disable ANSI color
-v, --verbose Show HTTP request details (with auth redacted)
-q, --quiet Suppress all non-error output
-h, --help Show help
--version Show version
Commands
login — Test and save credentials
hubmanager login [--registry URL] [--user USER] [--password PASS] [--save] [--encrypt]
Validates credentials against the registry. Use --save to write them to the config file.
Add --encrypt to store the password encrypted with AES-256-CBC (requires openssl).
hubmanager login --registry https://registry.example.com \
--user admin --password secret --save
# Login Succeeded — bearer auth, registry: https://registry.example.com
# Credentials saved to /home/user/.config/hubmanager.conf
unlock — Cache master passphrase
hubmanager unlock [--cache-timeout SECONDS]
Prompts for the master passphrase and stores it in the Linux kernel keyring
for the configured timeout (default: 300 seconds / 5 minutes). Useful before
running a batch of commands. Requires keyctl (keyutils package).
hubmanager unlock
# hubmanager master passphrase: ****
# Passphrase cached for 300s.
# Custom timeout
hubmanager unlock --cache-timeout 600
lock — Clear cached passphrase
hubmanager lock
Immediately revokes the cached passphrase from the kernel keyring.
hubmanager lock
# Passphrase cache cleared.
list — List repositories
hubmanager list [--limit N] [--last REPO]
Lists all repositories in the registry. On Docker Hub, lists repositories for the authenticated user (Docker Hub restricts the _catalog endpoint).
hubmanager list
# REPOSITORY
# myuser/myapp
# myuser/myapi
# myuser/nginx-custom
# Paginate
hubmanager list --limit 50 --last myuser/myapi
# JSON output
hubmanager list --json | jq '.repositories[]'
tags — List tags for an image
hubmanager tags <image> [--limit N] [--last TAG]
hubmanager tags myuser/myapp
# TAG
# latest
# v1.2.3
# v1.2.2
# develop
# Official Docker Hub images
hubmanager tags nginx
# JSON
hubmanager tags myuser/myapp --json | jq '.tags[]'
inspect — Show image details
hubmanager inspect <image>:<tag|digest> [--platform OS/ARCH]
Shows manifest digest, size, OS/arch, creation date, labels, and layer breakdown.
hubmanager inspect myuser/myapp:latest
# Image: myuser/myapp:latest
# Digest: sha256:abc123...
# MediaType: application/vnd.docker.distribution.manifest.v2+json
# CompressedSize: 32.7 MB (34299597 bytes)
# OS/Arch: linux/amd64
# Created: 2024-01-15T10:30:00.000000000Z
# Labels:
# maintainer=dev@example.com
# version=1.2.3
# Layers: 3
# [0] sha256:1111... (27.8 MB)
# [1] sha256:2222... (4.4 MB)
# [2] sha256:3333... (1000.0 KB)
# Multi-arch image — shows all platforms
hubmanager inspect nginx:latest
# Multi-arch — drill into a specific platform
hubmanager inspect nginx:latest --platform linux/arm64
# Inspect by digest
hubmanager inspect myuser/myapp@sha256:abc123...
# JSON output (includes _digest field)
hubmanager inspect myuser/myapp:latest --json | jq .
delete — Delete a tag or manifest
hubmanager delete <image>:<tag|digest> [--yes]
Deletes a manifest by resolving the tag to its content-addressable digest, then issuing a DELETE. Requires REGISTRY_STORAGE_DELETE_ENABLED=true on self-hosted registries. Not supported on Docker Hub.
hubmanager delete myuser/myapp:v1.0.0
# About to delete: myuser/myapp @ sha256:abc123...
# Registry: https://registry.example.com
# Type 'yes' to confirm: yes
# Deleted: myuser/myapp @ sha256:abc123...
# Skip confirmation
hubmanager delete myuser/myapp:v1.0.0 --yes
# Delete by digest directly
hubmanager delete myuser/myapp@sha256:abc123... --yes
copy — Copy or retag an image
hubmanager copy <src-image>:<tag> <dst-image>:<tag> [options]
Options:
--src-registry URL Source registry (default: global --registry)
--dst-registry URL Destination registry (default: global --registry)
--src-user USER Source username
--src-password PASS Source password
--dst-user USER Destination username
--dst-password PASS Destination password
--platform OS/ARCH Copy only one platform from a multi-arch image
Same-registry retag — attempts cross-repo blob mount (no data transfer):
hubmanager copy myuser/myapp:v1.2.3 myuser/myapp:stable
Cross-registry copy — streams blobs from source to destination:
hubmanager copy myuser/myapp:latest \
--src-registry https://registry-1.docker.io \
--dst-registry https://registry.example.com \
mycompany/myapp:latest
Copy specific platform from a multi-arch image:
hubmanager copy nginx:latest myuser/nginx-amd64:latest --platform linux/amd64
prune — Delete outdated tags
hubmanager prune <image> [options]
Options:
--keep N Number of recent tags to keep (default: 3)
--older-than DAYS Delete tags older than N days (overrides --keep)
--exclude PATTERN Extended regex of tags to never delete
Default: "^(latest|stable|main|master|release)$"
--no-exclude Disable the default exclusion pattern
-n, --dry-run Show what would be deleted without deleting
-y, --yes Skip confirmation prompt
Tags are sorted by image creation date (newest first). The newest N are kept; the rest are deleted. Tags matching the exclusion pattern are always preserved.
# Preview: keep 5 most recent, protect default tags
hubmanager prune myuser/myapp --keep 5 --dry-run
# Keep 3 most recent, auto-confirm
hubmanager prune myuser/myapp --keep 3 --yes
# Delete anything older than 30 days
hubmanager prune myuser/myapp --older-than 30 --dry-run
# Custom exclusion: never delete latest or any semver tag
hubmanager prune myuser/myapp --keep 5 \
--exclude "^(latest|v[0-9]+\.[0-9]+(\.[0-9]+)?)$"
# Prune everything (no exclusions)
hubmanager prune myuser/myapp --keep 1 --no-exclude --dry-run
Authentication
hubmanager automatically detects the authentication method by probing the registry's /v2/ endpoint:
| Registry type | Auth method |
|---|---|
| Docker Hub | Bearer tokens via auth.docker.io |
| Harbor, self-hosted with token server | Bearer tokens via registry-configured realm |
| Basic-auth self-hosted | HTTP Basic Auth on every request |
| Public/anonymous registries | No auth |
Bearer tokens are cached in memory for the duration of the session and refreshed automatically when they expire.
Docker Hub notes
listuses the Docker Hub REST API (hub.docker.com) because the_catalogendpoint is restricted on Docker Hub.deleteis not supported via the v2 API on Docker Hub. Use the web UI at https://hub.docker.com.pruneis not supported on Docker Hub for the same reason.
Cleaning up empty repositories
The Docker Registry v2 API does not provide an endpoint to delete repositories.
After deleting all tags from a repository, it will still appear in hubmanager list.
This is a limitation of the registry spec, not of hubmanager.
To remove empty repositories, you need direct access to the registry host:
# 1. Delete the repository directory from storage
rm -rf /var/lib/registry/docker/registry/v2/repositories/<repo-name>
# 2. Run garbage collection to reclaim disk space
docker exec <registry-container> registry garbage-collect /etc/docker/registry/config.yml
# 3. Restart the registry so the catalog refreshes
docker restart <registry-container>
JSON output
All commands support --json for machine-readable output:
# Get all tags as a JSON array
hubmanager tags myapp --json | jq '.tags'
# Get digest of latest
hubmanager inspect myapp:latest --json | jq '._digest'
# Delete and capture result
hubmanager delete myapp:old --yes --json
# {"deleted":true,"digest":"sha256:..."}
# Prune and capture counts
hubmanager prune myapp --keep 3 --yes --json
# {"deleted":5,"failed":0}
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General / usage error |
| 2 | Authentication failure |
| 3 | Resource not found (404) |
| 4 | Permission denied (403) |
| 5 | Registry server error (5xx) |
| 6 | Network / connectivity error |
| 7 | Operation not supported by registry |
Examples
# Mirror all tags of an image to a private registry
for tag in $(hubmanager tags nginx --json | jq -r '.tags[]'); do
hubmanager copy nginx:$tag \
--src-registry https://registry-1.docker.io \
--dst-registry https://registry.example.com \
mycompany/nginx:$tag
done
# List images older than 60 days (dry run)
hubmanager prune myuser/myapp --older-than 60 --dry-run
# Get the SHA256 digest of the production image
DIGEST=$(hubmanager inspect myuser/myapp:production --json | jq -r '._digest')
echo "Production image: $DIGEST"
# Promote staging image to production (retag)
hubmanager copy myuser/myapp:staging myuser/myapp:production
Configuration reference
| Key | Description |
|---|---|
REGISTRY |
Default registry URL |
USERNAME |
Default username |
PASSWORD |
Default password or token; prefix with enc: for encrypted values |
CACHE_TIMEOUT |
Passphrase keyring cache TTL in seconds (default: 300) |
REGISTRY_<ALIAS>_URL |
URL for a named registry alias |
REGISTRY_<ALIAS>_USERNAME |
Username for a named alias |
REGISTRY_<ALIAS>_PASSWORD |
Password for a named alias (supports enc: prefix) |
Aliases are case-insensitive and treat - as _. For example, alias my-reg maps to REGISTRY_MY_REG_URL.