Report this

What is the reason for this report?

How To Set Up a Private Docker Registry on Ubuntu

Updated on March 4, 2026
Anish Singh Walia

By Anish Singh Walia

Sr Technical Writer

Not using Ubuntu 20.04?
Choose a different version or distribution.
Ubuntu 20.04
How To Set Up a Private Docker Registry on Ubuntu

The author selected the Free and Open Source Fund to receive a donation as part of the Write for DOnations program.

Introduction

Docker Registry is an application that manages storing and delivering Docker container images. Registries centralize container images and reduce build times for developers. Docker images guarantee the same runtime environment through virtualization, but building an image can involve a significant time investment. For example, rather than installing dependencies and packages separately to use Docker, developers can download a compressed image from a registry that contains all of the necessary components. You can also automate pushing images to a registry using CI/CD pipelines or continuous integration tools to update images during production and development.

Docker provides a free public registry, Docker Hub, that can host your custom Docker images. When you need to keep images private (for example, when using proprietary software), a self-hosted private Docker registry gives you full control over access, storage, and TLS. Images typically contain all the code necessary to run an application, so using a private registry is preferable for sensitive or internal workloads.

In this tutorial, you will set up and secure your own private Docker Registry on Ubuntu. You will use Docker Compose to define the registry configuration and Nginx as a reverse proxy to forward traffic and terminate TLS. When you are done, you will be able to push and pull container images to and from your registry over HTTPS with authentication.

Ubuntu version support: This tutorial has been validated on Ubuntu 20.04 LTS, 22.04 LTS, 24.04 LTS, and 24.10. The same steps and commands work across these versions. On Ubuntu 22.04 and later, use the docker compose (with a space) command; if you installed the legacy Compose V1, use docker-compose (with a hyphen) instead.

Key Takeaways

  • A private Docker registry stores and serves container images only to authenticated users, unlike Docker Hub’s public repositories.
  • You run the registry as a container and put Nginx in front of it for TLS and optional HTTP basic auth.
  • Authentication uses htpasswd; TLS is provided by Let’s Encrypt (or a commercial SSL certificate).
  • The registry listens on port 5000 by default; Nginx proxies HTTPS (443) to it and handles large uploads via client_max_body_size.
  • You can run the same setup on Ubuntu 20.04, 22.04, and 24.04 LTS.

Prerequisites

  • Two Ubuntu servers (20.04, 22.04, or 24.04 LTS recommended) set up by following the Initial Server Setup Guide for Ubuntu, including a sudo non-root user and a firewall. One server will host your private Docker Registry and the other will be your client server.
  • Docker installed on both servers by following How To Install and Use Docker on Ubuntu (Steps 1 and 2) or the equivalent for your Ubuntu version.
  • Docker Compose installed on the host server by following How To Install and Use Docker Compose on Ubuntu (Step 1). On Ubuntu 22.04 and later, Docker Compose V2 may already be available as docker compose.
  • Nginx installed on your host server by following How To Install Nginx on Ubuntu or the equivalent for your version.
  • Nginx secured with Let’s Encrypt on the registry host by following How To Secure Nginx with Let’s Encrypt on Ubuntu. In Step 4, redirect all HTTP traffic to HTTPS.
  • A domain name that resolves to the server you use for the private Docker Registry (configured as part of the Let’s Encrypt prerequisite). This tutorial refers to it as your_domain.

Step 1 - Installing and Configuring the Docker Registry

Docker on the command line is useful when starting out and testing containers, but proves to be unwieldy for bigger deployments involving multiple containers running in parallel.

With Docker Compose, you can write one .yml file to set up each container’s configuration and the information containers need to communicate with each other. You use the docker compose command (or docker-compose on legacy installs) to run and control all components as a group.

Docker Registry is itself an application with multiple components, so you will use Docker Compose to manage it. To start an instance of the registry, you’ll set up a docker-compose.yml file to define it and the location on disk where your registry will be storing its data.

You’ll store the configuration in a directory called docker-registry on the main server. Create it by running:

  1. mkdir ~/docker-registry

Navigate to it:

  1. cd ~/docker-registry

Then, create a subdirectory called data, where your registry will store its images:

  1. mkdir data

Create and open a file called docker-compose.yml by running:

  1. nano docker-compose.yml

Add the following lines, which define a basic instance of a Docker Registry:

~/docker-registry/docker-compose.yml
version: '3'

services:
  registry:
    image: registry:2
    ports:
    - "5000:5000"
    environment:
      REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
    volumes:
      - ./data:/data

