- Global flags (--user, --registry, --password) now work after subcommand name - Fix raw_http variables lost in subshells by persisting to temp files - Remove config inline-comment stripping that truncated base64 ciphertext - Add trailing newline to openssl decrypt pipe input - Move config path to ~/.config/hubmanager.conf - Add "Cleaning up empty repositories" section to README Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
12 KiB
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 ~/.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.
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)
--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
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 |
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.