How to install a Private Docker Registry with JFrog and Docker Compose

David (Dudu) Zbeda
19 min readJan 24, 2025

--

Introduction

There are times when you need a private Docker registry to store your Docker images in a solution separate from your laptop or a public cloud. This is often required for solutions running in on-premises or air-gapped environments, as part of your CI/CD pipeline, or due to strict security requirements. In all these cases, a private Docker registry solution becomes essential.

There are various solutions for setting up a private Docker registry. While many of them offer basic functionality for free, additional features such as security, backups, and advanced integrations typically require an enterprise license. Here are some examples of popular private Docker registry

  • Jfrog JCR
  • Harbor
  • Docker based solution
  • Nexus Repository

Each of these solutions differs in terms of licensing, user interface (UI), built-in security features, backup capabilities, and additional functionality beyond Docker image storage.

In this guide, we will focus on how to install and deploy a private Docker registry based on the JFrog solution. However, it’s important to assess your specific requirements and choose the solution that best fits your needs.

Local Docker Registry vs. Docker Hub vs. Private Docker Registry

Local Docker Registry — when you download, build, and run a Docker image on your laptop, it works because you’ve installed the Docker engine in your environment. The Docker engine provides a local repository where all images are stored. However, this takes up storage space on your internal disk.
To check the Docker images stored in your local registry, you can use the following command docker image ls

docker image ls command

Docker Hub —Docker Hub is a cloud-based Docker registry where you can find official, released, and approved images, such as Ubuntu, Python, Nginx, and more. Additionally, Docker Hub allows you to save your custom images in a private repository provided by the service.

For example, searching for “Nginx” on Docker Hub will display various official and community-provided images:

NGIX search results in Docker Hub

Private Docker Registry (repository) — A private Docker registry is an additional service installed in your environment. It enables you to manage and store Docker images locally for your solution or CI/CD pipeline. This approach is particularly useful for on-premises or air-gapped environments, as well as for organizations with strict security or compliance requirements.

Important:

This guide assumes a basic understanding of Docker. If you’re new to Docker or need a refresher, check out my LinkedIn post where I’ve uploaded a hands-on presentation that guides you through Docker and Kubernetes.

👉 Docker ad Kubernetes training

Blog Goals

This blog will guide you through the process of installing a JFrog Private Docker Registry using Docker Compose. Additionally, we will explore some basic commands to help you get familiar with the JFrog user interface (UI) and demonstrate how to integrate the JFrog Private Docker Registry with the Docker CLI.

Topics Covered in This Blog:

  1. Install JFrog Private Docker Registry using Docker Compose.
  2. Perform basic configuration via the JFrog UI.
  3. Upload a Docker image to the JFrog Private Docker Registry using the UI.
  4. Configure the Docker engine to interact with the JFrog Private Docker Registry.
  5. Upload a Docker image to the JFrog Private Docker Registry using the Docker CLI.
  6. Download a Docker image from the JFrog Private Docker Registry using the Docker CLI.
  7. Delete a Docker image from the JFrog Private Docker Registry using the UI.
  8. Extras

In the Extra section, I will include a script that can:

  • Download a Docker image from any registry (private or cloud).
  • Save the image as a TAR file.
  • Upload the image to a different Docker registry (private or cloud).

Note:
The Docker CLI refers to the Docker engine installed on any server that needs to interact with the JFrog Private Docker Registry.
As part of your CI/CD pipeline, tools like Jenkins require the Docker engine to build Docker images and push them to the private Docker registry.

Prerequisites and Ingredients

Requirements:

  • A Linux machine with internet connectivity, including Docker and Docker Compose installed.
  • For this exercise, I used WSL2 (Windows Subsystem for Linux) running Ubuntu 22.04.

If you’re using Windows and need help installing WSL with Docker, Docker Compose, and related tools, check out my LinkedIn post:
👉 WSL2: Seamlessly Install Ubuntu OS, Docker, and Ansible

Note for WSL Users: Volume Mounting Considerations

When using Docker or Docker Compose on WSL, it’s important to understand how volume mounting works. WSL mounts your Windows drives (e.g., C:\, D:\) under the /mnt/<drive> path by default. However, mounting volumes directly from these /mnt/<drive> paths can lead to:

  • Permission issues
  • Unexpected errors

