Run your own docker registry

I had a need arise to run a configurable instance of the docker registry to host images built for the Kamal deployment tool. In this post, I'll describe the setup of running your own instance of the docker registry.

We'll utilize docker compose for this setup, as well as Traefik to proxy our requests, provide SSL over LetsEncrypt and whitelist our internal IPs.

First you'll want to spin up a virtual machine instance, droplet, EC2, whatever you prefer. For this service it shouldn't need much, I've given it a 1gb and 1vCPU instance.

Be sure you have the latest version of docker installed on this instance.

Configuring Traefik

We'll begin by configuring Traefik. Create a docker-compose.yml file. You'll likely need to create a network in docker as well (simply run docker network create traefik)

version: '3'

services:
  traefik:
    image: traefik
    ports:
    - "80:80"
    - "443:443"
    command:
     - "--log.level=INFO"
     - "--providers.docker=true"
     - "--providers.docker.exposedbydefault=false"
     - "--entrypoints.web.address=:80"
     - "--entrypoints.websecure.address=:443"
     - "--certificatesresolvers.le.acme.httpchallenge=true"
     - "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
     - "--certificatesresolvers.le.acme.caserver=https://acme-v02.api.letsencrypt.org/directory"
     - "--certificatesresolvers.le.acme.email=you@yourdomain.com"
     - "--certificatesresolvers.le.acme.storage=acme.json"
    volumes:
    - /var/run/docker.sock:/var/run/docker.sock
    - ./acme.json:/acme.json
    networks:
     - traefik
 
networks:
  traefik:
    external: true

From top to bottom, what this file is doing is passing command line configuration options to the traefik container. Setting the log level, telling Traefik there is a provider at the docker socket but to not expose any of our other services unless we tell it to. Further on, we're leaving our instance ports of 80 and 443 open to actively accessible from anywhere. These are known as entry points in Traefik and we can define routes and additional services as you'll see later. After that we get into setting up the LetsEncrypt certificate resolvers. This is using the most basic challenge, you can view the Traefik documentation for other scenarios.

The volumes section is identifying the docker socket that Traefik should know about. The acme.json file is a file you'll need to create within the directory you have your docker-compose.yml file. You may need to change its contents to include {} if an empty string causes issue.

If you were able to follow along this far, congratulations, you have a server that can accept connections on 80 and 443. To get that hooked up to your domain, you'll need to change your domains DNS to point to this instance. You should probably do that now.

Docker Registry Setup

Let's get the Docker registry container setup.

Add an extra service entry in your docker-compose.yml file. We'll go ahead and go over each of the configuration options below. If you are okay hosting the repository images on this instance, you can just configure the data directory environment REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data and point it to a local volume /data and be on your way, however I'm going to setup S3 to be the storage provider for this example.

registry:
    restart: always
    image: registry:latest
    environment:
      REGISTRY_AUTH: htpasswd
      REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm
      REGISTRY_AUTH_HTPASSWD_PATH: /auth/registry.password
      REGISTRY_STORAGE: s3
      REGISTRY_STORAGE_S3_ACCESSKEY: accesskey
      REGISTRY_STORAGE_S3_SECRETKEY: secretkey
      REGISTRY_STORAGE_S3_BUCKET: bucket-name
      REGISTRY_STORAGE_S3_REGION: region-1
      REGISTRY_HEALTH_STORAGEDRIVER_ENABLED: false
    volumes:
      - ./auth:/auth
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.registry.rule=Host(`yourdomain.com`)"
      - "traefik.http.routers.registry.tls=true"
      - "traefik.http.routers.registry.tls.certresolver=le"
      - "traefik.http.routers.registry.entrypoints=websecure"
      # You can remove the line below to make accessible publicly
      - "traefik.http.middlewares.private-network.ipwhitelist.sourcerange=comma,delimited,ips,here,to,allow"
      - "traefik.http.routers.registry.middlewares=private-network@docker"
      - "traefik.http.services.registry.loadbalancer.server.port=5000"
    networks:
     - traefik

First we need to install and set the htpasswd on our instance. Install apache2-utils, this includes what we need to generate our authorization information. Run sudo apt install apache2-utils.

Generate your login with htpasswd -Bc registry.password your_user. You can pass in the your user as however you'd like to identify the login. After that, this command will prompt you to enter a password that you'd like to use. The hash is stored in the registry.password file. You can verify your password set correctly with the htpasswd -v registry.password your_user command, which will prompt you to enter the password and return whether correct or not.

As you can see, we're setting up some environment variables. Be sure your pathing matches up. You may need to create a directory for the auth and push the file you just created.

S3 Setup

Do the Amazon S3 bucket configuration dance. And once you're done, bring back those required environment variable values with you and plug them into your docker-compose.yml.

When you're in configuring the IAMS user or group, here's a policy that might help you. I had a bit of an issue on understanding what was required and it seems the S3 storage component needed a special listing ability. The documentation example file is inaccessible or missing on the official Docker Registry repo.

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Effect": "Allow",
			"Action": [
				"s3:ListAllMyBuckets"
			],
			"Resource": "arn:aws:s3:::*"
		},
		{
			"Effect": "Allow",
			"Action": [
				"s3:ListBucket",
				"s3:GetBucketLocation",
				"s3:ListBucketMultipartUploads"
			],
			"Resource": "arn:aws:s3:::your-bucket"
		},
		{
			"Effect": "Allow",
			"Action": [
				"s3:PutObject",
				"s3:GetObject",
				"s3:DeleteObject",
				"s3:ListMultipartUploadParts",
				"s3:AbortMultipartUpload"
			],
			"Resource": "arn:aws:s3:::your-bucket/*"
		}
	]
}

We're coming into our final explanation of the remaining lines of the docker-compose.yml file. The labels under the registry service from top to bottom are telling traefik to be aware of this container. The remaining lines are setting up routers based on your domain and the LetsEncrypt certificate resolver we setup earlier. The middle wares can be configured to only allow your internal cloud network resources to access your registry as an extra security precaution. You can comment or remove that line altogether. Lastly, we tell traefik we're running our service on port 5000. By default, docker registry runs there.

Final Thoughts

You should be up and running with a docker compose up -d command. You can attempt to login to your registry from another server or your own terminal with docker login registry.yourdomain.com.

You can view the full gist here.