First, you name the service registry and set its image to registry:2. Under ports, you map host port 5000 to the container port 5000 so that requests to the server on port 5000 are forwarded to the registry. In the environment section, REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY tells the registry where to store data. In volumes, the host ./data directory is mounted at /data in the container so image layers are stored on the host filesystem.

Save and close the file.

Start the registry with:

  1. docker compose up

If your system uses the legacy Compose V1 binary, run docker-compose up instead.

The registry container and its dependencies will be downloaded and started.

Output
Creating network "docker-registry_default" with the default driver Pulling registry (registry:2)... 2: Pulling from library/registry e95f33c60a64: Pull complete 4d7f2300f040: Pull complete 35a7b7da3905: Pull complete d656466e1fe8: Pull complete b6cb731e4f93: Pull complete Digest: sha256:da946ca03fca0aade04a73aa94b54ff0dc614216bdd1d47585f97b4c1bdaa0e2 Status: Downloaded newer image for registry:2 Creating docker-registry_registry_1 ... done Attaching to docker-registry_registry_1 registry_1 | time="2021-03-18T12:32:59.587157744Z" level=warning msg="No HTTP secret provided - generated random secret. This may cause problems with uploads if multiple registries are behind a load-balancer. To provide a shared secret, fill in http.secret in the configuration file or set the REGISTRY_HTTP_SECRET environment variable." go.version=go1.11.2 instance.id=119fe50b-2bb6-4a8d-902d-dfa2db63fc2f service=registry version=v2.7.1 registry_1 | time="2021-03-18T12:32:59.587912733Z" level=info msg="redis not configured" go.version=go1.11.2 instance.id=119fe50b-2bb6-4a8d-902d-dfa2db63fc2f service=registry version=v2.7.1 registry_1 | time="2021-03-18T12:32:59.598496488Z" level=info msg="using inmemory blob descriptor cache" go.version=go1.11.2 instance.id=119fe50b-2bb6-4a8d-902d-dfa2db63fc2f service=registry version=v2.7.1 registry_1 | time="2021-03-18T12:32:59.601503005Z" level=info msg="listening on [::]:5000" go.version=go1.11.2 instance.id=119fe50b-2bb6-4a8d-902d-dfa2db63fc2f service=registry version=v2.7.1 ...

You can ignore the No HTTP secret provided warning for this setup. For production with multiple registry instances behind a load balancer, set the REGISTRY_HTTP_SECRET environment variable to a shared random value (for example, from openssl rand -hex 32). The last line of the output shows the registry listening on port 5000.

You can press CTRL+C to stop its execution.

In this step, you have created a Docker Compose configuration that starts a Docker Registry listening on port 5000. In the next steps, you’ll expose it at your domain and set up authentication.

Step 2 - Setting Up Nginx Port Forwarding

As part of the prerequisites, you’ve enabled HTTPS at your domain. To expose your secured Docker Registry there, you’ll only need to configure Nginx to forward traffic from your domain to the registry container.

You have already set up the /etc/nginx/sites-available/your_domain file, containing your server configuration. Open it for editing by running:

  1. sudo nano /etc/nginx/sites-available/your_domain

Find the existing location block:

/etc/nginx/sites-available/your_domain
...
location / {
  ...
}
...

You need to forward traffic to port 5000, where your registry will be listening for traffic. You also want to append headers to the request forwarded to the registry, which provides additional information from the server about the request itself. Replace the existing contents of the location block with the following lines:

/etc/nginx/sites-available/your_domain
...
location / {
    # Do not allow connections from docker 1.5 and earlier
    # docker pre-1.6.0 did not properly set the user agent on ping, catch "Go *" user agents
    if ($http_user_agent ~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go ).*$" ) {
      return 404;
    }

    proxy_pass                          http://localhost:5000;
    proxy_set_header  Host              $http_host;   # required for docker client's sake
    proxy_set_header  X-Real-IP         $remote_addr; # pass on real client's IP
    proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header  X-Forwarded-Proto $scheme;
    proxy_read_timeout                  900;
}
...

The if block checks the user agent of the request and verifies that the version of the Docker client is above 1.5, as well as that it’s not a Go application that’s trying to access. For more explanation on this, you can find the nginx header configuration in Docker’s registry Nginx guide.

Save and close the file when you’re done. Apply the changes by restarting Nginx:

  1. sudo systemctl restart nginx

If you get an error, double-check the configuration you’ve added.