Solution: Use WSL’s Native File System

To avoid these issues, always place your mount points within WSL’s native file system. For example:

  • Use a directory under /home/<your-username>/<your-mount-volume> instead of /mnt/<drive>.

Let’s start working

Configure and Run Jfrog private docker registry using docker compose

JFrog provides generated scripts to install the JFrog Docker registry solution with various options. However, I found them slightly confusing. Therefore, I’ve created a single Docker Compose file that combines the essential parts to run a JFrog Private Docker Registry along with a PostgreSQL database that serves the registry.

Configuration used in this guide

  • Postgres username: zbeda
  • Postgres password: 1qaz@WSX
  • Postgres DB name: artifactorydb

Here’s a polished and organized version of your section, improving structure, grammar, clarity, and formatting for your blog audience:

Configure and Run JFrog Private Docker Registry Using Docker Compose

JFrog provides generated scripts to install the JFrog Docker registry solution with various options. However, I found them slightly confusing. Therefore, I’ve created a single Docker Compose file that combines the essential parts to run a JFrog Private Docker Registry along with a PostgreSQL database that serves the registry.

Configuration Used in This Guide

  • PostgreSQL Username: zbeda
  • PostgreSQL Password: 1qaz@WSX
  • PostgreSQL Database Name: artifactorydb

Step-by-Step Instructions

  1. Login to you Linux box, If you’re using WSL, verify that you’re in your home folder (refer to the note in the Prerequisites and Ingredients section).
  • home-folder: /home/dzbeda
pwd command — /home/user-name

2. Create your working Directory by executing the command

  • Working folder: /home/dzbeda/jfrog-private-docker-registry
#mkdir -p /<home-directory-path>/<working-directory>
#For the exercise i will be using jfrog-private-docker-registry/

mkdir -p /home/dzbeda/jfrog-private-docker-registry

3. Create data folder for Jfrog under your working directory by executing the command

  • Jfrog data folder: /home/dzbeda/jfrog-private-docker-registry/jfrog-data
mkdir -p /home/dzbeda/jfrog-private-docker-registry/jfrog-data

4. Update jfrog-data folder permission by executing the command

sudo chown 1030:1030 /home/dzbeda/jfrog-private-docker-registry/jfrog-data
sudo chmod 755 /home/dzbeda/jfrog-private-docker-registry/jfrog-data

5. Create data folder for Jfrog under your working directory by executing the command

  • Postgres data folder: /home/dzbeda/jfrog-private-docker-registry/postgres-data
mkdir -p /home/dzbeda/jfrog-private-docker-registry/postgres-data

6. Update postgres-data folder permission by executing the command

sudo chown 999:999 /home/dzbeda/jfrog-private-docker-registry/postgres-data
sudo chmod 700 /home/dzbeda/jfrog-private-docker-registry/postgres-data

7. Navigate to the working directory by executing the command

cd /home/dzbeda/jfrog-private-docker-registry

8. Create a or Download Docker Compose file named jfrog-deployment-docker-compose.yml by executing the command

👉 GitHub Repository: private-docker-registry

vi jfrog-deployment-docker-compose.yml

9. Add the following content to the jfrog-deployment-docker-compose.yml file

version: '3' # Specifies the version of the Docker Compose file format.

services:
artifactory: # Defines the JFrog Artifactory service.
image: releases-docker.jfrog.io/jfrog/artifactory-jcr:7.90.15 # Specifies the Artifactory Docker image and version.
container_name: artifactory # Sets a custom name for the Artifactory container.
environment: # Environment variables for Artifactory to connect to the database.
DB_TYPE: postgresql # Database type used by Artifactory.
DB_HOST: postgres # Hostname of the PostgreSQL service defined below.
DB_PORT: 5432 # Port on which PostgreSQL is running.
DB_USER: zbeda # Database username for connecting to PostgreSQL.
DB_PASSWORD: 1qaz@WSX # Password for the PostgreSQL user.
DB_NAME: artifactorydb # Name of the database for Artifactory.
ports:
- "8081:8081" # Exposes Artifactory’s HTTP port on the host.
- "8082:8082" # Exposes Artifactory’s Docker Registry port on the host.
depends_on: # Ensures Artifactory starts only after the PostgreSQL service is up.
- postgres
volumes:
- ./jfrog-data:/var/opt/jfrog/artifactory # Maps a host directory to persist Artifactory data.
restart: always # Ensures the container restarts automatically if it stops.
ulimits: # Sets container resource limits for better performance.
nproc: 65535 # Maximum number of processes allowed.
nofile:
soft: 32000 # Soft limit for maximum open files.
hard: 40000 # Hard limit for maximum open files.

