psviderski/unregistry: push Docker images to a remote server over SSH without a registry
Push docker images directly to remote servers without an external registry
At a glance
- What is it?
- Unregistry is a Go-based container image registry that serves images straight from containerd's store, plus a docker pussh plugin that transfers only missing layers to a remote Docker host. It removes the registry from the path, at the cost of a containerd dependency and root on the server.
- Who is it for?
- Adopt unregistry if you deploy to one or a few Docker hosts you already control over SSH and you are tired of paying for or maintaining a registry. Skip it if your servers run Docker without the containerd image store and you cannot change that, if you need a registry that a third party can pull from, or if your platform is Windows.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 79 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The gap unregistry fills between Docker Hub and docker save
The README frames the problem bluntly: you built an image locally, you need it on a server, and every existing route has a tax. A hosted registry makes you pay for private repositories or publish the image. A self-hosted registry is another service to run, secure and store data for. The save/load pattern, docker save piped over SSH into docker load, moves the whole image every time, even when most layers already sit on the target. Rebuilding on the server burns CPU and turns a build failure into a production debugging session.
Unregistry targets the narrow case where the destination is a Docker host you already reach over SSH and the image only needs to exist there. It is not a distribution mechanism for end users pulling your software; it is a transport for one operator moving artifacts between machines they own. The project grew out of Uncloud, a tool for deploying containers across multiple Docker hosts, which needed something lighter than a registry but faster than save/load. That origin explains the design: no persistent server, no storage backend, no port left open.
How docker pussh moves only the missing layers
The mechanism is a temporary registry that lives only for the duration of one push. According to the README, docker pussh first opens an SSH tunnel to the remote server, then starts a short-lived unregistry container there, forwards a random localhost port through the tunnel to the unregistry port, and runs a normal docker push against that forwarded port. Because unregistry serves from containerd's image store rather than its own volume, layers already present on the host are recognised and skipped during the push. Only the missing layers cross the wire. When the push finishes, the container stops and the tunnel closes.
The Go code reflects that shape. The module depends on github.com/containerd/containerd/v2 and github.com/distribution/distribution/v3, so the HTTP surface is the standard distribution registry API while the storage layer is containerd. The Dockerfile builds a static binary, copies it into an Alpine image and sets EXPOSE 5000, which is the port the tunnel forwards to. The container runs as root by default, and the Dockerfile comment says this is to allow access to the containerd socket, calling it unfortunate and noting that running as non-root would require changing the socket permissions manually.
That is the honest trade-off at the centre of the project. You get rsync-like behaviour for images, and in exchange the helper container needs root and a containerd socket on the target host. The comparison to rsync is the README's own, and it holds for the transfer semantics: content-addressed layers are compared by digest, so an unchanged base image costs nothing to re-push.
Installing the docker-pussh plugin and pushing a first image
The client side is a Docker CLI plugin, so installation means putting an executable named docker-pussh in the CLI plugins directory. On macOS or Linux, Homebrew installs the binary but does not wire it up as a plugin, so the README asks for a symlink into ~/.docker/cli-plugins.
brew install psviderski/tap/docker-pussh
mkdir -p ~/.docker/cli-plugins
ln -sf $(brew --prefix)/bin/docker-pussh ~/.docker/cli-plugins/docker-pusshIf you would rather not use Homebrew, the README gives a direct download of the plugin script from the repository at the v0.4.3 tag, made executable in place. The same snippet works against main if you want unreleased code, and substituting main for the tag is the only difference.
mkdir -p ~/.docker/cli-plugins
curl -sSL https://raw.githubusercontent.com/psviderski/unregistry/v0.4.3/docker-pussh \
-o ~/.docker/cli-plugins/docker-pussh
chmod +x ~/.docker/cli-plugins/docker-pusshConfirm the plugin is visible to Docker before doing anything else. The README uses this as its verification step, and the same command prints the unregistry image tag, which matters later for air-gapped hosts.
docker pussh --help
docker pussh --versionWith the plugin in place, a push is a single command naming the local image and the SSH destination. The README's example is exactly this, and the expected result is that the image appears in the remote daemon's image list with no registry involved.
docker pussh myapp:latest user@serverOn the server side the requirements are Docker installed and running, an SSH user allowed to run docker commands (root, or a non-root user in the docker group), and passwordless sudo docker if sudo is needed. The host also needs outbound access to ghcr.io the first time, because the plugin pulls ghcr.io/psviderski/unregistry:latest to run as the temporary registry. For air-gapped servers the README describes preloading that image by reading the tag from docker pussh --version, pulling it on a connected machine, and piping docker save into ssh user@server docker load. Debian users have a third path: an unofficial repository maintained by @dariogriffo in the unregistry-debian project, which packages both unregistry and docker-pussh.
The containerd image store requirement is the real gate
Unregistry stores images in containerd's image store. Docker, by default, keeps its own separate storage layer and does not read images directly from containerd. The README is explicit that enabling containerd image store in Docker is what lets the daemon use the same images unregistry writes, eliminating duplication, and it marks that configuration as recommended.
This is the constraint that decides adoption. If your hosts run Docker with the default storage driver and you cannot switch, the images land in containerd but the Docker daemon will not see them the way the recommended path intends, and the deduplication story that makes pussh fast stops holding. Enabling containerd image store is a daemon-level change, not a flag on one command, so it is the kind of thing that needs a maintenance window and a rollback plan. The README does not document rollback for that switch.
There is a second, smaller failure mode in the requirements: the temporary container runs as root because it needs /run/containerd/containerd.sock. On hosts where you have deliberately kept the Docker socket away from unprivileged users, this is a step backwards in privilege terms, and the Dockerfile comment admits as much. Treat root-on-the-target as part of the price, not an implementation detail you can tune away.
Where unregistry is the wrong tool
Unregistry assumes the image only needs to exist on a machine you can SSH into. The moment someone else has to pull that image, the model breaks. There is no long-lived endpoint, no authentication surface for external consumers, no retention policy and no garbage collection story in the README. A CI pipeline that publishes an artifact for a fleet of Kubernetes nodes is not this project's problem; a registry is.
Multi-arch publishing is also outside what the README describes. It talks about pushing an image to a remote Docker daemon, and the Dockerfile cross-compiles the unregistry binary itself for multiple architectures, which is about building the tool, not about manifest lists for your application. If your workflow depends on pushing one tag that resolves to several platforms, plan on a registry.
Windows is unsupported. The README says so directly and suggests WSL 2 with the Linux instructions, which means the plugin is effectively a Unix tool. And the first push to any new host needs outbound access to ghcr.io unless you preload the image, so a fully offline environment adds a manual step to every new machine you bring into the fleet.
How it differs from running your own registry
The obvious alternative is a self-hosted registry, most commonly the reference implementation from the distribution project. The architectural difference is lifetime and storage. A registry is a long-running service with its own storage backend, a port you expose, TLS to terminate, and credentials to manage. It persists images after the push ends, which is exactly what you want when other machines pull from it, and exactly the overhead unregistry avoids.
Unregistry inverts both properties. It runs as a container for the duration of a single push and stops, and it keeps no storage of its own because it writes into containerd, the same store the Docker daemon uses. Nothing to expose, nothing to secure at the network edge, nothing to back up. The cost is that the image is only available where you pushed it, and the mechanism depends on containerd being the storage layer on that host.
If your servers run containerd directly, or Kubernetes with containerd as the runtime, the same idea shows up in tools that import images into a node's runtime. Unregistry's distinguishing choice is to speak the distribution registry protocol over an SSH-forwarded localhost port, which is why an ordinary docker push works against it and why no client-side image format conversion is needed.
Maintenance, releases and what the licence permits
The repository is not archived, and the last push was on 2026-07-14. The release cadence visible in the release list is roughly one minor release every few months: v0.4.1 on 2025-12-16, v0.4.2 on 2026-02-23, v0.4.3 on 2026-06-12. That is a small project with a single visible maintainer, so plan for the upgrade path to be manual rather than something a package manager handles for you.
Upgrades touch two things that can drift apart. The plugin script is versioned in the repository and installed by curl or Homebrew, and the server-side unregistry image is pulled separately from ghcr.io. The README's air-gapped instructions show the coupling: docker pussh --version prints the exact unregistry image tag the plugin expects. If you preload images by hand, that printed tag is the value you must pull, and a plugin upgrade can change it.
Licensing is Apache-2.0, which permits commercial use, modification and redistribution provided you keep the licence and notice files and state significant changes. The Debian packages are a separate, unofficial distribution channel maintained outside the project, so their packaging and signing keys are not covered by the project's own release process. Nothing here is legal advice; if you redistribute a modified unregistry, read the Apache-2.0 text rather than this paragraph.
Editorial conclusion
Adopt unregistry if you deploy to one or a few Docker hosts you already control over SSH and you are tired of paying for or maintaining a registry. Skip it if your servers run Docker without the containerd image store and you cannot change that, if you need a registry that a third party can pull from, or if your platform is Windows. Before rolling it out, run docker pussh --version on the machine that will push, confirm the printed unregistry image tag matches a digest you are willing to run as root, and check that /run/containerd/containerd.sock exists on each target host.
Frequently asked questions
How do I install unregistry's docker pussh plugin?
On macOS or Linux, brew install psviderski/tap/docker-pussh installs the binary, and the README then asks for a symlink from $(brew --prefix)/bin/docker-pussh into ~/.docker/cli-plugins so Docker picks it up as a plugin. Alternatively you can curl the docker-pussh script from the repository at a version tag straight into ~/.docker/cli-plugins and make it executable.
Does unregistry need the containerd image store enabled in Docker?
Unregistry stores images in containerd's image store, but Docker keeps its own separate storage layer by default and does not use images from containerd directly. The README marks enabling containerd image store as the recommended configuration because it lets Docker use the same images unregistry writes, eliminating duplication.
What are the requirements on the remote server for docker pussh?
Docker must be installed and running, and the SSH user needs permission to run docker commands, either as root or as a non-root user in the docker group, with passwordless sudo docker if sudo is required. The host also needs outbound access to ghcr.io to pull the unregistry image on first use, and the unregistry container needs access to /run/containerd/containerd.sock, which is why it runs as root.
Can I use unregistry on a server without internet access?
Yes, by preloading the image manually. The README says to read the required unregistry image version from docker pussh --version, pull that tag on a machine with internet access, then pipe docker save into ssh user@server docker load.
Is unregistry supported on Windows?
No. The README states that Windows is not currently supported and suggests trying WSL 2 with the Linux installation instructions.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/psviderski-unregistry)