Docker Hub stores public images for free, gives you only one free private repository, and throttles pull speed for anonymous requests. If images are built in a closed CI pipeline and contain product code, running your own Docker Registry on a VPS removes both limits and keeps the data wherever you decide — down to a dedicated server in the same data center as production.
Why run your own Registry instead of Docker Hub
A private Registry on your own server removes the hourly pull limits that Docker Hub applies to anonymous and free accounts — with an active CI pipeline that easily turns into a "too many requests" error in the middle of a deploy. The second reason is data: an image with a built application often contains pieces of source code and build secrets, and storing it on someone else's infrastructure is not always allowed under a client contract.
The third reason is speed. A Registry on the same VPS, or on the same local network as the production servers, serves image layers in milliseconds instead of pulling them over the internet on every deploy.
Registry v2 or Harbor: which to choose
Registry v2 — the minimal image from Docker
The official registry:2 image is a single container with no web UI that serves and accepts images over an HTTP API. It comes up in 5 minutes and suits a team of a few developers with no complex access requirements.
Harbor — a Registry with a UI, scanning, and RBAC
Harbor adds a web UI on top of the same API, a role-based access model per project, image vulnerability scanning, and replication between several Registry instances. It deploys through a Helm chart or docker-compose with several containers and needs more VPS resources.
| Parameter | Registry v2 | Harbor |
|---|---|---|
| Deployment time | 5-10 minutes | 30-60 minutes |
| Web UI | No | Yes |
| Vulnerability scanning | No | Built in (Trivy) |
| Minimum RAM | 512 MB | 4 GB |
Running a Registry with Docker Compose
For a team of 2-5 developers, Registry v2 with basic auth and layers stored on the VPS disk is usually enough.
version: "3.8"
services:
registry:
image: registry:2
restart: always
ports:
- "5000:5000"
volumes:
- ./data:/var/lib/registry
- ./auth:/auth
- ./certs:/certs
environment:
REGISTRY_AUTH: htpasswd
REGISTRY_AUTH_HTPASSWD_REALM: Registry
REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
REGISTRY_HTTP_TLS_CERTIFICATE: /certs/registry.crt
REGISTRY_HTTP_TLS_KEY: /certs/registry.key
TLS certificate and username/password authorization
Docker refuses to work with a Registry without TLS, except for localhost and hosts explicitly added to the insecure-registries list — the second option is only good for testing. The login file is created with the htpasswd utility from the apache2-utils package.
apt install -y apache2-utils
mkdir -p auth
htpasswd -Bc auth/htpasswd deploy-bot
docker-compose up -d
It is easier to issue a certificate for the registry.example.com domain through a reverse proxy with automatic TLS — for example, Traefik, which renews its Let's Encrypt certificate on its own and proxies requests to port 5000 of the registry container.
Pushing and pulling images, cleaning up old layers
After docker login, a private Registry works the same way as Docker Hub, except the full domain address is used instead of a username.
docker login registry.example.com
docker tag myapp:latest registry.example.com/myapp:1.4.0
docker push registry.example.com/myapp:1.4.0
docker pull registry.example.com/myapp:1.4.0
In a GitLab CI pipeline, building and pushing the image is usually a separate stage before deploying to the server. Old layers and unused tags should be cleaned up regularly with the registry garbage-collect command, otherwise the VPS disk fills up with images within a couple of months. If the Registry serves a cluster of several hosts, it is convenient to run it alongside Docker Swarm or Kubernetes — every node pulls images from the same source.
Checklist before production
- The TLS certificate is configured and renews automatically, insecure-registries is not used.
- Authorization is required for push, anonymous pull is disabled unless the images are meant for public access.
- A backup of the Registry data directory is taken on a schedule, not just the images inside the Registry itself.
- Regular garbage collection is scheduled through cron so the disk does not run out unexpectedly.