postgres: # Defines the PostgreSQL database service.
image: releases-docker.jfrog.io/postgres:15.6-alpine # Specifies the PostgreSQL Docker image (Alpine for smaller size).
container_name: postgres # Sets a custom name for the PostgreSQL container.
environment: # Environment variables for PostgreSQL configuration.
POSTGRES_USER: zbeda # PostgreSQL user.
POSTGRES_PASSWORD: 1qaz@WSX # Password for the PostgreSQL user.
POSTGRES_DB: artifactorydb # Name of the database to be created.
volumes:
- ./postgres-data:/var/lib/postgresql/data # Maps a host directory to persist PostgreSQL data.
restart: always # Ensures the container restarts automatically if it stops.
ulimits: # Sets container resource limits for PostgreSQL.
nproc: 65535 # Maximum number of processes allowed.
nofile:
soft: 32000 # Soft limit for maximum open files.
hard: 40000 # Hard limit for maximum open files.

For you setup the following parameters can be updates

  • DB_USER
  • DB_PASSWORD
  • Volume path

Note: Please makes sure to use the same values in both antifactory and Postgres configuration.

10. Run docker compose by executing the command

 docker compose -f jfrog-deployment-docker-compose.yml up

Command explanation:

  • -f flag: Specifies the Docker Compose file to use.
  • up flag: Starts the containers.

11. Let it run for around 30 seconds, then stop it with CTRL+D

12. Update the system.yaml file

By default, JFrog uses Derby DB. Even though we have defined the DB type in the docker compose file as Postgres. To update the DB type to Postgres, run the following steps

  • Navigate to your working directory by executing the command
cd /home/dzbeda/jfrog-private-docker-registry
  • Create or download a file named system.yaml by executing the command

👉 GitHub Repository: private-docker-registry

vi system.yaml
  • Add the following content to the system.yaml file
configVersion: 1
shared:
security:
node:
database:
## To run Artifactory with any database other than PostgreSQL allowNonPostgresql set to true.
allowNonPostgresql: false
type: postgresql
driver: org.postgresql.Driver
# The url include the container name (postgresql) and the DB name (artifactorydb)
url: "jdbc:postgresql://postgres:5432/artifactorydb"
username: zbeda
password: 1qaz@WSX

access:

Make sure to use the same DB user, password and DN name as we have configured in the docker compose file

  • Copy the updated system.yaml to the jfrom-data folder by executing the command
sudo cp system.yaml jfrog-data/etc/

13. Start docker compose by executing the command

docker compose -f jfrog-deployment-docker-compose.yml up

14. Verify logs

  • Verify PostgreSQL is up by checking the logs for: “database system is ready to accept connections”
  • Verify JFrog Private Docker Registry is up by checking for : “All service started successfully”
    Note:
    It may take around 1 minute for JFrog to start. During this time, you might see errors like Router readiness failed with status 503. These can be ignored.

JFrog Private Docker Registry — Basic Configuration

Congratulations! If you’ve reached this phase, it means your JFrog Private Docker Registry is up and running.

  1. Open your browser
  2. Navigate to JFrog UI http://<server-ip-running-docker-compose>:8082
Jfrog private docker registry login page

3. Enter the default username and password

  • username: admin
  • password: password

Note:
During the first login, the setup wizard should start, asking you to:

  • Confirm the EULA.
  • Complete basic setup.

If the wizard starts, proceed to Step 6.

However, if you encounter the message:
“No signed EULA found. To activate, proceed to sign the EULA,”
follow Steps 4–5 before continuing to Step 7.

4. Confirm EULA

Unfortunately, the EULA cannot be confirmed through the UI. Use the following steps to confirm the EULA via API:

From WSL, Linux, or Windows, run the following curl command:

# curl -XPOST -vu <defualt-username>:<default-password< http://jfrog-ip-or-host:8082/artifactory/ui/jcr/eula/accept
curl -XPOST -vu admin:password http://localhost:8082/artifactory/ui/jcr/eula/accept

