cainy / laravel-dockhand
A Laravel Package for interacting with registries following the Open Container Initiative Distribution Specification
Fund package maintenance!
Requires
- php: ^8.4
- ext-openssl: *
- christian-riesen/base32: ^1.6
- illuminate/contracts: ^10.0||^11.0||^12.0||^13.0
- laravel/forge-sdk: ^3.21
- lcobucci/clock: ^3.3
- lcobucci/jwt: ^5.5
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^2.9||^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^7.10.0||^8.1.1
- orchestra/testbench: ^8.22.0||^9.0.0||^10.0.0||^11.0
- pestphp/pest: ^3.0||^4.0
- pestphp/pest-plugin-arch: ^3.0||^4.0
- pestphp/pest-plugin-laravel: ^3.0||^4.0
- phpstan/extension-installer: ^1.3||^2.0
- phpstan/phpstan-deprecation-rules: ^1.1||^2.0
- phpstan/phpstan-phpunit: ^1.3||^2.0
This package is auto-updated.
Last update: 2026-03-06 21:12:16 UTC
README
A Laravel package for interacting with container registries following the OCI Distribution Specification.
Requirements
- PHP 8.4+
- Laravel 10, 11, 12, or 13
Installation
Install the package via Composer:
composer require cainy/laravel-dockhand
Publish the config file:
php artisan vendor:publish --tag="dockhand-config"
Configuration
Dockhand uses a multi-connection architecture. Each connection has its own driver, base URI, authentication, and logging configuration.
Supported Drivers
| Driver | Description |
|---|---|
distribution |
Standard OCI Distribution registry (Docker Registry, Harbor, etc.) |
zot |
Zot registry with extension support (search, user preferences, tag deletion) |
Supported Auth Drivers
| Auth Driver | Description |
|---|---|
jwt |
ECDSA JWT token authentication (for token-based registries) |
basic |
HTTP Basic authentication |
bearer |
Static Bearer token |
apikey |
API key authentication |
null |
No authentication |
Default Connection
The default connection is configured via environment variables:
DOCKHAND_CONNECTION=default DOCKHAND_DRIVER=distribution DOCKHAND_BASE_URI=http://localhost:5000/v2/ DOCKHAND_AUTH_DRIVER=jwt DOCKHAND_PRIVATE_KEY=/path/to/private_key.pem DOCKHAND_PUBLIC_KEY=/path/to/public_key.pem DOCKHAND_AUTHORITY_NAME=my_auth DOCKHAND_REGISTRY_NAME=my_registry DOCKHAND_LOG_DRIVER=stack
Multiple Connections
Define additional connections in config/dockhand.php:
'connections' => [ 'default' => [ 'driver' => env('DOCKHAND_DRIVER', 'distribution'), 'base_uri' => env('DOCKHAND_BASE_URI', 'http://localhost:5000/v2/'), 'logging' => [ 'driver' => env('DOCKHAND_LOG_DRIVER', 'stack'), ], 'auth' => [ 'driver' => env('DOCKHAND_AUTH_DRIVER', 'jwt'), 'jwt_private_key' => env('DOCKHAND_PRIVATE_KEY'), 'jwt_public_key' => env('DOCKHAND_PUBLIC_KEY'), 'authority_name' => env('DOCKHAND_AUTHORITY_NAME', 'auth'), 'registry_name' => env('DOCKHAND_REGISTRY_NAME', 'registry'), ], ], 'staging' => [ 'driver' => 'zot', 'base_uri' => env('ZOT_STAGING_BASE_URI', 'http://localhost:5050/v2/'), 'logging' => [ 'driver' => env('ZOT_STAGING_LOG_DRIVER', 'stack'), ], 'auth' => [ 'driver' => 'basic', 'username' => env('ZOT_STAGING_USERNAME'), 'password' => env('ZOT_STAGING_PASSWORD'), ], ], 'prod' => [ 'driver' => 'zot', 'base_uri' => env('ZOT_PROD_BASE_URI'), 'logging' => [ 'driver' => env('ZOT_PROD_LOG_DRIVER', 'stack'), ], 'auth' => [ 'driver' => 'apikey', 'api_key' => env('ZOT_PROD_API_KEY'), ], ], ],
Usage
Basic Usage
use Cainy\Dockhand\Facades\Dockhand; // Check if the registry is online Dockhand::isOnline(); // bool // Get the API version Dockhand::getApiVersion(); // RegistryApiVersion enum
Repositories & Tags
// List all repositories $repos = Dockhand::getRepositories(); // With pagination $page = Dockhand::getRepositories(limit: 10); // $page is a PaginatedResult when limit is set $page->items; // Collection<int, string> $page->hasMore(); // bool $page->nextUrl; // ?string — pass to next request // List tags of a repository $tags = Dockhand::getTagsOfRepository('library/nginx'); // With pagination $page = Dockhand::getTagsOfRepository('library/nginx', limit: 20);
Manifests
// Get a manifest (returns ImageManifest or ManifestList depending on content) $manifest = Dockhand::getManifest('library/nginx', 'latest'); if ($manifest->isManifestList()) { // ManifestList — multi-platform image $entry = $manifest->findManifestListEntryByPlatform( Platform::create('linux', 'amd64') ); // Fetch the platform-specific manifest from an entry $imageManifest = Dockhand::getManifestFromManifestListEntry($entry); } else { // ImageManifest — single-platform image $imageManifest = $manifest; } // Access manifest properties $imageManifest->digest; $imageManifest->config; // ImageConfigDescriptor $imageManifest->layers; // Collection<int, ImageLayerDescriptor> $imageManifest->getSize(); // Head request (lightweight — returns digest, content length, media type) $head = Dockhand::headManifest('library/nginx', 'latest'); $head->digest; $head->contentLength; $head->mediaType; // Push a manifest $result = Dockhand::putManifest('library/nginx', 'latest', $manifest); $result->digest; $result->location;
Blobs
// Download a blob $data = Dockhand::getBlob('library/nginx', 'sha256:abc123...'); // Get blob size without downloading $size = Dockhand::getBlobSize('library/nginx', 'sha256:abc123...'); // Get parsed image config from a manifest's config descriptor $config = Dockhand::getImageConfigFromDescriptor($imageManifest->config); $config->platform; // Platform $config->created; // Carbon
Blob Uploads
// Monolithic upload (single request) $result = Dockhand::uploadBlob('library/nginx', $data, $digest); // Chunked upload $upload = Dockhand::initiateBlobUpload('library/nginx'); $upload = Dockhand::uploadBlobChunk($upload, $chunk1); $upload = Dockhand::uploadBlobChunk($upload, $chunk2); $result = Dockhand::completeBlobUpload($upload, $digest); // Mount a blob from another repository (avoids re-uploading) $result = Dockhand::mountBlob('library/nginx', $digest, 'library/alpine'); // Check upload status / cancel $upload = Dockhand::getBlobUploadStatus('library/nginx', $uuid); Dockhand::cancelBlobUpload('library/nginx', $uuid);
Deletion
// Delete a manifest by digest Dockhand::deleteManifest('library/nginx', 'sha256:abc123...'); // Delete a blob Dockhand::deleteBlob('library/nginx', 'sha256:abc123...');
Note: Tag deletion (by tag name instead of digest) is only supported by the Zot driver. See Zot Driver Extensions.
Multiple Connections
use Cainy\Dockhand\Facades\Dockhand; // Use a specific connection $repos = Dockhand::connection('staging')->getRepositories(); // Typed accessors (throws if the connection's driver doesn't match) $zot = Dockhand::zot('staging'); // ZotDriver $dist = Dockhand::distribution(); // DistributionDriver (default connection) // Release a connection (useful in long-running workers) Dockhand::disconnect('staging');
Zot Driver Extensions
The Zot driver provides additional features beyond the standard OCI Distribution spec.
$zot = Dockhand::zot('staging'); // Discover available extensions $extensions = $zot->discoverExtensions(); // Search repositories via GraphQL $results = $zot->search('{ GlobalSearch(query: "nginx") { ... } }'); // Search CVEs for an image $cves = $zot->searchCVE('library/nginx', 'latest'); // User preferences $zot->starRepository('library/nginx'); $zot->unstarRepository('library/nginx'); $zot->bookmarkRepository('library/nginx'); $zot->unbookmarkRepository('library/nginx'); // Tag deletion (not supported by standard distribution registries) $zot->deleteManifest('library/nginx', 'latest');
Authentication
JWT (ECDSA)
JWT authentication is designed for token-based registry auth as described in the Docker Token Authentication Specification. Dockhand acts as the token authority — it signs JWTs with your private key, and the registry validates them using the corresponding public key.
Generate an ECDSA key pair:
# Generate private key openssl ecparam -genkey -name prime256v1 -noout -out private_key.pem # Extract public key openssl ec -in private_key.pem -pubout -out public_key.pem
Configure the connection:
DOCKHAND_AUTH_DRIVER=jwt DOCKHAND_PRIVATE_KEY=/path/to/private_key.pem DOCKHAND_PUBLIC_KEY=/path/to/public_key.pem DOCKHAND_AUTHORITY_NAME=my_auth DOCKHAND_REGISTRY_NAME=my_registry
The authority_name must match the issuer in the registry config, and registry_name must match the service.
Basic Auth
'auth' => [ 'driver' => 'basic', 'username' => env('REGISTRY_USERNAME'), 'password' => env('REGISTRY_PASSWORD'), ],
Bearer Token
'auth' => [ 'driver' => 'bearer', 'token' => env('REGISTRY_TOKEN'), ],
API Key
'auth' => [ 'driver' => 'apikey', 'api_key' => env('REGISTRY_API_KEY'), ],
No Auth
'auth' => [ 'driver' => 'null', ],
Webhook Notifications
Dockhand can receive and dispatch registry notification events as Laravel events.
Setup
- Enable notifications in your
.env:
DOCKHAND_NOTIFICATIONS_ENABLED=true DOCKHAND_NOTIFICATIONS_ROUTE=/dockhand/notify
- Generate a notification token:
php artisan dockhand:notify-token
- Configure your registry to send notifications to Dockhand:
notifications: endpoints: - name: EventListener url: http://your-app.test/dockhand/notify headers: Authorization: [ "Bearer <your-notify-token>" ] timeout: 500ms threshold: 5 backoff: 1s ignore: actions: - pull
Available Events
Events extend one of two base classes depending on whether the target still exists:
| Event | Extends | Trigger |
|---|---|---|
ManifestPushedEvent |
RegistryEvent |
A manifest was pushed |
ManifestPulledEvent |
RegistryEvent |
A manifest was pulled |
BlobPushedEvent |
RegistryEvent |
A blob was pushed |
BlobPulledEvent |
RegistryEvent |
A blob was pulled |
BlobMountedEvent |
RegistryEvent |
A blob was mounted from another repository |
ManifestDeletedEvent |
RegistryBaseEvent |
A manifest was deleted |
BlobDeletedEvent |
RegistryBaseEvent |
A blob was deleted |
TagDeletedEvent |
RegistryBaseEvent |
A tag was deleted |
RepoDeletedEvent |
RegistryBaseEvent |
A repository was deleted |
Event Properties
All events (RegistryBaseEvent) have:
| Property | Type |
|---|---|
$id |
string |
$timestamp |
Carbon |
$action |
EventAction (pull, push, mount, delete) |
$targetDigest |
?string |
$targetRepository |
string |
$requestId |
string |
$requestAddr |
string |
$requestHost |
string |
$requestMethod |
string |
$requestUserAgent |
string |
$actorName |
?string |
$sourceAddr |
string |
$sourceInstanceId |
string |
Events extending RegistryEvent additionally have:
| Property | Type |
|---|---|
$targetMediaType |
MediaType |
$targetSize |
int |
$targetUrl |
string |
$targetTag |
?string |
Listening to Events
use Cainy\Dockhand\Events\ManifestPushedEvent; class ManifestPushedListener { public function handle(ManifestPushedEvent $event): void { $repo = $event->targetRepository; $digest = $event->targetDigest; $tag = $event->targetTag; // Handle the pushed manifest... } }
Token & Scope Helpers
Dockhand provides facades for building JWT tokens and registry scopes, useful when implementing custom token endpoints or testing.
Scope Facade
use Cainy\Dockhand\Facades\Scope; // Create from a registry scope string $scope = Scope::fromString('repository:library/nginx:pull,push'); // Fluent builder $scope = Scope::repository('library/nginx')->allowPull()->allowPush(); $scope = Scope::readRepository('library/nginx'); // pull only $scope = Scope::writeRepository('library/nginx'); // push only $scope = Scope::catalog()->allowPull(); // catalog access $scope->hasPull(); // bool $scope->hasPush(); // bool $scope->hasDelete(); // bool $scope->toString(); // "repository:library/nginx:pull,push"
Token Facade
use Cainy\Dockhand\Facades\Token; use Cainy\Dockhand\Facades\Scope; $token = Token::issuedBy('my_auth') ->permittedFor('my_registry') ->relatedTo('username') ->expiresAt(now()->addMinutes(5)) ->withScope(Scope::readRepository('library/nginx')) ->sign(); $jwt = $token->toString();
TokenService Facade
For low-level token operations:
use Cainy\Dockhand\Facades\TokenService; $builder = TokenService::getBuilder(); $token = TokenService::signToken($builder); $valid = TokenService::validateToken($jwt, function ($token) { // Additional validation logic });
Example Registry Configuration
A minimal example pairing Dockhand with a Distribution registry (e.g., in Docker Compose):
DOCKHAND_PUBLIC_KEY=/path/to/public_key.pem DOCKHAND_PRIVATE_KEY=/path/to/private_key.pem DOCKHAND_BASE_URI=http://registry:5000/v2 DOCKHAND_AUTHORITY_NAME=my_auth DOCKHAND_REGISTRY_NAME=my_registry DOCKHAND_NOTIFICATIONS_ENABLED=true DOCKHAND_NOTIFICATIONS_ROUTE=/dockhand/notify
version: 0.1 log: fields: service: registry storage: cache: blobdescriptor: inmemory filesystem: rootdirectory: /var/lib/registry http: addr: :5000 secret: devsecret headers: X-Content-Type-Options: [ nosniff ] auth: token: realm: http://laravel/auth/token service: my_registry issuer: my_auth rootcertbundle: /root/certs/cert.pem notifications: endpoints: - name: EventListener url: http://laravel/dockhand/notify headers: Authorization: [ "Bearer <notify token>" ] timeout: 500ms threshold: 5 backoff: 1s ignore: actions: - pull health: storagedriver: enabled: true interval: 10s threshold: 3
Testing
composer test
composer test-coverage
composer analyse
Contributing
Contributions are welcome! Just create an issue or pull request, and I'll take a look.
Security Vulnerabilities
If you find any security vulnerabilities, please contact me via mail at info@techbra.in.
License
The MIT License (MIT). Please see License File for more information.