Push Docker images from localhost to a remote machine via SSH without requiring a Docker registry.
pussh is a Go library and Docker CLI plugin that enables seamless transfer of Docker images to remote hosts over SSH. It eliminates the need for external registries by leveraging the unregistry container on the remote host.
Note: This code is heavily inspired by the work of psvidersky/unregistry. I rewrote initial bash implementation to Go to maintain greater control over the process and add a possibility to use it as a Go library as this was my main usecase.
- SSH Connection: Establishes an SSH connection to the remote host
- Remote Docker Check: Verifies Docker is installed and accessible on the remote host
- Unregistry Setup: Starts an
unregistrycontainer on the remote host. Ensures it is available and pulls or copies it if not - Port Forwarding: Creates an SSH tunnel from localhost to the remote unregistry service
- Image Push: Tags the local image and pushes it through the forwarded port
- Remote Import: On the remote host, pulls the image from unregistry and retags it appropriately
The library automatically handles:
- Docker Desktop environments (creates additional tunnel)
- Remote Docker permission detection (sudo vs non-sudo)
- Containerd vs non-containerd image stores
- Multi-platform image support
- Air-gapped environments (image transfer via SSH stream)
go get github.com/gophertribe/pusshBuild the plugin:
make buildInstall the plugin:
make install-docker-pluginThis will copy the binary to ~/.docker/cli-plugins/docker-pussh, making it available as docker pussh.
package main
import (
"context"
"log/slog"
"os"
"github.com/gophertribe/pussh"
)
func main() {
ctx := context.Background()
opts := pussh.RunnerOptions{
Image: "myimage:latest",
SSHAddress: "user@example.com",
SSHKeyPath: "/path/to/ssh/key", // optional
Platform: "linux/amd64", // optional
ImageTransferMode: "remote", // "remote" or "copy"
Logger: slog.Default(),
Stdout: os.Stdout,
Stderr: os.Stderr,
}
if err := pussh.Execute(ctx, opts); err != nil {
log.Fatal(err)
}
}- Image: The Docker image reference to push (e.g.,
myimage:latest) - SSHAddress: Remote host address in format
[user@]host[:port](e.g.,user@example.com:2222) - SSHKeyPath: Optional path to SSH private key (if not using SSH agent)
- Platform: Optional target platform (e.g.,
linux/amd64,linux/arm64) for multi-platform images - ImageTransferMode: How to transfer the unregistry image to remote host:
"remote": Pull unregistry image directly on remote host (requires internet access)"copy": Transfer unregistry image via SSH stream (for air-gapped environments)
- Logger: Optional structured logger (uses
slog.Default()if nil) - Stdout/Stderr: Optional writers for Docker command output (discarded if nil)
- UnregistryImage: Optional override for unregistry image (defaults to
ghcr.io/psviderski/unregistry:latest)
Once installed, use it as a Docker plugin:
# Basic usage
docker pussh myimage:latest user@host
# With SSH key
docker pussh -i ~/.ssh/id_ed25519 myimage:latest user@host
# With custom SSH port
docker pussh myimage:latest user@host:2222
# Push specific platform from multi-platform image
docker pussh --platform linux/amd64 myimage:latest user@host
# Air-gapped mode (transfer unregistry image via SSH)
docker pussh --image-transfer-mode copy myimage:latest user@host
# Cross-platform build and push
docker build --platform linux/amd64 -t myimage:latest .
docker pussh --platform linux/amd64 myimage:latest user@host
# Verbose output
docker pussh --verbose myimage:latest user@host-i, --ssh-key: Path to SSH private key--platform: Target platform (e.g.,linux/amd64)--image-transfer-mode: Image transfer mode (remoteorcopy, default:remote)--verbose: Enable verbose/debug output-v, --version: Show version
- Docker installed and running
- OpenSSH client (
sshcommand available) - Go 1.24+ (for building from source)
- Docker installed and running
- SSH access with appropriate permissions
- User must be able to run Docker commands (either as root, in docker group, or with sudo)
import "github.com/gophertribe/pussh"
ctx := context.Background()
err := pussh.Execute(ctx, pussh.RunnerOptions{
Image: "myapp:v1.0.0",
SSHAddress: "deploy@production.example.com",
})import "github.com/gophertribe/pussh"
ctx := context.Background()
err := pussh.Execute(ctx, pussh.RunnerOptions{
Image: "myapp:latest",
SSHAddress: "user@arm-server",
Platform: "linux/arm64",
})import "github.com/gophertribe/pussh"
ctx := context.Background()
err := pussh.Execute(ctx, pussh.RunnerOptions{
Image: "myapp:latest",
SSHAddress: "user@isolated-host",
ImageTransferMode: "copy", // Transfer unregistry image via SSH
})The library provides structured error types:
SSHError: SSH connection or command execution failuresDockerError: Docker command failures (local or remote)
Sentinel errors:
ErrNoDocker: Docker not found on remote hostErrDockerPermission: Cannot run Docker commands on remote hostErrPortExhausted: No available port foundErrInvalidAddress: Invalid SSH address formatErrSSHNotFound: SSH client not found
The library consists of several components:
- Runner: Orchestrates the push process
- SSHConnection: Manages SSH connections using ControlMaster for efficiency
- Docker Operations: Wrappers for local Docker commands
- Transfer Logic: Handles image transfer in air-gapped scenarios
This project is heavily inspired by psviderski/unregistry by Pasha Sviderski. The original work is licensed under Apache 2.0. I consider this project a derivative work of Pasha's original project even though it has been rewritten in Go from original Bash implementation.
Licensed under the Apache License, Version 2.0. See LICENSE for details.
This project is a derivative work of psviderski/unregistry by Pasha Sviderski, which is also licensed under the Apache License, Version 2.0.