
September 09, 2026
11 min read
By Kokil Thapa | Last reviewed: September 2026
You need a place to store private Docker images without paying per-seat fees or exposing build artefacts on a public hub. When you self-host a Docker registry, you control access, retention, and network path for every image your CI pipeline produces. That matters on real client projects where Laravel apps, worker images, and staging builds must stay inside your VPC or a single Ubuntu box you already operate. This guide walks through a production-ready setup: Registry 2.x behind a reverse proxy, TLS, basic auth, storage layout, and the push/pull workflow your GitLab CI or Deployer pipeline expects. If you are new to containers, start with our guide on installing Docker on Ubuntu before continuing.
Why should you self-host a Docker registry instead of using Docker Hub?
Public registries solve discovery. Private registries solve control. Docker Hub rate-limits anonymous pulls and charges for private repositories. AWS ECR and GitLab Container Registry are fine when you already live inside those platforms. A self-hosted registry makes sense when you run GitLab CI, self-hosted runners, or a small EC2 fleet and want one private store without another SaaS bill.
On sister sites I maintain with Deployer 7 and GitLab CI, images rarely leave the server subnet. Storing them locally cuts pull latency and removes an external dependency during deploys. For a deeper comparison of hosted options, read our container registry guide: Docker Hub vs GitLab vs ECR.
The trade-off is operational ownership. You patch the host, renew TLS certs, monitor disk use, and plan backups. For teams already running Linux system administration on Ubuntu 22 or 24, that is usually acceptable. A single registry on a 2 vCPU / 4 GB RAM VM handles modest CI volume comfortably.
| Option | Best for | Cost pattern | Ops burden |
|---|---|---|---|
| Docker Hub private | Solo devs, public images | Per-seat / pull limits | Low |
| GitLab Container Registry | GitLab-native CI | Included with GitLab | Low–medium |
| AWS ECR | ECS/EKS workloads | Storage + data transfer | Low |
| Self-hosted Registry 2 | Own VPS, air-gapped, multi-tool CI | Server disk only (~Rs 2,000/mo, ~USD 15) | Medium |
Choose self-hosting when you want one registry that serves Docker Compose on a staging box, GitLab CI on the same subnet, and occasional laptop pulls without crossing the public internet for every layer.
How do you install and configure a self-hosted Docker registry on Ubuntu?
The official image is registry:2, built from the Distribution project. It speaks the Docker Registry HTTP API V2 and stores layers on local disk by default. The steps below assume Docker Engine is already installed and your user is in the docker group.
Create directories and a compose file
Keep registry data outside the container filesystem. I use a dedicated path under /srv on production hosts.
sudo mkdir -p /srv/registry/data /srv/registry/auth
sudo chown -R root:root /srv/registry
cat > /srv/registry/docker-compose.yml <<'EOF'
services:
registry:
image: registry:2
restart: unless-stopped
ports:
- "127.0.0.1:5000:5000"
environment:
REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /var/lib/registry
REGISTRY_HTTP_ADDR: 0.0.0.0:5000
volumes:
- /srv/registry/data:/var/lib/registry
EOF
Binding port 5000 to localhost prevents accidental public exposure before TLS is in place. Start the service:
cd /srv/registry
docker compose up -d
curl -s http://127.0.0.1:5000/v2/_catalog
An empty catalog returns {"repositories":[]}. That confirms the daemon is listening. For local-only testing you can add "insecure-registries": ["127.0.0.1:5000"] to /etc/docker/daemon.json, but production should never rely on that flag.
Resource limits and volume planning
Registry disk use grows with unique layers, not tag count alone. Tag the same digest ten times and storage barely moves. Push ten different builds and layers accumulate. Plan 50 GB minimum for active CI; monitor with df -h /srv/registry/data. Apply container limits as described in our post on limiting Docker container resources if the registry shares a busy host.
How do you secure a self-hosted Docker registry with TLS and authentication?
An open registry on the internet is a malware upload target within hours. Treat TLS and authentication as mandatory, not optional hardening. Docker clients refuse plain HTTP against remote hosts unless you weaken daemon settings globally.
Generate htpasswd credentials
Registry 2 supports htpasswd, token, and external auth. htpasswd is the fastest path for small teams. Create a user with a strong password — our password generator works fine for initial secrets.
docker run --rm --entrypoint htpasswd httpd:2 -Bbn ciuser 'YOUR_STRONG_PASSWORD' \
> /srv/registry/auth/htpasswd
chmod 600 /srv/registry/auth/htpasswd
Update compose to mount auth and enable basic auth:
volumes:
- /srv/registry/data:/var/lib/registry
- /srv/registry/auth:/auth
environment:
REGISTRY_AUTH: htpasswd
REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm
REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
Terminate TLS at Nginx
I terminate TLS on Nginx and proxy to localhost:5000. Certbot with Let's Encrypt keeps certs current. Full Nginx TLS patterns appear in our Traefik and reverse proxy guide; Nginx follows the same upstream idea.
server {
listen 443 ssl http2;
server_name registry.example.com;
ssl_certificate /etc/letsencrypt/live/registry.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/registry.example.com/privkey.pem;
client_max_body_size 0;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 900;
}
}
Set client_max_body_size 0 so large image layers are not truncated. Reload Nginx after Certbot obtains the certificate. Official guidance on registry configuration lives in the Docker registry deploying docs.
Enable UFW on Ubuntu: allow OpenSSH and 443, deny direct 5000 from the world. Store CI credentials in GitLab masked variables, not in the repository. Rotate passwords when staff leave. For stricter setups, place the registry on a private network and reach it through VPN or SSH tunnel only.
How do you push and pull images from your private Docker registry?
Once TLS and auth work, the client workflow matches Docker Hub. Only the hostname changes. Follow a consistent tagging scheme — our Docker image tagging strategies post covers semver, git SHA, and environment tags.
- Build the image locally or in CI.
- Tag it with the registry hostname as prefix.
- Log in once per host or CI job.
- Push; verify with the catalog API.
- Pull on deploy targets using the same tag.
docker build -t myapp:1.4.2 .
docker tag myapp:1.4.2 registry.example.com/myapp:1.4.2
docker login registry.example.com
docker push registry.example.com/myapp:1.4.2
curl -u ciuser:YOUR_STRONG_PASSWORD \
https://registry.example.com/v2/_catalog
docker pull registry.example.com/myapp:1.4.2
GitLab CI example
A typical pipeline builds on a runner, pushes to your registry, then SSH-deploys or triggers Compose on staging. Store CI_REGISTRY_USER and CI_REGISTRY_PASSWORD as protected variables.
build:
stage: build
script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" registry.example.com
- docker build -t registry.example.com/myapp:$CI_COMMIT_SHA .
- docker push registry.example.com/myapp:$CI_COMMIT_SHA
On production Laravel stacks I often build assets in CI and push a single runtime image. The server pulls by immutable SHA tag, not latest. That mirrors the GitLab CI → Deployer flow on projects like Notary Kathmandu and related sister sites. Read self-hosted CI runners setup and security for runner hardening that pairs with a private registry.
For multi-architecture builds targeting both amd64 and arm64 runners, use Buildx as covered in Docker Buildx multi-platform builds. The registry stores each architecture manifest under the same tag when you push a manifest list.
How do you back up, garbage-collect, and maintain a production Docker registry?
Registry data is plain files under /srv/registry/data. Back up that directory nightly with rsync or restic to another disk or S3-compatible storage. If you already run MinIO for self-hosted S3, you can configure Registry to use S3 backend instead of filesystem — useful when the registry must survive disk loss on the app server.
Filesystem backup script
#!/bin/bash
set -euo pipefail
SRC="/srv/registry/data"
DEST="/backup/registry/$(date +%F)"
mkdir -p "$DEST"
rsync -a --delete "$SRC/" "$DEST/"
Schedule it with cron on the host. Test restores quarterly by spinning a throwaway registry container pointed at a restored copy.
Garbage collection
Deleting a tag does not reclaim disk immediately. Unreferenced layers remain until garbage collection runs. Schedule downtime or read-only mode, then execute:
docker exec registry registry garbage-collect /etc/docker/registry/config.yml --delete-untagged
For compose setups without the config file inside the container, run the GC tool in a one-off container mounting the same data volume. Always back up before GC. Wrong flags can remove layers still referenced by an unexpected manifest.
Define retention in CI: keep last ten SHA tags per app, delete older tags with a scheduled script hitting the registry API. Document the runbook for your team. If you prefer managed oversight, support and maintenance covers the same host patching and backup discipline.
Common production failures I have seen: disk full mid-push (corrupt partial uploads), expired Let's Encrypt cert (sudden CI failures), and opcache-style stale nginx upstream after registry restart. Watch /var/log/nginx/error.log and registry container logs with docker compose logs -f registry.
Key Takeaways
- Bind Registry 2 to localhost and expose it only through Nginx or Traefik with valid TLS certificates.
- Enable htpasswd or token auth before any image reaches the registry — an open registry is an immediate security incident.
- Tag images with
registry.example.com/app:git-shaand avoid mutablelatesttags in production deploys. - Back up
/srv/registry/datanightly and test restores; registry data is your deploy artefact store. - Run garbage collection on a schedule after backups to reclaim disk from deleted tags.
- Integrate login, build, and push steps into GitLab CI or your self-hosted runner pipeline for repeatable releases.
People Also Ask
Can Docker Registry run without Docker Hub at all?
Yes. Once images live in your private registry, builds and deploys never contact Docker Hub except when your Dockerfile starts with FROM ubuntu:24.04 or another public base. Mirror those base images into your registry or use a pull-through cache if outbound rate limits bite.
How much disk space does a private Docker registry need?
Plan from your largest app image times retained build count. A 400 MB Laravel runtime pushed daily with thirty-day retention needs roughly 12 GB plus layer deduplication savings. Monitor weekly; expand the volume before you hit ninety percent.
Is the official registry enough or do I need Harbor?
Registry 2 covers push, pull, and basic auth for small teams. Harbor adds a UI, RBAC, vulnerability scanning, and replication. Start with Registry 2 on one Ubuntu server; move to Harbor when audit requirements or scan policies demand it.
Does a self-hosted registry work with Kubernetes?
Yes. Configure an imagePullSecret with the same credentials you use for docker login. Point pod specs at registry.example.com/namespace/app:tag. Ensure worker nodes trust your TLS certificate.
Ship private images on infrastructure you control
When you self-host a Docker registry, you own the full path from CI build to production pull. Start with Registry 2 on Ubuntu, lock it behind TLS and htpasswd, wire GitLab CI to push immutable tags, and automate backups before the first production deploy depends on it. The setup takes an afternoon; the operational habits pay off on every release after that.
Need help wiring a private registry into a Laravel CI pipeline or a multi-site Deployer workflow? Review our Adventure Third Pole Trek deployment stack or explore related guides on Docker networking and volumes, Docker Compose for local Laravel, and hosting Laravel on AWS EC2. For hands-on implementation on your server, contact us — or browse enterprise application development services if you are planning a full platform rollout.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

