Self-hosted service
psviderski/unregistry avatar
psviderski/unregistry

docker pussh: how Unregistry ships only the layers the remote host is missing

Push docker images directly to remote servers without an external registry

4,900 stars98 forksGoApache-2.0

At a glance

What is it?
Unregistry is a small Go registry that lives in the remote Docker host's containerd store, and its docker pussh plugin opens an SSH tunnel, runs a throwaway container, and pushes the missing layers. It trades persistence for a transfer with no registry to run and no port to expose.
Who is it for?
Reach for Unregistry when you already hold SSH access to the remote host and want a one-off image delivery without standing up a registry; it is a poor fit when the target is locked down, air-gapped with no preload step, or run by a team that will not hand an account root or passwordless sudo. Before you commit, check whether the remote Docker has the containerd image store enabled, because without it every push writes the image twice and adds a pull.
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 124 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 November 14, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • psviderski/unregistry4,900 ★ · +35
Star history
Repository2026-09-232026-11-14
psviderski/unregistry4.9k4.9k

35 Stars in the last 52 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

Save over SSH resends every layer, including the ones already on disk

The project frames the problem as four bad options, and each one costs something specific. Docker Hub or GitHub Container Registry makes your code public unless you pay for private repos. A self-hosted registry adds a service to maintain, secure, and pay storage for. Rebuilding remotely burns time and server resources, then leaves you debugging why the build fails in production. The fourth is the one people fall back on:

shell
docker save | ssh <remote server> docker load

That streams the whole image tarball across the wire and unpacks it on the far side, so a host that already holds 90 percent of the layers still receives all of them. Unregistry's answer is not a better tar format but layer awareness: keep the transfer, drop the bytes the remote daemon already has. The project's own analogy is rsync, and the origin explains the goal. It was written for Uncloud, a lightweight tool for deploying containers across multiple Docker hosts, because the author needed something simpler than a full registry and more efficient than save/load. What you give up is a permanent home for tagged images, along with the subscription, the intermediate storage, and the exposed port.

One pussh call runs five steps, then tears the container down

The whole tool is one command:

shell
docker pussh myapp:latest user@server

Underneath it does five things in order. It establishes an SSH tunnel to the remote server. It starts a temporary unregistry container there. It forwards a random localhost port to the unregistry port over that tunnel. It runs `docker push` against unregistry through the forwarded port, so only the layers the remote daemon does not already have cross the connection, and the image is available on the remote Docker daemon when the push returns. Then it stops the unregistry container and closes the tunnel. Two consequences follow from that order. Nothing is left listening afterwards, so there is no registry port to expose, secure, or remember. And there is no long-lived state, which makes this a transport rather than a place you tag and store things.

Four install paths, and only some of them hand you a docker pussh command

Homebrew installs the plugin binary but not the subcommand:

shell
brew install psviderski/tap/docker-pussh
mkdir -p ~/.docker/cli-plugins
ln -sf $(brew --prefix)/bin/docker-pussh ~/.docker/cli-plugins/docker-pussh

The symlink is the step people skip, because Docker only exposes a binary sitting at `~/.docker/cli-plugins/docker-pussh` as `docker pussh`, while Homebrew puts it under its own prefix. The direct download needs no symlink, because the curl target already is that path, and it can pin a release:

shell
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-pussh

A second documented URL points the same command at the main branch instead of the tag, so the two download instructions deliberately differ in stability. Debian users are pointed at an unofficial repository maintained by someone other than the author, and Windows is not supported, with WSL 2 offered as the workaround. Your own machine needs a Docker CLI with plugin support, Docker 19.03 or newer, plus an OpenSSH client. Neither ships with the tool, so `docker pussh --help` after any install is the cheap way to confirm the symlink or the downloaded file actually took.

The unregistry container runs as root to reach the containerd socket

Three conditions on the remote side are easy to get wrong. The SSH user must be able to run `docker` commands, either as root or by belonging to the docker group. If sudo is required, it has to work without a password prompt, because nothing in this flow can answer one. And the unregistry container itself runs as root, since it needs access to the containerd socket at /run/containerd/containerd.sock. The Dockerfile states the reason in a comment: running as a non-root user requires changing the containerd socket permissions, which it leaves as a manual job for advanced users. That is the sharpest constraint in the tool. It means your account drives a root process with access to the container runtime socket for the duration of the push, reachable through your own SSH session.

The first push on a host needs ghcr.io, or a manual image preload

The first push on any host reaches out to ghcr.io, because the plugin starts the server by pulling `ghcr.io/psviderski/unregistry:latest`. Hosts behind a proxy need Docker's daemon proxy configuration; hosts with no route to ghcr.io have to preload the image by hand, and the plugin is what tells you which version to preload:

shell
docker pussh --version

That output includes a line of the form `unregistry image: ghcr.io/psviderski/unregistry:X.Y.Z`, which is the tag to stage from a machine that does have connectivity:

shell
docker pull ghcr.io/psviderski/unregistry:X.Y.Z
docker save ghcr.io/psviderski/unregistry:X.Y.Z | ssh user@server docker load

This handshake is why the plugin and the server image have to be treated as two separate things. Read the version off the plugin, stage exactly that tag, and an offline host stops depending on ghcr.io entirely.

Without the containerd image store the image is written twice and pulled twice

Unregistry writes images into containerd's image store, but stock Docker keeps a separate storage layer and will not use images it did not pull itself. With the containerd image store enabled, which the project marks as recommended, a pushed image is available to Docker immediately, the bytes are stored once, and the push has no extra step. With default Docker behaviour, the push finishes and then pussh runs an additional `docker pull` on the remote host to move the image from unregistry into Docker's own store, and the image now exists in two places: once in containerd, once in the classic store. So a setting you cannot see from the command line decides whether a delivery is one write or a write plus a copy, and on a host with tight disk that is the difference between fitting and not.

Plugin and server image move on separate version tracks

Both pieces are built from one repository. The releases are v0.4.1 on 2025-12-16, v0.4.2 on 2026-02-23, and v0.4.3 on 2026-06-12, and the last push to the default branch was 2026-07-14. The install instructions use that tag literally: the pinned download URL hardcodes v0.4.3 while the alternative points at main, so the documented paths hand you either a fixed plugin or a moving one. The server image comes from the Dockerfile, which cross-compiles from golang:1.24-alpine into an alpine:3.22.0 image that copies the binary to /usr/local/bin, exposes 5000, and sets `unregistry` as the entrypoint. The Go module requires containerd v2.1.1, distribution v3, cobra, and logrus, and the Makefile copies the plugin binary into your cli-plugins directory with `cp docker-pussh ~/.docker/cli-plugins/docker-pussh`. The layout mirrors those two halves: `docker-pussh` is a single script at the root, `cmd/` holds the package the Dockerfile compiles, `config.go` and `registry.go` sit beside them, and `internal/`, `test/`, `scripts/`, and `misc/` fill out the tree. Three Makefile targets cover the local loop, including one that runs shellcheck with every rule enabled across the script and the shell files. The license is Apache-2.0 and the project points at https://uncloud.run as its home.

Editorial conclusion

Reach for Unregistry when you already hold SSH access to the remote host and want a one-off image delivery without standing up a registry; it is a poor fit when the target is locked down, air-gapped with no preload step, or run by a team that will not hand an account root or passwordless sudo. Before you commit, check whether the remote Docker has the containerd image store enabled, because without it every push writes the image twice and adds a pull. Pin the plugin to a release tag rather than main so the client and the server image stay in step.

Frequently asked questions

What is a Docker registry, and how does unregistry differ from one?

A registry stores and serves images so a Docker daemon can pull them. Unregistry is a lightweight registry that keeps images in the Docker daemon's own storage, and its purpose is to let `docker pussh` push straight to a remote host over SSH with no hosted or self-managed registry involved.

What is the difference between a registry and a repository in this workflow?

Unregistry acts as the registry, and the storage consequence depends on Docker's configuration: with the containerd image store enabled an image is stored once and Docker sees it immediately, otherwise it lives in containerd and again in Docker's own store, and pussh runs an extra `docker pull` on the remote host.

Does unregistry work on Windows or on hosts with no internet access?

Windows is not supported, though the Linux instructions can be tried under WSL 2. For an air-gapped server, read the image tag that `docker pussh --version` reports, pull that tag on a connected machine, and load it onto the host before the first push.

Do I need root or sudo on the remote host for unregistry?

The SSH user must be able to run `docker` commands, as root or through the docker group, and if sudo is required, `sudo docker` must work without a password prompt. The unregistry container itself runs as root so it can reach /run/containerd/containerd.sock.

How do I install the docker pussh plugin?

Homebrew installs the `docker-pussh` binary and you then symlink it into `~/.docker/cli-plugins/docker-pussh`, or you curl the script straight into that path. Debian users can use an unofficial repository maintained by another party. Verify with `docker pussh --help`.

Official sources

  1. License: Apache-2.0
  2. Project website
  3. psviderski/unregistry on GitHub
  4. README
  5. Releases
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/psviderski-unregistry.svg)](https://hysenlabs.com/projects/psviderski-unregistry)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/psviderski-unregistry.svg)](https://hysenlabs.com/projects/psviderski-unregistry)

Was this useful?