To confirm that Nginx is properly forwarding traffic to your registry container on port 5000, run the registry:

  1. docker compose up

Then, in a browser window, navigate to your domain and access the v2 endpoint, like so:

https://your_domain/v2

You will see an empty JSON object:

{}

In your terminal, you’ll receive output similar to the following:

Output
registry_1 | time="2018-11-07T17:57:42Z" level=info msg="response completed" go.version=go1.7.6 http.request.host=cornellappdev.com http.request.id=a8f5984e-15e3-4946-9c40-d71f8557652f http.request.method=GET http.request.remoteaddr=128.84.125.58 http.request.uri="/v2/" http.request.useragent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/604.4.7 (KHTML, like Gecko) Version/11.0.2 Safari/604.4.7" http.response.contenttype="application/json; charset=utf-8" http.response.duration=2.125995ms http.response.status=200 http.response.written=2 instance.id=3093e5ab-5715-42bc-808e-73f310848860 version=v2.6.2 registry_1 | 172.18.0.1 - - [07/Nov/2018:17:57:42 +0000] "GET /v2/ HTTP/1.0" 200 2 "" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/604.4.7 (KHTML, like Gecko) Version/11.0.2 Safari/604.4.7"

You can see from the last line that a GET request was made to /v2/, which is the endpoint you sent a request to, from your browser. The container received the request you made, from the port forwarding, and returned a response of {}. The code 200 in the last line of the output means that the container handled the request successfully.

Press CTRL+C to stop its execution.

Now that you have set up port forwarding, you’ll move on to improving the security of your registry.

Step 3 - Setting Up Authentication

Nginx allows you to set up HTTP authentication for the sites it manages, which you can use to limit access to your Docker Registry. To achieve this, you’ll create an authentication file with htpasswd and add username and password combinations to it that will be accepted.

You can obtain the htpasswd utility by installing the apache2-utils package. Do so by running:

  1. sudo apt install apache2-utils -y

You’ll store the authentication file with credentials under ~/docker-registry/auth. Create it by running:

  1. mkdir ~/docker-registry/auth

Navigate to it:

  1. cd ~/docker-registry/auth

Create the first user, replacing username with the username you want to use. The -B flag orders the use of the bcrypt algorithm, which Docker requires:

  1. htpasswd -Bc registry.password username

Enter the password when prompted, and the combination of credentials will be appended to registry.password.

Note: To add more users, re-run the previous command without -c, which creates a new file:

  1. htpasswd -B registry.password username

Now that the list of credentials is made, you’ll edit docker-compose.yml to order Docker to use the file you created to authenticate users. Open it for editing by running:

  1. nano ~/docker-registry/docker-compose.yml

Add the highlighted lines:

~/docker-registry/docker-compose.yml
version: '3'

services:
  registry:
    image: registry:2
    ports:
    - "5000:5000"
    environment:
      REGISTRY_AUTH: htpasswd
      REGISTRY_AUTH_HTPASSWD_REALM: Registry
      REGISTRY_AUTH_HTPASSWD_PATH: /auth/registry.password
      REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
    volumes:
      - ./auth:/auth
      - ./data:/data

You’ve added environment variables specifying the use of HTTP authentication and provided the path to the file htpasswd created. For REGISTRY_AUTH, you have specified htpasswd as its value, which is the authentication scheme you are using, and set REGISTRY_AUTH_HTPASSWD_PATH to the path of the authentication file. REGISTRY_AUTH_HTPASSWD_REALM signifies the name of htpasswd realm.

You’ve also mounted the ./auth directory to make the file available inside the registry container. Save and close the file.

You can now verify that your authentication works correctly. First, navigate to the main directory:

  1. cd ~/docker-registry

Then, run the registry:

  1. docker compose up

In your browser, refresh the page for your domain. You’ll be asked for a username and password.

After providing a valid combination of credentials, you’ll see an empty JSON object:

{}

This means that you’ve successfully authenticated and gained access to the registry. Exit by pressing CTRL+C.

Your registry is now secured and can be accessed only after authentication. You’ll now configure it to run as a background process while being resilient to reboots by starting automatically.

Step 4 - Starting Docker Registry as a Service

You can ensure that the registry container starts every time the system boots up, or after it crashes, by instructing Docker Compose to always keep it running. Open docker-compose.yml for editing:

  1. nano docker-compose.yml

Add the following line under the registry block:

docker-compose.yml
...
  registry:
    restart: always
...

