Creating a Private Docker Registry – Local Development (Part 1)

This is the first (real) part of a series describing how we set up a private docker registry and front end. It will go over:

  • docker installation
  • a (brief) explanation of what a docker image and container is
  • project structure
  • using docker-compose
  • username/password authorization for the registry
  • testing that the registry works

A private registry is great for:

  • keeping projects that you don't want distributed publicly
  • work in progress or experimental projects

Installation

Hopefully you have Docker installed locally, but if you don't.... Go to the Docker installation page and follow the instructions for your platform. We tend to use Macs for local development here so installation is just a matter of downloading an application and running it. Super easy.

Project Structure

To start, you'll want to create a new project with the following files and directories.

        
          /my-project
|_ docker-compose.yml
|_ server.js
|_ webpack.config.js
|_ /.bin
|_ /auth
|_ /mnt
|_ /public
|_ /utils        
    

Getting started with docker-compose

The docker-compose file is critical for this project. You can do everything from the command line if you really want, but the commands get kind of long. Basing everything on a file will do several things:

  • make it clear what docker is running
  • make it easy (or at least easier) to debug problems with the docker containers
  • allow for adding a frontend later
  • let other people run the project more easily

Eventually this project will have two services defined in this file, but I want to start with the registry first and go over what each part does.

        
          version: '3'
services:
  registry:
    image: registry:2
    container_name: registry
    ports:
      - "5000:5000"
    environment:
      REGISTRY_AUTH: htpasswd
      REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
      REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm
    volumes:
      - ./auth:/auth
      - ./mnt/registry:/var/lib/registry
    restart: always        
    
  • version - the version of syntax docker-compose should use when interpreting the file.
  • services - this is where you can define the structure of each container including: names, ports, volume binding, etc...
  • registry - this is the beginning of the definition for the registry container
  • image - which "blueprint" docker should use as an image to run the container
  • container_name - what to call the container in the docker application (otherwise it gets named something like myproject_registry_1)
  • ports - inside the container the port 5000 is available to the public. Setting 5000:5000 means, use port 5000 on the host (your computer/server/wherever docker is running) and bind it to port 5000 in the container. If you want to use port 5000 on the host for something else, that's ok! Pick any publicly available port instead {my_port}:5000.
  • environment - environment variables to use in the container
    • REGISTRY_AUTH - what kind of security the registry should expect you to log in with. In this case I wanted something straightforward so I chose basic auth secured with htpasswd (more on that later)
    • REGISTRY_AUTH_HTPASSWD_PATH - the location inside the container where the username(s)/password(s) will be kept to check against
    • REGISTRY_AUTH_HTPASSWD_REALM - a way to keep track of which username/password to use
  • volumes - directories and/or files to share between the host and the container in this case I wanted two
    • auth - I wanted to make it easy to add or remove users from being authorized and not have to go into the container to manage this
    • mnt - it's also nice to bind the location where the registry keeps its data so that if it goes down, there's always a local version that will be re-created when the registry comes back.

Basic Authorization

Now I've got everything I need to make a registry container from the registry image. Except one thing....

I want to secure the container so that I (or you...) need to enter a username and password in order to push or pull images from it. That's why I set up the environment variables and mounted the ./auth directory. But I haven't actually created those. So..... what's a simple way to do this so I don't have to keep writing the same commands over and over (locally and on remote servers)? A little script! Here it is.

        
          #!/bin/bash

# found at ./.bin/auth

# create a variable so that everything matches.
CRED=admin

# Create an auth directory at the root
mkdir -p $PWD/auth

# add an htpasswd file with hashed credentials
htpasswd -Bbn $CRED $CRED > $PWD/auth/htpasswd

# create a .env.js file with the same credentials for the express server
echo "module.exports = { USERNAME: '$CRED', PASSWORD: '$CRED' }" > .env.js
        
    

My goal with this is to create two files.

  • the htpasswd file that will be used for access inside the container
  • a javascript file that'll get used by our express server later

Step by step, here's what the script does:

  • for testing purposes, I don't care what the credentials are, so I create a CRED variable and set it to admin.
  • make an auth directory if it doesn't already exist at the root of the project
  • run the htpasswd command that will take admin, set it to the username and password, and create the htpasswd file the registry container expects
  • create a .env.js file for the express server using the same credentials.

The last thing you need to do is make the script executable. Right now, it's a nice text file, but it won't do anything. From the root of the project, run chomd +x ./.bin/auth.

Registry Go!

Logging In

Hopefully you're still with me!

In order to get the registry running and make sure we can push and pull images, you need to run two commands.

  • docker-compose up
  • then open another terminal window and type docker login localhost:5000 (you'll be prompted for a username/password).

If you can login successfully, you're a good part of the way there!

 

Pushing and Pulling

The last part of this initial setup is to check that images can be pushed and pulled. To do that, you need to:

  • pull a public docker image
  • create a new version by tagging it
  • be logged into the local registry
  • push the tagged image.

That looks like this!

  • docker pull hello-world
  • docker tag hello-world localhost:5000/hello/world
  • docker login localhost:5000
  • docker push localhost:5000/hello/world

Conclusion

That's it!

In the next part, I'll show you how to create a small express server that can be used to build a frontend. This will help keep track of what images are available and details about each one.

Posted by Adam Berkowitz