psviderski/unregistry
{ "createdAt": "2025-06-03T01:48:51Z", "defaultBranch": "main", "description": "Push docker images directly to remote servers without an external registry", "fullName": "psviderski/unregistry", "homepage": "https://uncloud.run", "language": "Go", "name": "unregistry", "pushedAt": "2025-11-13T06:45:17Z", "stargazersCount": 4027, "topics": [ "containerd", "docker", "golang", "kubernetes", "oci", "registry" ], "updatedAt": "2025-11-27T06:23:22Z", "url": "https://github.com/psviderski/unregistry"}Unregistry is a lightweight container image registry that stores and serves images directly from your Docker daemon’s storage.
The included docker pussh command (extra ‘s’ for SSH) lets you push images straight to remote Docker servers over SSH.
It transfers only the missing layers, making it fast and efficient.
https://github.com/user-attachments/assets/9d704b87-8e0d-4c8a-9544-17d4c63bd050
The problem
Section titled “The problem”You’ve built a Docker image locally. Now you need it on your server. Your options suck:
- Docker Hub / GitHub Container Registry - Your code is now public, or you’re paying for private repos
- Self-hosted registry - Another service to maintain, secure, and pay for storage
- Save/Load -
docker save | ssh <remote server> docker loadtransfers the entire image, even if 90% already exists on the server - Rebuild remotely - Wastes time and server resources. Plus now you’re debugging why the build fails in production
You just want to move an image from A to B. Why is this so hard?
The solution
Section titled “The solution”docker pussh myapp:latest user@serverThat’s it. Your image is on the remote server. No registry setup, no subscription, no intermediate storage, no exposed ports. Just a direct transfer of the missing layers over SSH.
Here’s what happens under the hood:
- Establishes SSH tunnel to the remote server
- Starts a temporary unregistry container on the server
- Forwards a random localhost port to the unregistry port over the tunnel
docker pushto unregistry through the forwarded port, transferring only the layers that don’t already exist remotely. The transferred image is instantly available on the remote Docker daemon- Stops the unregistry container and closes the SSH tunnel
It’s like rsync for Docker images — simple and efficient.
[!NOTE] Unregistry was created for Uncloud, a lightweight tool for deploying containers across multiple Docker hosts. We needed something simpler than a full registry but more efficient than save/load.
Requirements
Section titled “Requirements”On local machine
Section titled “On local machine”- Docker CLI with plugin support (Docker 19.03+)
- OpenSSH client
On remote server
Section titled “On remote server”- Docker is installed and running
- SSH user has permissions to run
dockercommands (user isrootor non-root user is indockergroup — see Manage Docker as a non-root user for details) - If
sudois required, ensure the user can runsudo dockerwithout a password prompt - Your server has internet access to ghcr.io to pull the unregistry image
ghcr.io/psviderski/unregistry:lateston firstdocker pusshuse.- If your server requires a proxy to access the internet, configure Docker to use it by following the Daemon proxy configuration guide.
- For air-gapped environments or where the access to ghcr.io is restricted, you can preload the
image manually:
Terminal window # Get the needed unregistry image version from the plugin version outputdocker pussh --version# Will return:# ...# unregistry image: ghcr.io/psviderski/unregistry:X.Y.Z# On a machine with internet accessdocker pull ghcr.io/psviderski/unregistry:X.Y.Zdocker save ghcr.io/psviderski/unregistry:X.Y.Z | ssh user@server docker load
- Unregistry container requires access to the containerd socket at
/run/containerd/containerd.sock, so the container runs asrootto have the necessary permissions
Installation
Section titled “Installation”macOS/Linux via Homebrew
Section titled “macOS/Linux via Homebrew”brew install psviderski/tap/docker-pusshAfter installation, to use docker-pussh as a Docker CLI plugin (docker pussh command) you need to create a symlink:
mkdir -p ~/.docker/cli-pluginsln -sf $(brew --prefix)/bin/docker-pussh ~/.docker/cli-plugins/docker-pusshmacOS/Linux via direct download
Section titled “macOS/Linux via direct download”Download the current version:
mkdir -p ~/.docker/cli-plugins
# Download the script to the docker plugins directorycurl -sSL https://raw.githubusercontent.com/psviderski/unregistry/v0.3.1/docker-pussh \ -o ~/.docker/cli-plugins/docker-pussh
# Make it executablechmod +x ~/.docker/cli-plugins/docker-pusshIf you want to download and use the latest version from the main branch:
curl -sSL https://raw.githubusercontent.com/psviderski/unregistry/main/docker-pussh \ -o ~/.docker/cli-plugins/docker-pusshchmod +x ~/.docker/cli-plugins/docker-pusshDebian
Section titled “Debian”Via unofficial repository packages created and maintained at unregistry-debian by @dariogriffo
You can install unregistry the debian way by running:
curl -sS https://debian.griffo.io/EA0F721D231FDD3A0A17B9AC7808B4DD62C41256.asc | sudo gpg --dearmor --yes -o /etc/apt/trusted.gpg.d/debian.griffo.io.gpgecho "deb https://debian.griffo.io/apt $(lsb_release -sc 2>/dev/null) main" | sudo tee /etc/apt/sources.list.d/debian.griffo.io.listapt install -y unregistryapt install docker-pusshor in the releases page of the repository here
Windows
Section titled “Windows”Windows is not currently supported, but you can try using WSL 2 with the above Linux instructions.
Verify installation
Section titled “Verify installation”docker pussh --help⚠️ Containerd image store configuration
Section titled “⚠️ Containerd image store configuration”Unregistry stores images directly in containerd’s image store, which is the underlying container runtime used by Docker. However, by default, Docker maintains its own separate storage layer and doesn’t directly use images from containerd.
When you enable containerd image store in Docker, it allows Docker to directly use the same images that unregistry stores in containerd, eliminating duplication.
With containerd image store enabled (recommended)
Section titled “With containerd image store enabled (recommended)”- Images pushed through unregistry are immediately available to Docker
- No additional storage space is used (images are stored once in containerd)
- Faster
pusshoperations without the additional pull step from unregistry to the classic Docker image store
Without containerd image store (default Docker behaviour)
Section titled “Without containerd image store (default Docker behaviour)”- After pushing,
pusshruns an additionaldocker pullon the remote host to pull the image from unregistry to make it available to Docker - Images are stored twice: once in containerd (by unregistry) and once in the classic Docker image store
- These unmanaged images in containerd can fill up disk space over time. To manage them manually, use:
Terminal window sudo ctr -n moby images lssudo ctr -n moby images rm <image>
How to enable containerd image store
Section titled “How to enable containerd image store”Please refer to the official Docker documentation: Enable containerd image store on Docker Engine.
[!WARNING] Switching to containerd image store causes you to temporarily lose images and containers created using the classic storage driver. Those resources still exist on your filesystem, and you can retrieve them by turning off the containerd image store feature.
Push an image to a remote server. Please make sure the SSH user has permissions to run docker commands (user is
root or non-root user is in docker group). If sudo is required, ensure the user can run sudo docker without a
password prompt.
docker pussh myapp:latest user@server.example.comWith SSH key authentication if the private key is not added to your SSH agent:
docker pussh myapp:latest ubuntu@192.168.1.100 -i ~/.ssh/id_rsaUsing a custom SSH port:
docker pussh myapp:latest user@server:2222Push a specific platform for a multi-platform image. The local Docker has to use containerd image store to support multi-platform images.
docker pussh myapp:latest user@server --platform linux/amd64Use a specific unregistry image version on the remote host:
UNREGISTRY_IMAGE=ghcr.io/psviderski/unregistry:A.B.C docker pussh myapp:latest user@server.example.comUse cases
Section titled “Use cases”Deploy to production servers
Section titled “Deploy to production servers”Build locally and push directly to your production servers. No middleman.
docker build --platform linux/amd64 -t myapp:1.2.3 .docker pussh myapp:1.2.3 deploy@prod-serverssh deploy@prod-server docker run -d myapp:1.2.3CI/CD pipelines
Section titled “CI/CD pipelines”Skip the registry complexity in your pipelines. Build and push directly to deployment targets.
- name: Build and deploy run: | docker build -t myapp:${{ github.sha }} . docker pussh myapp:${{ github.sha }} deploy@staging-serverHomelab and air-gapped environments
Section titled “Homelab and air-gapped environments”Distribute images in isolated networks that can’t access public registries over the internet.
Advanced usage
Section titled “Advanced usage”Running unregistry standalone
Section titled “Running unregistry standalone”Sometimes you want a local registry without the overhead. Unregistry works great for this:
# Run unregistry locally and expose it on port 5000docker run -d -p 5000:5000 --name unregistry \ -v /run/containerd/containerd.sock:/run/containerd/containerd.sock \ ghcr.io/psviderski/unregistry
# Use it like any registrydocker tag myapp:latest localhost:5000/myapp:latestdocker push localhost:5000/myapp:latestCustom SSH options
Section titled “Custom SSH options”Need custom SSH settings? Use the standard SSH config file:
Host prod-server HostName server.example.com User deploy Port 2222 IdentityFile ~/.ssh/deploy_key
# Now just usedocker pussh myapp:latest prod-serverThird-party projects
Section titled “Third-party projects”- https://github.com/SonOfBytes/unregistry-action - GitHub Action to push Docker images to remote servers using
docker-pusshplugin for Docker CLI - https://github.com/RezaKargar/setup-unregistry - GitHub Action to install
docker-pusshplugin for Docker CLI - https://github.com/iloveitaly/docker-image-cleanup - Python tool to manage Docker images in self-hosted registries by automatically removing outdated images while preserving recent and actively used ones
Contributing
Section titled “Contributing”Found a bug or have a feature idea? We’d love your help!
- 🐛 Found a bug? Open an issue
- 💡 Have questions, ideas, or need help?
- Start a discussion or join an existing one in the Discussions.
- Join the Uncloud Discord community where we discuss features, roadmap, implementation details, and help each other out.
Inspiration & acknowledgements
Section titled “Inspiration & acknowledgements”- Spegel - P2P container image registry that inspired me to implement a registry that uses containerd image store as a backend.
- Docker Distribution - the bulletproof Docker registry implementation that unregistry is based on.