Add `login --save --encrypt` flag: passwords are encrypted with openssl AES-256-CBC (PBKDF2) and stored as `enc:<base64>` in the config file. A master passphrase is prompted once per session and cached in memory. Both load_config() and resolve_registry_alias() detect the enc: prefix and decrypt transparently. The passphrase is passed to openssl via a temp file to avoid argv/env exposure. openssl is an optional dependency, checked on demand. Co-Authored-By: Claude Sonnet 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)
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 ~/.hubmanager.conf.
The file uses a simple KEY=VALUE format:
# ~/.hubmanager.conf
# chmod 600 ~/.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/.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.
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: ~/.hubmanager.conf)
--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/.hubmanager.conf
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.
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 |
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.