GICT knowledge base
How to Set Up a Private & Self-hosted Docker Registry
Published
A private registry gives your Docker hosts one place to push and pull your own images. The registry stores image layers and manifests; Docker still builds the images and runs the containers.
In this guide, You will learn how to set up your own private Docker Registry on Ubuntu 24.04.
Before you start
Use an Ubuntu 24.04 VPS with sudo access, Docker Engine and the Compose plugin installed. A second Linux Docker host is needed for the pull check. Follow the Docker installation guide first if necessary. Reserve disk space for the images you intend to retain.
Replace registry.example.com throughout with a hostname you control. Its DNS record must resolve to the registry VPS from both machines. You also need an existing PEM certificate chain and matching private key for that hostname. The certificate must be in date and include the hostname in its Subject Alternative Name. Obtaining and renewing certificates is outside this guide.
TCP port 443 must be free on the VPS and reachable from your clients through the provider firewall. If a web server already owns it, use a separate host for this layout. Docker-published ports can bypass UFW rules; do not assume a UFW deny rule restricts this container. Use the provider firewall to limit source addresses if required. Only port 443 is published, not port 5000.
Prepare the registry host
On the VPS, confirm Docker is working and check the listener before changing anything:
$ sudo docker version
$ sudo docker compose version
$ sudo ss -ltnp 'sport = :443'
$ sudo apt-get update
$ sudo apt-get install -y apache2-utils curl
$ sudo install -d -m 0755 /opt/private-registry
$ sudo install -d -m 0700 /opt/private-registry/auth /opt/private-registry/certs
$ cd /opt/private-registry
Copy your existing certificate files from a protected local directory. Replace the two source paths below. fullchain.pem must contain the server certificate first, followed by its intermediate certificates. Copy the actual files, not symlinks whose targets are outside the mounted directory.
$ sudo install -m 0644 /path/to/fullchain.pem /opt/private-registry/certs/fullchain.pem
$ sudo install -m 0600 /path/to/privkey.pem /opt/private-registry/certs/privkey.pem
$ sudo openssl x509 -in certs/fullchain.pem -noout -subject -issuer -dates -ext subjectAltName
$ sudo htpasswd -Bc auth/htpasswd registryuser
$ sudo chmod 0600 auth/htpasswd
Enter a strong password twice at the hidden prompts. -B selects bcrypt, which Distribution requires. -c creates the initial file: use it only for the first account, because it replaces an existing password file. To add accounts later, omit -c and restart the registry after changing the file.
Create /opt/private-registry/compose.yaml with a privileged editor:
$ sudo nano /opt/private-registry/compose.yaml
File contents:
name: private-registry
services:
registry:
image: registry:3
restart: unless-stopped
ports:
- "443:5000"
environment:
REGISTRY_HTTP_ADDR: 0.0.0.0:5000
REGISTRY_HTTP_TLS_CERTIFICATE: /certs/fullchain.pem
REGISTRY_HTTP_TLS_KEY: /certs/privkey.pem
REGISTRY_AUTH: htpasswd
REGISTRY_AUTH_HTPASSWD_REALM: Private Registry
REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /var/lib/registry
REGISTRY_LOG_LEVEL: info
OTEL_TRACES_EXPORTER: none
volumes:
- ./certs:/certs:ro
- ./auth:/auth:ro
- registry-data:/var/lib/registry
volumes:
registry-data:
The certificate and password directories are read-only in the container; image data lives in a named volume. registry:3 is a moving major-version tag. Record the deployed digest and configuration for reproducibility.
$ sudo docker compose config -q
$ sudo docker compose pull
$ sudo docker compose up -d
$ sudo docker compose ps
$ sudo docker image inspect registry:3 --format '{{json .RepoDigests}}'
Verify HTTPS before logging in
On each Linux Docker host that will access the registry, a publicly trusted certificate needs no extra CA installation. For a private CA, obtain only its public root certificate through a trusted channel and install it for this exact registry hostname:
$ sudo install -d -m 0755 /etc/docker/certs.d/registry.example.com
$ sudo install -m 0644 /path/to/registry-ca.crt /etc/docker/certs.d/registry.example.com/ca.crt
These instructions target rootful Docker Engine on Linux, not Docker Desktop. Because the image reference uses the default HTTPS port without :443, the trust directory also has no port suffix. Do not use insecure-registries to work around a certificate error.
From the second host, check the unauthenticated endpoint:
$ curl -sS -D - -o /dev/null https://registry.example.com/v2/
For a private CA, use this variant instead:
$ curl --cacert /etc/docker/certs.d/registry.example.com/ca.crt -sS -D - -o /dev/null https://registry.example.com/v2/
Expect HTTP 401 and a WWW-Authenticate: Basic challenge. That is the correct result before login: HTTPS works, but image access requires credentials. A certificate error must be fixed before continuing; do not add -k.
Push a small test image
On the registry VPS, create an isolated build directory:
$ mkdir -p ~/registry-check
$ cd ~/registry-check
Create ~/registry-check/proof.txt:
Private registry transfer verified.
Create ~/registry-check/Dockerfile:
FROM scratch
COPY proof.txt /proof.txt
This fixture has no executable and downloads no base image. It tests image storage and retrieval, not an application runtime. Build it, log in with the password from htpasswd, then push:
$ sudo docker build -t registry.example.com/registry-check:v1 .
$ sudo docker login registry.example.com --username registryuser
$ sudo docker push registry.example.com/registry-check:v1
$ sudo docker image inspect registry.example.com/registry-check:v1 --format '{{index .RepoDigests 0}}'
$ sha256sum proof.txt
Docker asks for the password interactively. Unless a credential helper is configured, Docker stores login credentials in its configuration file in reversible form; use a trusted client account and log out after testing.
Record the full repository digest printed by the inspect command, in the form registry.example.com/registry-check@sha256:.... A tag can be moved to another image; the digest identifies the specific manifest you just pushed.
Pull from the second host
On the separate Docker host, log in and pull the recorded digest, replacing PASTE_DIGEST with the 64 hexadecimal characters after sha256::
$ sudo docker login registry.example.com --username registryuser
$ sudo docker pull registry.example.com/registry-check@sha256:PASTE_DIGEST
$ sudo docker image inspect registry.example.com/registry-check@sha256:PASTE_DIGEST --format '{{json .RepoDigests}}'
$ sudo docker create --name registry-proof registry.example.com/registry-check@sha256:PASTE_DIGEST /unused
$ sudo docker cp registry-proof:/proof.txt ./received-proof.txt
$ sha256sum received-proof.txt
$ sudo docker rm registry-proof
$ sudo docker logout registry.example.com
Use an empty destination path and an unused container name. The proof container is created but not started. Compare the received file’s SHA-256 with the value you recorded on the source host; require an exact match.
Both the image digest and the file hash must match.
Log out on the publisher too when finished. Logout removes saved credentials; it does not remove local images or erase the registry’s data volume.
When a basic registry is not enough
Every account in this htpasswd file can push and pull every repository. Native basic authentication is not per-project authorization or a read-only user system. Choose a registry with repository roles, such as Harbor, if different teams need different access. This setup also has no web UI or image vulnerability scanner.
A hosted registry can be simpler if you do not want to manage storage and certificates. This single-host setup makes sense when you need control of where your images live and the users belong to one trusted group.
If login or image transfers fail
- Unknown certificate authority: check the client’s CA file and its
.crtextension. For a public certificate, check that the server sends the intermediate chain. Trusting the registry in your browser does not configure a remote Docker daemon. - Certificate hostname mismatch: use the certificate’s DNS name in both image references and login. Substituting the server IP does not fix a missing hostname SAN.
- 401 after login: use the same registry hostname and sudo context for every command. Check that the password file uses bcrypt, then retry the hidden password prompt. Logging in without sudo does not populate root’s Docker credentials.
- Connection refused or timeout: check the DNS address, provider firewall, occupied port 443, and container state. A successful local request does not prove a remote client can connect.
- Upload fails or the container exits: inspect the service log and available disk space on the registry VPS:
$ cd /opt/private-registry
$ sudo docker compose logs --tail=50 registry
$ df -h /var/lib/docker
The named volume survives container recreation. Do not run docker compose down -v unless you intend to remove the stored images. If you replace the certificate files, restart the registry so it loads them; that does not replace a certificate-renewal process.
Reference: Distribution registry deployment. Check the documentation for your installed version before applying changes.
Related guides
How to Self-Host Karakeep with AI Tagging on a VPS
Self-host Karakeep with local Ollama tagging. Restrict first-user setup, disable public registration and verify collection, search and suggested tags.
Docker ManagementDocker Compose Beyond Basics: Multi-Container Apps That Actually Work
Docker Compose Beyond Basics: build production multi-container apps with shared data, dependencies, restart policies, and reboot-safe networking.
Docker ManagementFix Docker Application Permissions Without Changing Its Data Root
Diagnose Docker bind-mount and volume permissions by runtime identity and mapping. Avoid recursive data-root changes and unsafe startup repair jobs.
Docker ManagementHow to Install Portainer CE with Docker on Linux
Learn to install Portainer CE with Docker on Linux, simplifying container management with a user-friendly web interface.