5. Define baseURL by running the following steps

  • navigate to http://localhost:8082
  • Login with the default credentials.
  • Navigate to Administration > General Managment > Setting
  • Under Custom Base URL define your preferred URL. For this exercise, I’ve used http://zbeda-repo.com
  • Click Save
Define Base URL

6. Configure via wizard (Note: If you have performed steps 4–5, you can skip this step)

  • Confirm the EULA; make sure to read it :-)
  • Skip the sign for updates — or you can sign
  • Set a new password for admin
  • Set Base URL — If you are setting an FQDN name you will need to make sure that the servers from which you are trying to connect, you can resolve the FQDN. For this exercise, I’ve used http://zbeda-repo.com. Instead, you can use your server IP http://<server-IP>
  • Skip the proxy
  • Skip the Create repository
  • Click finish

7. Create new repository by running the following steps

  • navigate to http://localhost:8082
  • Login with the default credentials.
  • Navigate to Administration > Repositories > Create a Repository
Create new repository
  • Click on Local
  • Click on Docker
Docker package
  • Define a name to the repository under Repository Key , I have named it devops and click the Create Local Repository
define repository
Available repositories

8. Create new User

  • Click on Administrator User Managment > Users New user
Create new user
  • I have created jenkins user and gave it admin permission, you can update the Roles and options based on your needs
Create new user
Jenkins user was created

9. To apply all changes, Restart the Jfrog using the following options

  • Press CTRL+D where docker compose is running
  • By executing the command docker stop artifactory postgres

10. Run docker containers using docker compose by executing the command

docker compose -f jfrog-deployment-docker-compose.yml up

11. If you set the Base URL to an FQDN, you need to update the hosts file on your Windows machine to resolve the FQDN

  • In widows nevigate to your hosts file located under C:\Windows\System32\drivers\etc\hosts
  • Add the following record 127.0.0.1 zbeda-repo.com

Upload Docker image via the Jfrog private docker registry UI

In this step, we will download a Docker image from Docker Hub, save it as a TAR file, and upload it to the JFrog Private Docker Registry we set up earlier.

  1. Login to your Linux box —I’ll be using WSL.
  2. Pull the python:alpine image from docker Hub by executing the command
docker pull python:alpine
docker pull — python alpine

3. Once downloaded, the Docker image will be stored in your local Docker repository. You can list all Docker images saved locally by executing the command

docker image ls
docker image ls
  • You should see the python:alpine image listed along with the Postgres and JFrog images (downloaded when you ran Docker Compose earlier).

4. Save the python:alpine image as a TAR by executing the command

# docker save --output <path and file-name>.tar <image you wish to save>:<image-tag>
docker save --output /mnt/d/python-alpine.tar python:alpine
saved image under /mnt/d = Windows D drive

5. Navigate to Jfrog UI http://zbeda-repo.com:8082

6. Navigate to Application > Jfrog Container Registry > Artifacts

7. Click on the Devops repo and Deploy

navigate to devops repo

8. Click on Select file and choose the Python:alpine image & click deploy

9. Verify that the image was uploaded to devops

Docker image was added

Note:

  • When using the JFrog UI, Docker images larger than 100MB cannot be uploaded.
  • For larger images, you must use the Docker CLI to upload them.

Configure Docker engine to integrate with JFrog Private Docker Registry

When integrating with a private Docker registry via the Docker CLI, especially as part of a CI/CD pipeline, additional configuration is often required. This step is essential for two main reasons:

  • Insecure Registry Configuration: If the registry is set up using HTTP (insecure) instead of HTTPS, Docker needs to be explicitly configured to allow communication
  • FQDN Resolution: Since we previously set the Base URL as zbeda-repo.com, we must ensure that the server running Docker CLI can resolve the FQDN to the correct IP address.

In this exercise, I am using Docker CLI from my WSL machine. The same configurations must also be applied to any other server interacting with the JFrog Private Docker Registry, such as your Jenkins client server.

  1. Update /etc/hosts

