As I build my Docker Swarm cluster, I am needing a private registry. This is so I can host my own images internally, without needing to use a cloud service.

Some assumptions:

  • I will be following the same naming as used in my other Docker posts. You can see those names here: https://www.frakkingsweet.com/docker-swarm-on-debian-and-hyper-v/#Assumptions.
  • You already have a trusted certificate pair that your hosts will accept. Setting this up is way beyond the scope of this post.
  • Your private key does not require a password to be used.
  • Your private and public keys are stored in 2 files in the PEM format.
  • Your private key is ~/certs/docker.example.com.key.pem.
  • Your public key is ~/certs/docker.example.com.pub.pem.
  • You store your local Docker Volumes in /data/docker.
  • You are remotely working with your Docker Swarm from your client. To see how to do this: https://www.frakkingsweet.com/securing-the-remote-docker-instance/. Everything can be done from the client, except for creating the directories and htpasswd file.

To keep things simple, I'm using the local file system to store the files. To keep things secure I will be using the secrets feature of Docker to store my keys.

Build and deploy a local registry

First add your private/public key pair to the secrets in Docker.

docker secret create certificates.docker.example.com.key.pem ~/certs/docker.example.com.key.pem
docker secret create certificates.docker.example.com.pub.pem ~/certs/docker.example.com.pub.pem

If all went well you should have received an ID for each secret, something like m02d5nxibpgqxelioy5g4l0pc. We don't need it so you can ignore it. If you want to verify that the secrets are added, you can run docker secret ls

Now, we need to create the docker-compose.yml for the registry. In this file we will specify everything we need to setup the registry. The REGISTRY_HTTP_SECRET should be set to something random. This is used if you are clustering the registry, or if you don't want to see the warning about it missing. I have a password generator that runs entirely in JavaScript if you want to use it to generate the key: https://apps.frakkingsweet.com/Password/. To keep things simple, I used a 16-character field with numbers and letters only.

We'll save this docker-compose.yml as ~/registry/docker-compose.yml.

version: "3.7"
services:
  registry:
    restart: always
    image: registry:2
    ports:
      - 5000:5000
    environment:
      REGISTRY_HTTP_TLS_CERTIFICATE: /run/secrets/certificate.crt
      REGISTRY_HTTP_TLS_KEY: /run/secrets/certificate.key
      REGISTRY_AUTH: htpasswd
      REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
      REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm
      REGISTRY_HTTP_SECRET: <<CHANGE ME TO A RANDOM VALUE>>
    volumes:
      - type: volume
        source: registry.library
        target: /var/lib/registry
        volume:
          nocopy: true
      - type: volume
        source: registry.auth
        target: /auth
    networks:
      - web
    deploy:
      placement:
        constraints: [node.role == manager]
    secrets:
      - certificate.key
      - certificate.crt
volumes:
  registry.library:
    driver: local
    driver_opts:
      type: none
      device: /data/docker/registry/library
      o: bind
  registry.auth:
    driver: local
    driver_opts:
      type: none
      device: /data/docker/registry/auth
      o: bind
networks:
  web:
secrets:
  certificate.key:
    external: true
    name: certificates.docker.example.com.key.pem
  certificate.crt:
    external: true
    name: certificates.docker.example.com.pub.pem

Now that we have the docker-compose.yml file we need to create the required directories for it. On the Swarm master run the following as root.

mkdir -p /data/docker/registry/library
mkdir -p /data/docker/registry/auth

To create the htpasswd file so we can authenticate to the registry we'll use htpasswd from the registry container on the master node. Following the same names from my previous Docker posts, we'll use a username of user1 and a password of P@ssword1.

docker run --rm --entrypoint htpasswd registry:2 -Bbn user1 P@ssword1 > /data/docker/registry/auth/htpasswd

Now that we have everything created, directories for the volumes, secrets, htpasswd for authentication and the docker-compose.yml file, we need to create the service in the Swarm and start it up. As long as your client is configured to access the Swarm remotely, the rest of this guide will be done on the client.

docker stack deploy -c ~/registry/docker-compose.yml prod

You can see the status of the service by running:

docker service ps prod_registry

Push an image to your new registry

To push an image to the new registry, you first login, then tag the image, then push it. On our client, we will create a basic image based on the ASP.Net Core Application, tag it, then push it to our private registry.

On your client system, login to the new registry:

docker login docker.example.com:5000

Create our Dockerfile with the following contents:

FROM mcr.microsoft.com/dotnet/core/samples:aspnetapp

RUN echo "Hello from the build"

Build and tag our image:

docker image build --tag docker.example.com:5000/testsite:latest .

Push our image:

docker push docker.example.com:5000/testsite:latest .

If you get an error about missing credentials, use docker login docker.example.com:5000 the re-run the push.

Now to run the service and watch it start. The docker-compose.yml file should have the following:

version: "3.7"
services:
  testsite:
    image: docker.example.com:5000/testsite:latest
    ports:
      - 8123:80

Now start the service:

docker stack deploy -c .\docker-compose.yml prod --with-registry-auth

In a browser, you should be able to view the page that is hosted by a container that was pulled from your local registry: http://docker.example.com:8123

When deploying your stack to the Swarm and your image is on your private registry, be sure to include the --with-registry-auth argument. Otherwise, whatever node will run your container will not have access to that image and  docker service ps <service name> will show an error of No such image: <image name>.  The logs on the worker node may also have entries similar to this

Jun 19 20:33:00 docker1 dockerd[52954]: time="2019-06-19T20:33:00.737298982-06:00" level=info msg="Attempting next endpoint for pull after error: Get https://docker.example.com:5000/v2/testsite/manifests/latest: no basic auth credentials"
Jun 19 20:33:00 docker1 dockerd[52954]: time="2019-06-19T20:33:00.737977081-06:00" level=error msg="pulling image failed" error="Get https://docker.example.com:5000/v2/testsite/manifests/latest: no basic auth credentials" module=node/agent/taskmanager node.id=1jqulb76r6kehkntxa8ll4b27 service.id=x853j3aa5re9ttun1bfek1ozb task.id=uv7gnvykuk4p58th7kjzc1fyx
Jun 19 20:33:00 docker1 dockerd[52954]: time="2019-06-19T20:33:00.739003880-06:00" level=error msg="fatal task error" error="No such image: docker.example.com:5000/testsite:latest" module=node/agent/taskmanager node.id=1jqulb76r6kehkntxa8ll4b27 service.id=x853j3aa5re9ttun1bfek1ozb task.id=uv7gnvykuk4p58th7kjzc1fyx
Jun 19 20:33:00 docker1 dockerd[52954]: time="2019-06-19T20:33:00.876358232-06:00" level=warning msg="failed to deactivate service binding for container prod_testsite.1.5myssmf1bhc0ubbnuaeh9gqcy" error="No such container: prod_testsite.1.5myssmf1bhc0ubbnuaeh9gqcy" module=node/agent node.id=1jqulb76r6kehkntxa8ll4b27

And that's it for setting up a local private registry, connecting to it from your local client, pushing an image, and running that image on your Swarm cluster.