Building a Docker Repository with Nexus Repository Manager

Introduction

This article is a rewrite of an old article, so some information may be outdated. However, the NXRM built using this procedure is still running as of September 2026.

It seems that the use of Docker has been increasing, but Docker images were not shared between developers. When using Docker, each developer had to build the images from Dockerfiles individually, which was inconvenient.

To solve this problem, I built a Docker repository. The procedure for building and using it is described below.

I used Nexus Repository OSS (NXRM) to build the repository.

NXRM is an OSS repository manager that can be used not only for Docker repositories, but also for various libraries and images such as Maven. It can also work as a proxy for Docker Hub. In addition, by merging a private (hosted) repository with an external (cached) repository using the group repository feature, internal and external repositories can be handled transparently.

ECR (Elastic Container Registry), etc. can also be used, but NXRM may be easier to adopt for teams where it is difficult to use such services because of cost.

  • In an internal LAN, it is also possible to cache Docker images and various libraries to reduce traffic.
  • It is also possible to build an image with GitLab CI, and then register (push) the image to NXRM. GitLab can also be self-hosted.

Using NXRM may solve repository-related problems. If you have problems like those described above, it may be worth trying.


Environment

  • OS: CentOS 8 (built as a KVM guest) Currently upgraded to Rocky 9
  • The repository directory is mounted from a directory on the host. Since the cache repository tends to become large, I thought mounting a directory from the host would be easier to manage than managing it inside the KVM guest.
  • I initially tried to use 9p_virtio, but it did not work well, so I use NFS.
  • Configuring Blob Stores says to use NFS v4, so I use NFSv4.
  • Configure the NFS server on the KVM host in advance.
  • The directory on the host is /var/opt/nexus.

Prerequisites

  • TLS (HTTPS) is not enabled because this is operated in a closed environment with internal access only. There are several ways to enable TLS for NXRM. Please refer to the official documentation, etc.
  • nexus_hostname is the hostname of the host where Nexus Repository Manager is installed. If the host cannot be accessed by hostname, an IP address can also be used.

Installation Procedure (Server)

dnf update

dnf update

NFS Client Configuration

Mount the directory on the KVM host using NFS.

Install the package

dnf -y install nfs-utils

Edit the configuration file

vi /etc/idmapd.conf

Edit the following part:

Domain = mosaos.local

Mount

First, check whether it can be mounted.

mount -t nfs kvmhost:/var/opt/nexus /mnt

If it works, unmount it.

umount /mnt

Add the configuration to /etc/fstab so that it is automatically mounted.

vi /etc/fstab

Add the following:

kvmhost:/var/opt/nexus /opt/sonatype-work nfs4 rw,sync,tcp,hard,intr,rsize=32768,wsize=32768 0 0

Mount it.

mount -a

Install NXRM

Install it using ansible-playbook.

mosaos/sonatype-nexus3-ansible

Clone the project:

git clone https://github.com/mosaos/sonatype-nexus3-ansible

Run ansible-playbook:

cd sonatype_nexus3_ansible
ansible-playbook -i hosts site.yml

NXRM Configuration

The configuration is mostly based on the following article:

Using Nexus 3 as Your Repository - Part 3: Docker Images

Create the following repositories:

NameTypePort (HTTP)
docker-privatedocker (hosted)5001
docker-hubdocker (proxy)-
docker-groupdocker (group)5000

The settings when creating each repository are as follows.

docker-private

  • Create it as docker (hosted).
  • Check HTTP and specify the port number (5001).
  • Check Enable Docker V1 API:.

docker-hub

  • Create it as docker (proxy).
  • Specify https://registry-1.docker.io for Remote storage:.
  • Select User Docker Hub for Docker Index:.
  • Check Enable Docker V1 API:.

docker-group

  • Create it as docker (group).
  • Check HTTP and specify the port number (5000).
  • Add the two repositories created above to Member repositories:.
  • Check Enable Docker V1 API:.

In my environment, I could not push unless Enable Docker V1 API: was checked.

Additional Firewall Configuration

After creating the repositories, allow 5000/tcp and 5001/tcp on the host where NXRM is installed.

firewall-cmd --add-port=5000/tcp --permanent --zone=public
firewall-cmd --add-port=5001/tcp --permanent --zone=public
firewall-cmd --reload

Installation Procedure (Client)

Set up the Docker environment.

A configuration is required to connect to a repository that does not use TLS.

Environment

I confirmed this procedure with the following environment.

  • OS: CentOS 8
  • Docker: 19.03.8

Connectivity Check

When sending a request to NXRM with curl, if it returns a response saying that it is not a Docker request, it is OK.

curl http://nexus_hostname:5000/
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Error 400 Not a Docker request</title>
</head>
<body><h2>HTTP ERROR 400 Not a Docker request</h2>
<table>
<tr><th>URI:</th><td>/</td></tr>
<tr><th>STATUS:</th><td>400</td></tr>
<tr><th>MESSAGE:</th><td>Not a Docker request</td></tr>
<tr><th>SERVLET:</th><td>-</td></tr>
</table>
<hr><a href="http://eclipse.org/jetty">Powered by Jetty:// 9.4.30.v20200611</a><                                                                                                                                         hr/>

</body>
</html>

Configure daemon.json

Create daemon.json and configure Docker.

vi /etc/docker/daemon.json

When using a repository that does not use TLS, it must be specified in insecure-registries.

It is also possible to specify this every time on the command line, but it is troublesome, so I recommend configuring the Docker daemon.

Add the registries created in NXRM to insecure-registries.

{
  "debug": true,
  "insecure-registries": ["nexus_hostname:5000", "nexus_hostname:5001"]
}

The configuration when also using the mirror setting is described below.

After creating daemon.json, restart the Docker daemon.