Setting restart to always ensures that the container will survive reboots. When you’re done, save and close the file.

Start the registry as a background process with:

  1. docker compose up -d

With your registry running in the background, you can freely close the SSH session, and the registry won’t be affected.

Because Docker images may be very large in size, you’ll now increase the maximum file size that Nginx will accept for uploads.

Step 5 - Increasing File Upload Size for Nginx

Before you can push an image to the registry, you need to ensure that your registry will be able to handle large file uploads.

The default size limit of file uploads in Nginx is 1m, which is not nearly enough for Docker images. To raise it, you’ll modify the main Nginx config file, located at /etc/nginx/nginx.conf. Open it for editing by running:

  1. sudo nano /etc/nginx/nginx.conf

Find the http section, and add the following line:

/etc/nginx/nginx.conf
...
http {
        client_max_body_size 16384m;
        ...
}
...

The client_max_body_size parameter is now set to 16384m, making the maximum upload size equal to 16GB.

Save and close the file when you’re done.

Restart Nginx to apply the configuration changes:

  1. sudo systemctl restart nginx

You can now upload large images to your Docker Registry without Nginx blocking the transfer or erroring out.

Step 6 - Publishing to Your Private Docker Registry

Now that your Docker Registry server is up and running, and accepting large file sizes, you can try pushing an image to it. Since you don’t have any images readily available, you’ll use the ubuntu image from Docker Hub, a public Docker Registry, to test.

From your second, client server, run the following command to download the ubuntu image, run it, and get access to its shell:

  1. docker run -t -i ubuntu /bin/bash

The -i and -t flags give you interactive shell access into the container.

Once you’re in, create a file called SUCCESS by running:

  1. touch /SUCCESS

By creating this file, you have customized your container. You’ll later use it to check that you’re using exactly the same container.

Exit the container shell by running:

  1. exit

Now, create a new image from the container you’ve just customized:

  1. docker commit $(docker ps -lq) test-image

The new image is now available locally, and you’ll push it to your new container registry. First, you have to log in:

  1. docker login https://your_domain

When prompted, enter in a username and password combination that you’ve defined in step 3 of this tutorial.

The output will be:

Output
... Login Succeeded

Once you’re logged in, rename the created image:

  1. docker tag test-image your_domain/test-image

Finally, push the newly tagged image to your registry:

  1. docker push your_domain/test-image

You’ll receive output similar to the following:

Output
The push refers to a repository [your_domain/test-image] 420fa2a9b12e: Pushed c20d459170d8: Pushed db978cae6a05: Pushed aeb3f02e9374: Pushed latest: digest: sha256:88e782b3a2844a8d9f0819dc33f825dde45846b1c5f9eb4870016f2944fe6717 size: 1150

You’ve verified that your registry handles user authentication by logging in, and allows authenticated users to push images to the registry. You’ll now try pulling the image from your registry.

Step 7 - Pulling From Your Private Docker Registry

Now that you’ve pushed an image to your private registry, you’ll try pulling from it.

On the main server, log in with the username and password you set up previously:

  1. docker login https://your_domain

Try pulling the test-image by running:

  1. docker pull your_domain/test-image

Docker should download the image. Run the container with the following command:

  1. docker run -it your_domain/test-image /bin/bash

List the files present by running:

  1. ls

You will see the SUCCESS file you’ve created earlier, confirming that its the same image you’ve created:

SUCCESS  bin  boot  dev  etc  home  lib  lib64  media   mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

Exit the container shell by running:

  1. exit

Now that you’ve tested pushing and pulling images, you have a secure private registry for your container images.

Docker Registry vs Docker Hub

The differences between a private Docker registry and Docker Hub are summarized in the table below:

Feature Private Docker Registry Docker Hub
Hosting location Self-hosted on your own infrastructure Hosted by Docker
Control Full control over access, storage, backup Limited; managed and controlled by Docker
Cost Free to run (excluding your infrastructure costs) Public repos free; private repos and higher pull limits are paid
Privacy Images stay private to your organization Public repos are visible to all; private repos require payment
Customization Fully customizable (storage backend, auth, etc.) Limited customization options
Push/Pull Limits No rate limits or repository count limits Pull limits and private repo limits apply
Authentication You manage user authentication (e.g., htpasswd) Managed via Docker account
Integration Use standard Docker commands: docker login, docker push, docker pull Same standard Docker commands
Use Case Critical for air-gapped, regulated, or private environments; total data control Useful for public sharing and easy hosting

