How to set up and use a private Docker registry 

How to set up and use a private Docker registry 

When working on complex projects with Docker, using Docker Hub is a convenient way to handle images. However, when storing and sharing images internally or for sensitive applications you don’t want to expose publicly, a public registry like Docker Hub might not cut it.

In this guide, we’ll learn how to set up and use Docker private registries for effective and secure Docker image storage and management.

What is a private Docker registry?

A private registry offers a secure and centralized location for Docker images, which gives developers control over who can access and manage them.

This offers a number of benefits:

  • Reduced dependency on third-party services for more control in the event of outages, disruptions, or breaches.
  • Easy access to images and versioning, so you can push and pull your images like code.
  • Faster performance thanks to centralized hosting.
  • Better customization, as you get full control over the configuration of your private registry.
  • Improved security, as access is restricted to authorized users.

Hosting your registry on a VPS enhances these benefits by adding an extra layer of security, flexibility, and cost efficiency. It offers easy scaling, full control and customization, personalized network configuration, and more, making it an ideal solution for teams who want to leverage all the benefits of Docker with minimal or no external dependencies.

Prerequisites

Before we begin with our tutorial, make sure you have the following:

  • A VPS running Ubuntu 20.04 or later. Hostinger offers VPS hosting with a one-click Docker with Ubuntu template, starting at $4.49/month.
  • A user account with sudo privileges.
  • Docker and Docker Compose installed on the VPS.
  • Nginx installed and configured.
  • Certbot for SSL certificates.
  • A domain or subdomain pointing to your VPS (optional but recommended for secure setup with HTTPS).
  • Basic command line skills.

Setting up a private Docker registry

1. Install and configure a Docker registry

First off, on your VPS of choice and using the command line, create a dedicated folder for your project and access it:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
mkdir ~/docker-registry
cd docker-registry
mkdir ~/docker-registry cd docker-registry
mkdir ~/docker-registry

cd docker-registry

Let’s also create three additional folders:

  • a folder named data, where you will store your registry’s private images, 
  • a folder named auth, where credentials will be stored, 
  • a folder named nginx, where your Nginx configuration and SSL certificates will be stored.
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
mkdir data auth nginx
mkdir data auth nginx
mkdir data auth nginx

Now let’s pull the official Docker private registry image. You can pull it directly from Docker Hub by running the following command:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
docker pull registry:2
docker pull registry:2
docker pull registry:2

To keep things clean, we will run the private registry container using Docker Compose. In your project folder, create a Docker compose file and open it using nano:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
touch docker-compose.yml
nano docker-compose.yml
touch docker-compose.yml nano docker-compose.yml
touch docker-compose.yml

nano docker-compose.yml

Once in the file, add the following:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
version: '3'
services:
registry:
image: registry:2
ports:
- "5000:5000"
volumes:
- ./data:/var/lib/registry
version: '3' services: registry: image: registry:2 ports: - "5000:5000" volumes: - ./data:/var/lib/registry
version: '3'

services:

  registry:

image: registry:2

ports:

   - "5000:5000"

volumes:

   - ./data:/var/lib/registry

Here:

  • image: registry:2: will pull the image if you haven’t yet done so separately,
  • ports: – “5000:5000”: will map the container’s port 5000 to your VPS’s port 5000,
  • volumes: – ./data:/var/lib/registry: will mount a volume to store Docker image layers.

Now you can start the configuration:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
docker-compose up -d
docker-compose up -d
docker-compose up -d

Check that your Docker registry container is up by running:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
docker-compose ps
docker-compose ps
docker-compose ps

Alternatively, you can use curl:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
curl http://localhost:5000/v2/
curl http://localhost:5000/v2/
curl http://localhost:5000/v2/

If everything is running smoothly, this should return:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
{}
{}
{}

2. Set up the Nginx reverse proxy

Let’s ensure Nginx is installed correctly:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
sudo systemctl status nginx
sudo systemctl status nginx
sudo systemctl status nginx

If not, run the following to install it on your VPS:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
sudo apt update
sudo apt install nginx -y
sudo apt update sudo apt install nginx -y
sudo apt update

sudo apt install nginx -y

Configure Nginx as a reverse proxy