If you set the Base URL using an IP address, skip this step. However, if you set an FQDN in the Base URL update the /etc/hosts file to ensure proper name resolution. — I’m running these steps in my WSL

  • sudo vi /etc/hosts
  • add the following record 127.0.0.1 zbeda-repo.com
  • Note:I am using 127.0.0.1 because the Docker Compose setup is running on WSL, which is part of my Windows server.
/etc/hosts

2. If the JFrog Private Docker Registry is running as an insecure registry (HTTP), Docker must be explicitly configured to allow it.

  • Configure Jfrog Private Docker Registry as insecure registry by executing the command
sudo vi /etc/docker/daemon.json
  • Add or update the configuration with the following content:
{
"insecure-registries": ["zbeda-repo.com:8082"]
}
  • Restart docker service by executing the command
 sudo systemctl restart docker

Note:
I did notice that sometime restarting the service is not enough and i need to reboot the server. When using WSL , open power shell and execute the command wsl — — shutdown

3. Login to the jfrog private docker registry by executing the command

docker login zbeda-repo.com:8082
  • When prompted, enter the Jenkins username (created in the previous step) and its password.
  • Verify that you receive the message: Login Succeeded
docker lohin

You can now interact with jfrog private docker registry using docker cli

Upload Docker Imageto JFrog Private Docker Registry via Docker CLI

In this step, we will pull an NGINX Docker image, tag it for the JFrog Private Docker Registry, and upload it using the Docker CLI.

  1. Pull the NGINX image from Docker Hub by executing the command
docker pull nginx
docker pull nginx

2. List all Docker images saved in your local Docker repository to ensure the NGINX image was downloaded successfully by executing the command

docker image ls
docker image ls

3. To upload the image to the JFrog Private Docker Registry, you need to tag it with the registry’s repository URL, this is done by executing the command

# docker tag <the image you wish to retag> <repo-url-name>/repo-name/<image-name>:<image:tage>
docker tag nginx:latest zbeda-repo.com:8082/devopds/nginx:latest

4. After tagging, the image is now saved in your local repository with the new tag. Verify this by listing all images by executing the command

docker image ls
Docker tag with remote repository

5.Upload the tagged image to the JFrog Private Docker Registry by executing the command

docker push zbeda-repo.com:8082/devops/nginx:latest
Docker push to private docker repository

6. Verify the docker image was added to the jfrog private docker registry

  • Navigate to Jfrog UI http://zbeda-repo.com:8082
  • Navigate to Application > Jfrog Container Registry > Artifacts > devops
Docker repository view with NGINX

Download Docker Image from JFrog Private Docker Registry to Your Local Docker Repository vi Docker CLI

In this step, we will download the NGINX image from the JFrog Private Docker Registry to the local Docker repository. To prove that the image is being pulled from the JFrog registry, we will first delete the NGINX image from the local repository.

  1. To delete the NGINX docker image run the following steps
  • Check the images currently available in your local Docker repository by executing the command
docker image ls
docker image ls
  • delete the NGIX images by executing the command
docker image rm nginx:latest
docker image rm zbeda-repo.com:8082/devops/nginx:latest
docker image rm
  • verify images were deleted by executing the command
docker image ls

2. To download the NGINX image from the JFrog registry by executing the command

# docker tag <the image you wish to retag> <repo-url-name>/repo-name/<image-name>:<image:tage>
docker pull zbeda-repo.com:8082/devops/nginx:latest
docker pull from Jfrog private docker registry

3. Check that the NGINX image now exists in your local Docker repository by executing the command

docker image ls 

Delete Docker Images From JFrog Private Docker Registry Using JFrog UI

Deleting a Docker image from the JFrog Private Docker Registry requires two steps:

  • Deleting the image or tag.
  • Running the empty Trash Can to permanently remove it.
  1. Navigate to JFrog UI http://zbeda-repo.com:8082

2. Navigate to Application > Jfrog Container Registry > Artifacts > devops

3. Right click on the Nginx Delete or you can do it in TAG level

4. After deletion, the image is moved to the Trash Can. To confirm , Click on the Trash Can , and verify that the image or tag is listed there.

5. Right click and click “Delete Permanently”

delete per image

6. If you have multiple items in the Trash Can, you can delete them all at once. Navigate to Administration > Artifactory setting > General Setting & Click on “Empty trash can

🎉 Congratulations! You now have a fully functional repository that you can seamlessly integrate into your pipeline, use for lab environments, deploy at customer sites, or simply leverage for practice.