Use a self-hosted private registry when you need full control, want no repository/pull rate limits, and require higher privacy or compliance. Docker Hub is convenient for public images or small-scale private projects.

Troubleshooting

  • “unauthorized: authentication required” when pushing or pulling: Ensure you ran docker login https://your_domain and used the same credentials you created with htpasswd. If Nginx is in front of the registry, ensure the registry container has the same auth config and is running (docker compose ps).
  • “received unexpected HTTP status: 413 Request Entity Too Large”: Nginx is limiting upload size. Increase client_max_body_size in /etc/nginx/nginx.conf (Step 5) and restart Nginx.
  • Registry not reachable after reboot: Ensure the registry service has restart: always in docker-compose.yml and that you started it with docker compose up -d. Check with docker compose ps.
  • Insecure registry (HTTP): The Docker client requires HTTPS for registries by default. For development-only HTTP registries, you can add "insecure-registries": ["your_domain:5000"] to the client’s Docker daemon config (not recommended for production).

FAQs

1. What is a private Docker registry?

A private Docker registry is a server that stores and serves Docker (OCI) container images only to authenticated users. It implements the same push/pull API as Docker Hub but runs under your control, so you can keep proprietary or internal images off the public internet.

2. Is Docker Registry free to use?

Yes. The Docker Registry (the open-source application) is free. Running it on your own Ubuntu server incurs only the cost of the server and domain. Docker Hub’s private repositories are a paid product; a self-hosted registry has no per-repository fee.

3. Why do I need HTTPS for a Docker registry?

The Docker client refuses to send credentials or push/pull over plain HTTP by default. Using HTTPS (for example with Let’s Encrypt) encrypts traffic and allows the client to authenticate to your registry safely. For development, you can configure the client to allow an “insecure” HTTP registry, but that is not recommended for production.

4. How do I make Docker trust an insecure registry?

On the client machine, add the registry to the Docker daemon’s insecure registries list. Edit /etc/docker/daemon.json (or the equivalent on your OS), add "insecure-registries": ["your-registry.example.com:5000"], then restart Docker. Use only in non-production environments.

5. How do I back up a private Docker registry?

The registry stores image data in the volume you mount (for example ./data). Back up that directory regularly (for example with rsync, a snapshot, or your host’s backup tool). Optionally, run the registry with a storage backend (e.g. S3) and back up that storage. Restore by restoring the data directory or storage and starting the registry again.

6. Can I run the registry behind a reverse proxy?

Yes. This tutorial runs the registry behind Nginx as a reverse proxy. Nginx handles TLS termination, optional HTTP auth, and large uploads. The registry listens on port 5000; Nginx proxies HTTPS (443) to it. Other proxies (Caddy, HAProxy) work similarly.

7. What port does Docker Registry use by default?

The Docker Registry container listens on port 5000 by default. In this setup, Nginx listens on 443 (HTTPS) and forwards requests to localhost:5000. Clients connect to your domain over 443, not directly to 5000.

How do I delete images from a private registry?

The registry does not delete image data when you docker rmi from a client; it only deletes when the registry’s garbage collection runs and no references remain. You can use the registry’s delete API (with auth) or a tool like registry-cli or the registry’s optional delete feature. After deleting manifests, run garbage collection (see Docker Registry configuration) so disk space is reclaimed.

Conclusion

In this tutorial you set up your own private Docker Registry on Ubuntu, secured it with Nginx and Let’s Encrypt, added htpasswd authentication, and verified push and pull. You can use the same registry from CI/CD pipelines or from Kubernetes by logging in with the same credentials. For more on building images, see the Docker documentation on Dockerfile best practices.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the author

Anish Singh Walia
Anish Singh Walia
Author
Sr Technical Writer
See author profile

I help Businesses scale with AI x SEO x (authentic) Content that revives traffic and keeps leads flowing | 3,000,000+ Average monthly readers on Medium | Sr Technical Writer @ DigitalOcean | Ex-Cloud Consultant @ AMEX | Ex-Site Reliability Engineer(DevOps)@Nutanix

Still looking for an answer?

Was this helpful?


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

how to setup a Docker Registry with LDAP Authentication

You did not tell we need to redirect to https scheme after nginx proxy.

Then… how can I delete the image from the private registry?

How can I change this private registry to a cache proxy ?

How to list and delete images from registry?

Creative CommonsThis work is licensed under a Creative Commons Attribution-NonCommercial- ShareAlike 4.0 International License.
Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Get started for free

Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

*This promotional offer applies to new accounts only.