Next, create a config file for Nginx:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
sudo nano /etc/nginx/sites-available/docker-registry
sudo nano /etc/nginx/sites-available/docker-registry
sudo nano /etc/nginx/sites-available/docker-registry

And add the following:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
server {
listen 80;
server_name your-domain.com; # Add your domain here or leave as _ if no domain, or localhost
location / {
proxy_pass http://localhost: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;
}
}
server { listen 80; server_name your-domain.com; # Add your domain here or leave as _ if no domain, or localhost location / { proxy_pass http://localhost: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; } }
server {

listen 80;

server_name your-domain.com;  # Add your domain here or leave as _ if no domain, or localhost

location / {

         proxy_pass http://localhost: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;

     }

}

Note that HTTP forwarding to your domain name won’t work properly without a domain.

Now you can enable the config:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
sudo ln -s /etc/nginx/sites-available/docker-registry /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/docker-registry /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/docker-registry /etc/nginx/sites-enabled/

And test and reload Nginx:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
sudo nginx -t
sudo systemctl reload nginx
sudo nginx -t sudo systemctl reload nginx
sudo nginx -t

sudo systemctl reload nginx

You can also test for proper forwarding by running:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
curl http://your-vps-ip/v2/_catalog
curl http://your-vps-ip/v2/_catalog
curl http://your-vps-ip/v2/_catalog

You should get a JSON response listing the repositories in your own private Docker registry.

Configure Nginx to handle HTTPS requests for the registry.

For this step to be successful, you’ll need:

  • Your Docker registry already running on your VPS at localhost:5000,
  • A domain or subdomain pointing to your VPS IP,
  • Your Nginx config enabled,
  • Certbot.

Let’s begin by installing Certbot:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
sudo apt update
sudo apt install certbot python3-certbot-nginx
sudo apt update sudo apt install certbot python3-certbot-nginx
sudo apt update

sudo apt install certbot python3-certbot-nginx

Now, we have to obtain an SSL certificate using Certbot:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
sudo certbot --nginx -d registry.yourdomain.com
sudo certbot --nginx -d registry.yourdomain.com
sudo certbot --nginx -d registry.yourdomain.com

This will edit your Nginx config with SSL settings and create a secure HTTPS server block.

In your config file, you can expect to see this:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
server {
listen 443 ssl;
server_name registry.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/registry.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/registry.yourdomain.com/privkey.pem;
… server { listen 443 ssl; server_name registry.yourdomain.com; ssl_certificate /etc/letsencrypt/live/registry.yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/registry.yourdomain.com/privkey.pem; …
…

server {

listen 443 ssl;

server_name registry.yourdomain.com;

ssl_certificate /etc/letsencrypt/live/registry.yourdomain.com/fullchain.pem;

ssl_certificate_key /etc/letsencrypt/live/registry.yourdomain.com/privkey.pem;

…

As before, test and reload Nginx to conclude and ensure everything is up to date.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
sudo nginx -t
sudo systemctl reload nginx
sudo nginx -t sudo systemctl reload nginx
sudo nginx -t

sudo systemctl reload nginx

Finally, use curl again to test that the forwarding works:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
curl -k https://registry.yourdomain.com/v2/_catalog
curl -k https://registry.yourdomain.com/v2/_catalog
curl -k https://registry.yourdomain.com/v2/_catalog

You should get a JSON object such as:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
{"repositories": [ ]}
{"repositories": [ ]}
{"repositories": [ ]}

If your run fails, make sure your private registry is running and that ports 80 and 443 are not blocked by your firewall.

3. Increase file upload size in Nginx

Increasing file upload size might be beneficial and even necessary if you are using larger Docker images, as Nginx’s default client_max_body_size is only 1 MB.

Adjusting upload size is pretty straightforward. For example, let’s imagine we want to set the limit to 2 GB. Open the Nginx config file again and add the following:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
server {
client_max_body_size 2G
}
server { … client_max_body_size 2G … }
server {

…

     client_max_body_size 2G

…

}

As usual, test and reload Nginx to make sure your changes are applied.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
sudo nginx -t
sudo systemctl reload nginx
sudo nginx -t sudo systemctl reload nginx
sudo nginx -t

sudo systemctl reload nginx

In order to verify the new upload size limit, push an image to your registry that is larger than the default size of 1 MB:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
docker push registry.yourdomain.com/your-image
docker push registry.yourdomain.com/your-image
docker push registry.yourdomain.com/your-image

4. Authenticate users for the private registry

Setting up authentication for your private registry is crucial to keeping your private images safe. With Nginx, you can set up HTTP authentication and create users with ad hoc credentials, thus deciding who has access to your Docker registry.

Let’s do this by setting up the htpasswd utility within an authentication file and adding the accepted users and passwords. In order to obtain htpasswd, you’ll need to install the apache2-utils package:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
sudo apt install apache2-utils -y
sudo apt install apache2-utils -y
sudo apt install apache2-utils -y

You can now create an .htpasswd file as well as your very first user. Be sure to do this within your project directory:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
mkdir -p ./auth
sudo htpasswd -c ./auth/htpasswd username
mkdir -p ./auth sudo htpasswd -c ./auth/htpasswd username
mkdir -p ./auth

sudo htpasswd -c ./auth/htpasswd username

To add new users, run the same command, without including -c.

Configure basic authentication

To configure Nginx to require authentication, open your Nginx site config (e.g., /etc/nginx/sites-available/docker-registry) and add the following inside your location / block:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
location / {
auth_basic "Docker Registry Authentication";
auth_basic_user_file /full/path/to/docker-registry/auth/htpasswd;
}
location / { … auth_basic "Docker Registry Authentication"; auth_basic_user_file /full/path/to/docker-registry/auth/htpasswd; … }
location / {

…

auth_basic "Docker Registry Authentication";

auth_basic_user_file /full/path/to/docker-registry/auth/htpasswd;

…

}

Replace /full/path/to/docker-registry/auth/htpasswd with the absolute path to your .htpasswd file on your VPS.

Now update your Docker compose file to include authentication:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
version: '3'
services:
registry:
image: registry:2
ports:
- "5000:5000"
environment:
- REGISTRY_AUTH=htpasswd
- REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm
- REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd
volumes:
- ./data:/var/lib/registry
- ./auth:/auth
version: '3' services: registry: image: registry:2 ports: - "5000:5000" environment: - REGISTRY_AUTH=htpasswd - REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm - REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd volumes: - ./data:/var/lib/registry - ./auth:/auth
version: '3'

services:

  registry:

image: registry:2

ports:

   - "5000:5000"

environment:

   - REGISTRY_AUTH=htpasswd

   - REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm

   - REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd

volumes:

   - ./data:/var/lib/registry

   - ./auth:/auth

Restart your container to apply the changes, like so:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
docker-compose down
docker-compose up -d
docker-compose down docker-compose up -d
docker-compose down

docker-compose up -d

5. Configure Docker clients to trust the private registry

You must specifically set up Docker to trust your registry if it is not using a valid TLS/SSL certificate (i.e., HTTP or a self-signed cert); otherwise, Docker will prevent any communication with it.

If you’re using HTTPS with a valid certificate from Let’s Encrypt or similar, you can skip this step.

Update the Docker daemon

First off, open your daemon file.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
sudo nano /etc/docker/daemon.json
sudo nano /etc/docker/daemon.json
sudo nano /etc/docker/daemon.json

If it doesn’t exist, you can create it with:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
touch /etc/docker/daemon.json
touch /etc/docker/daemon.json
touch /etc/docker/daemon.json

Add the following to the JSON object:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
{
"insecure-registries": ["your.domain:5000"]
}
{ … "insecure-registries": ["your.domain:5000"] … }
{

…

  "insecure-registries": ["your.domain:5000"]

…

}

If needed, you can replace your.registry.domain:5000 with:

  • Your VPS public IP and port (such as 123.123.123.123:5000), or
  • A domain or a subdomain that points to your VPS (most common scenario)
  • localhost:5000 or 127.0.0.1:5000, if the Docker registry is on the same VPS (less common scenario)

Important note: use this only if you’re using HTTP or self-signed HTTPS certs.

Make sure to restart Docker to apply your changes:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
sudo systemctl restart docker
sudo systemctl restart docker
sudo systemctl restart docker

6. Push and pull images from the private registry

At this point, your registry should be running reliably and your clients should be able to trust it. You can now store and retrieve images securely. Below are a few additional tips.

Log in to the registry via Docker CLI

As we previously set up authentication, your registry will request that you log in. If you have not enabled authentication, Docker will skip this step for you. To log in, run:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
docker login your.registry.domain:5000
docker login your.registry.domain:5000
docker login your.registry.domain:5000

Where your.registry.domain:5000 is whatever domain, subdomain, or VPS public IP and port you have chosen.

At this point, you’ll be prompted to input the password and username you created. Docker will save your login information locally in ~/.docker/config.json if the login is successful.

Tag an image for the private registry

To correctly deliver a desired image to your private registry, you need to tag it with the full registry address. You can do this by running the following:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
docker tag ubuntu:latest your.registry.domain:5000/my-image:my-tag
docker tag ubuntu:latest your.registry.domain:5000/my-image:my-tag
docker tag ubuntu:latest your.registry.domain:5000/my-image:my-tag

Where my-image is the name of your image, and my-tag is the tag you want to assign to it. If you omit the tag (i.e., :my-tag), Docker will default to using :latest.

Push and pull tagged images to the private registry

After you are done tagging, your image is ready to be pushed to your Docker registry. At this stage, Docker will authenticate you (if authentication has been set up), connect to the registry, and upload your image layers:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
docker push your.registry.domain:5000/my-image
docker push your.registry.domain:5000/my-image
docker push your.registry.domain:5000/my-image

To pull the image, simply use the pull command:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
docker pull your.registry.domain:5000/my-image
docker pull your.registry.domain:5000/my-image
docker pull your.registry.domain:5000/my-image

Docker will look for your image in the registry, cache it locally for usage, and pull it. Note that for this step to be successful, your registry needs to be running.

Interacting with images in your Docker registry: example

Let’s take a look at a comprehensive example of how to use your private registry.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# Tag your image
docker tag nginx:latest your.registry.domain:5000/my-image
# Login if needed
docker login your.registry.domain:5000
# Push your image
docker push your.registry.domain:5000/my-image
# On another machine (pointing to the same registry)
docker pull your.registry.domain:5000/my-image
# Browse what’s available in the registry
curl http://your.registry.domain:5000/v2/_catalog
# Tag your image docker tag nginx:latest your.registry.domain:5000/my-image # Login if needed docker login your.registry.domain:5000 # Push your image docker push your.registry.domain:5000/my-image # On another machine (pointing to the same registry) docker pull your.registry.domain:5000/my-image # Browse what’s available in the registry curl http://your.registry.domain:5000/v2/_catalog
# Tag your image

docker tag nginx:latest your.registry.domain:5000/my-image

# Login if needed

docker login your.registry.domain:5000

# Push your image

docker push your.registry.domain:5000/my-image

# On another machine (pointing to the same registry)

docker pull your.registry.domain:5000/my-image

# Browse what’s available in the registry

curl http://your.registry.domain:5000/v2/_catalog

Conclusion

In this guide, we have seen how to set up a private Docker registry to gain full control over those container images you wish to keep private. This also enhances security, keeping your images safe and distributing them with reduced risk, and reduces latency, making for a better development experience both in testing and production environments.

By following the steps outlined in this guide, you can build a secure and efficient foundation for privately managing your container images and scaling your projects without worrying about security.

Docker private registry FAQ

What is a private Docker registry?

A private Docker registry is a local or cloud-hosted service that allows you to store and manage your Docker images privately and securely, and keep them within your own infrastructure or VPS.

How do I set up a private Docker registry?

You can deploy a private registry by launching the Docker Registry container, adding reverse proxy support with Nginx, and enabling HTTPS. You can also secure it with basic authentication.

What’s the difference between a public and private Docker registry?

The key differences are visibility and access. Private registries are self-hosted and secure, used internally within your team or organization. Public registries are shared platforms where images can be downloaded by anyone.

Author
The author

Marta Palandri

Marta Palandri is a senior technical editor with over six years of experience as a developer, working extensively with APIs and backend systems. She now combines her development experience with her editorial background to create content focused on accessibility and storytelling. Find her on LinkedIn.