If you liked this blog don’t forget to clap and 🙏 Follow me on both Medium and LinkedIn

👉LinkedIn: www.linkedin.com/in/davidzbeda

👉Medium: https://medium.com/@david-dudu-zbeda

Extras

To streamline Docker image management, I’ve developed a script that automates various tasks such as pulling, saving, tagging, pushing, deleting, and scanning Docker images. This script is especially useful when transferring images between online environments to air-gapped environments where direct internet access is unavailable. Below, I outline the script’s capabilities and how it can be used in day-to-day operations.

This script is designed to address common use cases, including:

  • Moving Docker images from online environments to air-gapped environments.
    It ensures images are downloaded, saved as TAR files, and transferred securely to a location where they can be uploaded to an air-gapped Docker registry.
  • Offline deployments for customer sites or private lab setups.
  • Vulnerability scanning of Docker images using Trivy.

👉 GitHub Repository: docker-stuff

  • File location: ./image-management/docker_image_transfer.py

Script Features

The script provides six main functionalities:

  1. Pull and Save Docker Images as TAR Files
  • Pull images from the source Docker registry (defined in image_list).
  • Save the pulled images as TAR files in the folder specified by docker_image_folder.
  • This option is crucial for moving images to air-gapped environments where direct access to online Docker registries is not possible.

2. Load, Tag, and Push Docker Images

  • Load images from the TAR files in the docker_image_folder.
  • Tag the images with the destination Docker registry’s repository details.
  • Push the tagged images to the destination registry.
  • This option is ideal for uploading images to a site-specific or air-gapped Docker registry.

3. Pull, Tag, and Push Docker Images Directly

  • Pull images directly from the source registry.
  • Tag the images with the destination Docker registry’s repository details.
  • Push the tagged images to the destination registry.
  • This feature is primarily useful for lab environments with direct internet access.

4. Delete Images by Original Repository Name

  • Remove images from the local Docker engine based on their original repository name.
  • Use this feature after saving images to TAR files (Option 1).

5. Delete Images by Destination Repository Name

  • Remove images from the local Docker engine based on their destination repository name.
  • Use this option after loading and tagging images from TAR files (Option 2).

6. Scan TAR Images with Trivy

  • Scan TAR files located in the docker_image_folder for vulnerabilities using Trivy.
  • Save the scan results as JSON files in the scan_report_folder.
  • Ensure that Trivy is installed on the server before running this option.

Script Parameters

  • image_list
    A list of images to be managed, including the repository, image name, and tag.
    Example:
image_list = [
{'repository': 'docker.io', 'image-name': 'ubuntu', 'image-tag': 'latest'},
{'repository': 'docker.io', 'image-name': 'python', 'image-tag': 'alpine'}
]
  • destination_docker_registry
    The destination Docker registry URL where images will be pushed. Ensure it ends with a /.
    Example: zbeda-repo.com:8082/devops/
  • docker_image_folder
    The folder where TAR files will be saved or loaded. Ensure it ends with a /.
    Example: /home/zbeda/
  • scan_report_folder
    The folder where Trivy scan reports will be saved. Ensure it ends with a /.
    Example: /home/zbeda/

How to Use the Script

  1. Run the script in a Python-supported environment.
  2. The script displays a menu with six options:
  • [1] Pull images from the original repository and save them as TAR files.
  • [2] Load TAR files, tag images, and push them to the destination repository.
  • [3] Pull, tag, and push images directly to the destination repository.
  • [4] Delete images from the local Docker engine by original repository name.
  • [5] Delete images from the local Docker engine by destination repository name.
  • [6] Scan TAR image files using Trivy.

3. Select an option by entering the corresponding number.

4. Follow the prompts displayed by the script.

If you liked this blog don’t forget to clap and 🙏 Follow me on both Medium and LinkedIn

👉LinkedIn: www.linkedin.com/in/davidzbeda

👉Medium: https://medium.com/@david-dudu-zbeda

--

--

David (Dudu) Zbeda
David (Dudu) Zbeda

Written by David (Dudu) Zbeda

DevOps | Infrastructure Architect | System Integration | Professional Services | Leading Teams & Training Future Experts | Linkedin: linkedin.com/in/davidzbeda

No responses yet