systemctl restart docker

Registering (Pushing) Docker Images

Log in to NXRM

Specify an NXRM user for the username and password.

docker login nexus_hostname:5000
docker login nexus_hostname:5001

You will be asked to enter a username and password. Log in with your own account.

A WARNING will be displayed when running the command, but if Login Succeeded is displayed, the login was successful.

The authentication information is recorded in ~/.docker/config.json after running the command.

Push

Push the image to the hosted repository.

docker tag image_name:version nexus_hostname:5001/image_name:version
docker push nexus_hostname:5001/library/image_name:version

For some reason, if I did not add library, I got a 404 (NOT FOUND) when pulling.

I think it may be necessary to use a tag such as accountname/imagename:version, in the same way as Docker Hub. Official images do not have the account name part, but in this kind of tagging, they seem to be under library. In fact, when I downloaded the official MariaDB image and checked it in the proxy repository, its name was library/mariadb.

Pull

Check whether the pushed image can be retrieved.

The destination is not the hosted repository that was registered above, but the group repository that combines the hosted and proxy repositories.

docker pull nexus_hostname:5000/image_name:version

Mirror Configuration

With the configuration so far, it is possible to register (push) and retrieve (pull) Docker images that were created by ourselves from the repository server.

However, with this configuration, our own images and external images (hosted on Docker Hub) cannot be handled transparently.

Change the configuration so that they can be handled transparently.

Specifically, even when retrieving an image hosted by ourselves, it should be possible to retrieve it without specifying repository-server:port, as follows.

When pushing, repository-server:port must be specified.

docker pull image_name:version

Docker can be configured to use registry-mirrors as a Docker Hub mirror, so it should be possible to solve this by configuring the group repository we created as the mirror.

However, there is a problem where authentication information is not correctly passed to the mirror site, so some additional configuration is necessary.

Reference: registry-mirrorsが機能しない

As a solution, I use the method described in Docker is not passing auth informations when pulling from a mirror registry #30880, where the authentication information is overwritten on the Nginx (proxy) side and the request is sent to Nexus.

Edit the nginx Configuration

I finally used the following configuration.

events {
    worker_connections  1024;
}

http {

  proxy_send_timeout 120;
  proxy_read_timeout 300;
  proxy_buffering    off;
  keepalive_timeout  5 5;
  tcp_nodelay        on;

  log_format upstreamlog [$time_local] $remote_addr -  $host - $upstream_addr - $request;
  access_log /var/log/nginx/upstream.log upstreamlog;

  server {
    listen   *:80;
    server_name  www.example.com;

    # allow large uploads of files
    client_max_body_size 1G;

    # optimize downloading files larger than 1G
    #proxy_max_temp_file_size 2G;

    location ~ ^/(v1|v2)/ {
      if ($authorization = '') {
        set $authorization "Basic XyzAbCdefg1234567890Mzk2Zi00NWYxLWJiNjYtN2ExOWRkM2EyOTVk";
      }
      proxy_pass http://127.0.0.1:5000;
      proxy_set_header   Authorization $authorization;
      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_set_header   X-Forwarded-Port $server_port;
    }

    location / {
      # Use IPv4 upstream address instead of DNS name to avoid attempts by nginx to use IPv6 DNS lookup
      proxy_pass http://127.0.0.1:8081/;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
  }
}

When accessing a repository, the URL starts with /v1 or /v2, so in this case the request is forwarded to the group repository. At this time, authentication information is set using proxy_set_header Authorization ....

The Basic Authentication value to set in $authorization can be generated as follows.

echo -n 'username:password' | base64

The above configuration also outputs upstreamlog so that the actual forwarding destination can be checked.

I noticed the problem where pulling did not work unless I added library when pushing by checking this log. If an image can be pushed but cannot be pulled and a 404 occurs, the URL that the Docker client uses to retrieve the manifests may be different from the location where the image was pushed. In this case, checking upstreamlog may help solve the problem.

Restart nginx

Check that there are no problems with the configuration:

nginx -t

If there are no problems, restart it.

systemctl restart nginx

Client-side Configuration

Change /etc/docker/daemon.json as follows. Specify the Nexus host in registry-mirrors and insecure-registries.

{
  "debug": true,
  "insecure-registries": [
    "nexus_hostname",
    "nexus_hostname:5000",
    "nexus_hostname:5001"
  ],
  "registry-mirrors": [
    "http://nexus_hostname"
  ]
}

Troubleshooting

The mirror configuration was the most difficult part for me.

When a problem occurs, I recommend checking the following.

  • Docker (client-side) logs In my environment, they were output to syslog (/var/log/messages/).
  • Nexus Repository Manager logs The output directory is /opt/sonatype-work/nexus3/log.
  • nginx logs If you are using a proxy, also use the upstreamlog described above.

Incorrect Request Destination

When I pushed an image without adding library, it was pushed to the hosted repository with the following structure:

/v2
  |- blobs
  |  |- sha256:.....
  |  |- sha256:.....
  |  |- sha256:.....
  |  |- sha256:.....
  |- image_name
    |- manifests
    |  |- sha256:...
    |- tags
       |- latest

After configuring the mirror, when I ran docker pull image_name, the following error was output in the Docker log:

Oct 13 02:36:40 localhost dockerd[17647]: time="2020-10-13T02:36:40.054981034-04:00" level=info msg="Attempting next endpoint for pull after error: Get https://nexus_hostname:5000/v2/library/image_name/manifests/latest: unauthorized: access to the requested resource is not authorized"

I found that it was accessing /v2/library/image_name/.... (In fact, I noticed this from upstreamlog before checking the Docker log.)

After pushing the image again as follows, I confirmed that it could be pulled.

docker push nexus_hostname:5001/library/image_name:latest
docker pull image_name

References