Host a Private Docker Registry
Tags: docker, devops, productivity, tutorial
Published: 2020-03-30 13:54:37


Hosting a private Docker Registry can help the productivity of teams that are building containers to deploy software and services.  Websites like [Docker Hub](https://hub.docker.com) provide free public repos but not all teams want their containers to be public.  Private repos require a [paid](https://hub.docker.com/pricing) plan that begins at $7/month.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/5z1b8vht7cx8pxy70j1x.png)

AWS, Google, Azure and other public clouds also provide Docker Registry services as well.  These are all priced around $0.10/Gb.  For example if you were using 50 Gb of storage across any number of repos you're hosting them with, it could be around $5/month.

If you maintain a lot of Docker images this could potentially add up.  If you have experience building containers you know how difficult it can be to trim the size of a container.  It's a process to optimize size. 

In this tutorial I'm going to show you how to setup a private Docker Registry, make it publicly available, password-protected and use TLS.  It'll be secure and only available to you and your team, wherever they're located.

# Requirements

This is what you need to run through this tutorial.  We need Docker installed, in addition we'll use docker-compose to make setting this up and maintaining (software updates) more convenient.

* Packetriot [account](https://packetriot.com/signup) 
* [Docker CE](https://docs.docker.com/install/) installed (Linux, Mac or Windows)
* [Docker-compose](https://docs.docker.com/compose/install/)

# Setup the Containers

I use a convention for deploying a set of containers using docker-compose.  I create a directory for storing the compose file and any other small configuration items that I want available my host system.  I use Docker volumes for storing large amounts of data and making it accesseable and recoverable in-case the containers are accidentally destroyed.

This convention makes upgrades easy to perform and we'll review that later in this tutorial.

To start, let's create a directory called `private-registry` and place our compose file `docker-compose.yml` there.

```
[user@host:~/] mkdir private-registry

# change directories
[user@host:~/] cd private-registry

[user@host:~/private-registry ]
```

Below is the compose file we'll use.  It creates two containers, one for the Packetriot client and another for the Docker Registry.  It's good practice to segment containers that don't need to communicate.  To do this we create a separate network called `registry-net`.

Finally, our registry will store *a lot* of data.  Container images can consume space quickly and we don't want to lose any of that data accidently.  We also want to spare the hosts that are pushing these images time.  For that we want to reuse existing layers whenever possible throughout the operational life of this registry.  So we define a volume called `registry-data` to store image data.


```
version: '2'

networks:
  registry-net:
    external: false

volumes:
  registry-data:

services:
  pktriot:
    container_name: tunnel
    image: packetriot/pktriot:latest
    networks:
      - registry-net
    restart: unless-stopped
    volumes:
      - ./pktriot:/data

  registry:
    container_name: registry
    image: registry:2
    networks:
      - registry-net
    restart: unless-stopped
    environment:
      REGISTRY_AUTH: htpasswd
      REGISTRY_AUTH_HTPASSWD_REALM: "Registry Realm"
      REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
    volumes:
      - registry-data:/var/lib/registry
      - ./auth:/auth

```

There are some other important items to highlight in this compose file.  Under the `pktriot` service we have a local directory mapped to `/data` in that container.  `/data` is the default Packetriot client configuration directory for containers.  It's useful to maintain this outside the container so we can upgrade our container.  You want to avoid reconfiguring your tunnel just to upgrade the client.

We also added a volume mapping for the `registry` container.  In addition, we mapped a local directory called `auth` to `/auth` in the container.  This is where we will store the hashed authentication credentials.  Generating them is our next step.

Create a sub-directory called `auth` inside our `private-registry` directory.  We are going to create and hash credentials for Docker Registry using a program called `htpasswd`.  If you don't have `htpasswd` already installed, here are some commands to install on different Linux systems.

```
# debian or ubuntu
[user@host:~/private-registry ] apt-get install apache2-utils

# latest fedora releases
[user@host:~/private-registry ] dnf install httpd-tools

# centos 7.x
[user@host:~/private-registry ] yum install httpd-tools
```

On a Linux system you can use the following command to create a hash for registry credentials.  Replace `username` and `secretpassword` with your own values.

```
[user@host:~/private-registry ] mkdir auth

[user@host:~/private-registry ] htpasswd -Bbn username secretpassword > auth/htpasswd
```

Not on a Linux system?  Well we can use the `registry:2` container image to do this as well.

```
[user@host:~/private-registry ] mkdir auth

[user@host:~/private-registry ] docker run --entrypoint htpasswd registry:2 -Bbn username secretpassword > auth/htpasswd
```

Check out the official [documentation](https://docs.docker.com/registry/deploying/) for more options on setting up authentication for a registry.

We're ready at this point to create our containers and start them up.  Just run the following command `docker-compose up -d`.

```
[user@host:~/private-registry ] docker-compose up -d

Creating network "services_registry-net" with the default driver
Creating volume "services_registry-data" with default driver
Pulling pktriot (packetriot/pktriot:latest)...
latest: Pulling from packetriot/pktriot
e7c96db7181b: Pull complete
bf73012dc58e: Pull complete
144a721ceff9: Pull complete
94370349f88c: Pull complete
1ca8e6a39ad4: Pull complete
Digest: sha256:c0008414d7c0dd40d29d4150bdc67b0d32ae7454372629aafcff6a354f250343
Status: Downloaded newer image for packetriot/pktriot:latest
Pulling registry (registry:2)...
2: Pulling from library/registry
486039affc0a: Pull complete
ba51a3b098e6: Pull complete
8bb4c43d6c8e: Pull complete
6f5f453e5f2d: Pull complete
42bc10b72f42: Pull complete
Digest: sha256:7d081088e4bfd632a88e3f3bcd9e007ef44a796fddfe3261407a3f9f04abe1e7
Status: Downloaded newer image for registry:2
Creating tunnel   ... done
Creating registry ... done

```

Docker-compose will create our the `registry-net` network for our two containers and the volume for our registry.  It will pull down the latest versions of both of our images for the tags we're using in our compose file.  Run the command `docker ps` and check that our containers are running.

```
[user@host:~/private-registry ] docker-compose up -d
CONTAINER ID        IMAGE                       COMMAND                  CREATED              STATUS              PORTS               NAMES
74545f6b7c1b        packetriot/pktriot:latest   "/usr/bin/pktriot --…"   About a minute ago   Up About a minute                       tunnel
000d2a809b65        registry:2                  "/entrypoint.sh /etc…"   About a minute ago   Up About a minute   5000/tcp            registry

```

Our containers are created, the registry is running but our Packetriot client is not yet configured.

# Configure Packetriot Client

The Packetriot client container works just like the client when it's locally available on your computer.  However, we need to use the command `docker exec -it tunnel` to execute commands inside the container.

Before we continue, print out the contents of the local `pktriot` directory.  We'll see a config.json file there.  It's empty, as expected.

```
[user@host:~/private-registry ] ls pktriot/
config.json

[user@host:~/private-registry ] cat pktriot/config.json
{}

[user@host:~/private-registry ] 
```

We're going to configure our client like normal.  If your new to Packetriot check out the [quickstart](https://docs.packetriot.com/quickstart) for a quick run-through of configuring tunnels.


```
[user@host:~/private-registry ] docker exec -it tunnel pktriot configure
Choose a path to the configuration file:
[1] /etc/pktriot/config.json
[2] /data/config.json
[3] /root/.pktriot/config.json

Input selection [#]: 2

Authenticate client with login credentials:

Email: user@example.com
Password: 
Authenticated!

Choose the region for the edge server to connect to:
+------------------------+
| #   | Region           |
+------------------------+
| 1   | us-east          |
+------------------------+
| 2   | us-west          |
+------------------------+
| 3   | eu-central       |
+------------------------+
| 4   | asia-southeast   |
+------------------------+

Input selection [#]: 1

Tunnel configuration:
  Hostname: epic-butterfly-62904.pktriot.net
  Server: us-east-65319.packetriot.net
  IP: 159.203.126.35

Start the tunnel and visit URL to check its working:
  pktriot --config /data/config.json start
  https://epic-butterfly-62904.pktriot.net

Detailed help and step-by-step tutorials:
  https://packetriot.com/docs
  https://packetriot.com/tutorials.

Need more support?
  Email: packetriot@gmail
  Twitter: @packetriot (please follow us, we like new friends :)
```

After we configure the tunnel the client will print out all the information we need to setup our HTTP/S traffic rules.  Note the hostname assigned to this tunnel `epic-butterfly-62904.pktriot.net`.  Yours will obviously be different.

```
Tunnel configuration:
  Hostname: epic-butterfly-62904.pktriot.net
  Server: us-east-65319.packetriot.net
  IP: 159.203.126.35
```

I don't be setting up a custom domain in this tutorial.  That is possible and you can use Let's Encrypt in the client to manage your certificates for you.  Briefly, using a custom domain would require:

1) Verifying ownership of the domain
2) Create an DNS record (A or CNAME) that will point to the server we connect to, `us-east-65319.packetriot.net` or `159.203.126.` 
3) Substitute the assigned hostname in the commands below with your custom domain.
4) Enable Let's Encrypt with the --letsencrypt flag (if you plan to use LE)

Back to the tutorial... 

We can quickly verify that our tunnel is running by just restarting the container.  When there are no rules a client will just provide the default "Welcome" page when you visit the hostname assigned to your tunnel.

```
[user@host:~/private-registry ] docker restart tunnel
```

In our example, we would visit https://epic-butterfly-62904.pktriot.net in our browser to verify the tunnel is working as expected.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ldsvqseska0hft6iyt47.png)

Let's configure our HTTP/S traffic rule.  The hostname assigned to our tunnel will make use the wildcard TLS certificates `*.pktriot.net` on the Packetriot server we connect to, so that takes cares of HTTPS support.

When you run a set of containers in a private Docker network, it will provide name resolution for the container.  E.g. `registry` will be resolved to the containers private IP.  Since these IPs can change, we will use the container name, this will eliminate any need to maintain our traffic rule.

Our rule will forward to the `registry` container using the `--destination` flag and since the registry listens on port 5000 so we will pass that to use that as the argument with `--http`.

```
[user@host:~/private-registry ] pktriot tunnel http add --domain epic-butterfly-62904.pktriot.net \
	--destination registry --http 5000

[user@host:~/private-registry ] docker restart tunnel
```

Restarting the tunnel is necessary.  Our new rule will be setup once the container restarts and now https://epic-butterfly-62904.pktriot.net will make our Docker Registry accessible over TLS.  Since we added authentication, it'll be private.

Verify the tunnel has connected and our new rule will be printed out as well.

```
[user@host:~/private-registry ] docker logs tunnel
Connecting...

Running HTTP services:
+----------------------------------------------------------------------------------------------------------+
| Domain                             | Destination   | HTTP   | TLS   | Secure   | Protect   | Site Root   |
+----------------------------------------------------------------------------------------------------------+
| epic-butterfly-62904.pktriot.net   | registry      | 5000   | 0     | true     |           | --          |
+----------------------------------------------------------------------------------------------------------+
```

# Test

From the computer you're working on, or a remote one, connect to the Docker Registry using the command `docker login`

```
[user@host:~/private-registry ] docker login epic-butterfly-62904.pktriot.net
Username: username
Password: 
WARNING! Your password will be stored unencrypted in /home/user/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded
```

Congrats!  Our private Docker Registry is working.  Let's do a quick test to make sure it works.  We'll pull down the `hello-world` image, tag it and then push to our registry.

```
[user@host:~/private-registry ] docker pull hello-world

[user@host:~/private-registry ] docker tag hello-world epic-butterfly-62904.pktriot.net/hello-world

[user@host:~/private-registry ] docker push epic-butterfly-62904.pktriot.net/hello-world
The push refers to repository [epic-butterfly-62904.pktriot.net/hello-world]
af0b15c8625b: Pushed 
latest: digest: sha256:92c7f9c92844bbbb5d0a101b22f7c2a7949e40f8ea90c8b3bc396879d95e899a size: 524

[user@host:~/private-registry ] 
```

# Updating Our Registry

Updating our set of containers is simple.  We've configured them such that the our authentication credentials, client configuration and image data are all hosted and accessible outside of the containers.  The containers can be destroyed and recreated and to update the underlying software in the containers.

We'll perform some update steps and then verify we can pull down that `hello-world` from our registry when we bring them back up.

```
# stop the containers
[user@host:~/private-registry ] docker-compose stop
Stopping tunnel   ... done
Stopping registry ... done

# destroy the containers
[user@host:~/private-registry ] docker-compose rm 
Going to remove tunnel, registry
Are you sure? [yN] y
Removing tunnel   ... done
Removing registry ... done

# recreate the containers, note, when updates exist the typical
# "image pulling layers" information will be printed
[user@host:~/private-registry ] docker-compose up -d
Creating tunnel   ... done
Creating registry ... done
```

Verify we can still connect to our registry after this "updating" operation.

```
[user@host:~/private-registry ] docker pull epic-butterfly-62904.pktriot.net/hello-world:latest
latest: Pulling from hello-world
Digest: sha256:92c7f9c92844bbbb5d0a101b22f7c2a7949e40f8ea90c8b3bc396879d95e899a
Status: Image is up to date for epic-butterfly-62904.pktriot.net/hello-world:latest
epic-butterfly-62904.pktriot.net/hello-world:latest

```

Awesome, it works!  Now you're ready to host and *maintain* your own private, secure and Internet accessible Docker Registry.

# Conclusion

The process for setting up a private Docker Registry is pretty straight-forward, especially for those already familiar with Docker and the Packetriot client.  

What else could we do next?  I would setup the registry using a custom domain.  You just need to follow the typical steps for exposing a service with a custom domain.  And you can use Let's Encrypt for your TLS certs as well.

It's important to note that container images can be quite big.  Pushing and pulling from your private registry will use bandwidth.  You can definitely exercise this tutorial on your free tier but for practical use, you will want to sign up for at least a Basic ($5/month) plan.

Have any questions or issues reproducing this setup?  Please leave comments below I'm glad to help.  

If you really found this helpful I invite you to share with friends.  Cheers!