Contents

Your personal Docker registry, like a pro

What

In this article, we’ll create a docker registry, as a docker container, with authentication and authorization, a web gui, with public readonly access and readwrite access based on unsername/password.

All of which behind a nice reverse proxy, with Let’s Encrypt certificate.

Details

There will be 4 containers:

  • the reverse proxy, which will mange the SSL connection and route the traffic to the correct container; this is the only container facing the external interface. As we can use this container for other things, this will be managed separately from the docker-specific containers.
  • the docker registry itself: this is the standard docker registry container, with token authentication enabled.
  • the docker authentication container, which will manage authentication and authorization/ACL. We’ll create all the users here.
  • the docker web ui: this container will be displayed when the domain is accessed via browser As stated before, the proxy server will be the only container which can be accessed from the outside world. Anything else is restricted on an internal docker network.

The final product will have:

Just replace docker.example.com with your domain/subdomain.

Prerequisites

  • A server/VM with a static IP address
  • A linux distro installed on that server
  • Docker (and docker-compose) installed on that server
  • A domain/subdomain pointing to this server’s address

First step: the proxy

We’ll store all the data in the /srv directory. Feel free to adapt the config to your needs, if you don’t like this.

Please note that if you use a directory different than proxy-manager, the resulting backend network will be named differently. Just update the docker-compose.yml file in the second step accordingly.

 1mkdir -p /srv/proxy-manager
 2cd /srv/proxy-manager
 3
 4cat << EOF  > /srv/proxy-manager/docker-compose.yml
 5version: "3"
 6services:
 7  app:
 8    image: jc21/nginx-proxy-manager:latest
 9    restart: always
10    ports:
11      - 0.0.0.0:80:80
12      - 0.0.0.0:81:81
13      - 0.0.0.0:443:443
14    networks:
15      - default
16      - backend
17    volumes:
18      - /srv/proxy-manager/config.json:/app/config/production.json
19      - /srv/proxy-manager/data:/data
20      - /srv/proxy-manager/letsencrypt:/etc/letsencrypt
21    depends_on:
22      - db
23  db:
24    image: jc21/mariadb-aria
25    restart: always
26    environment:
27      MYSQL_ROOT_PASSWORD: "joo2uab3seeNgahDe1"
28      MYSQL_DATABASE: "nginxproxymanager"
29      MYSQL_USER: "nginxproxymanager"
30      MYSQL_PASSWORD: "opea7AoCe5oquoocei"
31    volumes:
32      - /srv/proxy-manager/data/mysql:/var/lib/mysql
33
34networks:
35  backend:
36    external: false
37    driver: bridge
38EOF
39
40cat << EOF >  /srv/proxy-manager/config.json
41{
42  "database": {
43    "engine": "mysql",
44    "host": "db",
45    "name": "nginxproxymanager",
46    "user": "nginxproxymanager",
47    "password": "opea7AoCe5oquoocei",
48    "port": 3306
49  }
50}
51EOF
52
53docker-compose up -d

Please, please, please, replace ALL the passwords above. Also, replace ‘0.0.0.0’ with your server’s external IP address. Now that this container is started, it’s time to configure it.

/2019/personal-docker-registry/selezione_005_hu01483c78ef78e6a253409565b4b86f8d_8986_8d92e07b52fb82abf471e454b7db6378.webp

Open a browser and go to http://your.ip.address:81, and login with the following credentials:

Now follow the wizard and create a more secure account with a robust password.

We’ll come back to this interface after the creation of other containers.

For more info, take a look at this page.

Some info about docker authentication

The default registry setup is unauthenticated; that means everyone has read/write access to all images, which is not acceptable in most cases (this included).

You can easily add authentication, with native basic authentication (see here) but any authenticated user still have full read/write access. So this is also a no-go.

The answer is to add a token server, which is a bit more complicated, but offers authentication and authorization. The registry and the auth shares common encryption certificates for token generation and validation.

But there’s a catch: when you contact the registry anonymously, AKA without a token (this happens with curl or with some other clients that we’ll see below), you’re requested to authenticate to the auth server, that’ll give you a token, that you have to use for your request.

This sucks, but it makes sense: if the registry is configured for external authentication and authorization, it can’t know what is anonymously available and what isn’t, so every access has to go through the auth server.

This sucks because a lot of docker-web-UIs doesn’t expect that, and this case is not handled very well. In fact the webUI in this article is the only one I managed to put in production.

Here is a comment of mine that goes a bit in detail of what happens.

Other web interfaces I’ve tried without luck (not in this order):

This works: https://github.com/klausmeyer/docker-registry-browser

Second step: the registry

1mkdir -p /srv/docker/certs /srv/docker/auth/config
2cd /srv/docker/certs
3openssl req -nodes -new -newkey rsa:4096 > registry.csr
4openssl rsa -in privkey.pem -out registry.key
5openssl x509 -in registry.csr -out registry.crt -req -signkey registry.key -days 10000

The thing above is needed for the token authentication in docker. Both the registry and the auth requires these certs. You can enter the information you want (organization and so on), it doesn’t matter at all.

 1cat << EOF > /srv/docker/auth/config/auth_config.yml
 2server:
 3  addr: ":5001"
 4
 5token:
 6  issuer: "The Auth Service"  # Must match issuer in the Registry config.
 7  expiration: 900
 8  certificate: "/certs/registry.crt"
 9  key: "/certs/privkey.pem"
10
11users:
12  # Password is specified as a BCrypt hash. Use `htpasswd -nB USERNAME` to generate.
13  "god":
14    password: "$2y$05$dfgB/wZJwB0jrbhd0g804OT5zrqQnbfcsGqYnGnChnomSrrY3fisq"
15  "webuser":
16    password: "$2y$05$GCRVqbDHv0UkWX0LzKeTNeZhZ756uiVHz8D7xlOirL9z8S75tLNye"
17  "": {}
18
19acl:
20  - actions: ['*']
21    match: {account: '', name: catalog, type: registry}
22
23  - actions: ['*']
24    match: {account: 'webuser', name: catalog, type: registry}
25
26  - match: {account: "god"}
27    actions: ["*"]
28
29  - match: {account: "webuser"}
30    actions: ["pull"]
31
32  - match: {account: ""}
33    actions: ["pull"]
34EOF

Please take note of the “token issuer” (you can change it, but you have to change it in the following docker-compose.yml file). In this example, we have two users:

  • god with password eij5Eghaipha7aebee
  • webuser with password Cemur6oosh5ahphupu

Please change these passwords. You have to use htpasswd -nB USERNAME to get a new password. If you don’t have htpasswd in your system, you probably need to install a package (usually apache2-utils).

These are the final user accounts used by our registry. The webuser is required for the webUI to work.

For more info on the configuration of the ACLs, look here.

 1cat << EOF > /srv/docker/docker-compose.json
 2version: '2'
 3
 4services:
 5  registry:
 6    image: registry:2
 7    expose:
 8      - 5000
 9    volumes:
10      - /srv/docker/registry/data:/var/lib/registry/
11      - /srv/docker/certs:/certs:ro
12    networks:
13      - backend
14    depends_on:
15      - auth
16    restart: always
17    environment:
18      - REGISTRY_HTTP_ADDR=0.0.0.0:5000
19      - REGISTRY_AUTH=token
20      - REGISTRY_AUTH_TOKEN_REALM=https://docker.example.com/auth
21      - REGISTRY_AUTH_TOKEN_SERVICE="The Docker Registry"
22      - REGISTRY_AUTH_TOKEN_ISSUER="The Auth Service"
23      - REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE=/certs/registry.crt
24
25
26  auth:
27    image: cesanta/docker_auth
28    networks:
29      - backend
30    expose:
31      - 5001
32    volumes:
33      - /srv/docker/auth/config:/config:ro
34      - /srv/docker/auth/logs:/logs
35      - /srv/docker/certs:/certs:ro
36    command: /config/auth_config.yml
37    restart: always
38
39
40  webui:
41    image: klausmeyer/docker-registry-browser
42    expose:
43      - 8080
44    networks:
45      - backend
46    restart: always
47    depends_on:
48      - registry
49    environment:
50      - DOCKER_REGISTRY_URL=https://docker.example.com
51      - TOKEN_AUTH_USER=webuser
52      - TOKEN_AUTH_PASSWORD=Cemur6oosh5ahphupu
53      - ENABLE_DELETE_IMAGES=false
54
55networks:
56  backend:
57    external:
58      name: proxy-manager_backend
59EOF
60
61docker-compose up -d

Now all the containers should be up and running.

Third step: configure the proxy

Time to go back to the proxy web interface.

After login, you should be greeted with this empty dashboard:

/2019/personal-docker-registry/selezione_006_hu4d7c957dea81ce70047891235da480c9_9352_6d30dec38738fd972fb8dffe3853c052.webp

Select “Hosts / Proxy Hosts”

/2019/personal-docker-registry/selezione_008_hu832251787ef2ad748a589c88fcb3b51c_8995_81311067ecc2efae7494337df910043d.webp

Now click “Add Proxy Host”

/2019/personal-docker-registry/selezione_009_hud0286ed64ab8af15f804aadfb05620ba_8875_bde50be75c3ad1ac9e0dfba7c315f429.webp

Now enter the correct domain name, select http as scheme (this is the scheme of the destination container), enter docker_webui_1 as the “forward hostname” and 8080 as port. Then click on “Custom locations”.

/2019/personal-docker-registry/selezione_007_hu63ba07fc3a8bb875de02bba3580aa0f7_13020_d122c132605d02b995f503b320aaa9f5.webp

Add two locations:

  • /v2 will point to docker_registry_1 on port 5000 http
  • /auth will point to docker_auth_1 on port 5001 http Then click on SSL

/2019/personal-docker-registry/selezione_010_hu465ca9a90002d1a3dd29a99bd6632b2d_16707_00130f762c8ac0ab5770bafe42e6ab56.webp

You have to select “Request a new SSL certificate” so that this container can do its magic with Let’s Encrypt.

Please enable “Force SSL” and “HTTP/2 support”.

/2019/personal-docker-registry/selezione_011_hu5819748338ac2a2f2603621eea39de32_13057_02f0f58d03b0f130c65e42e089154e7e.webp

Then click SAVE.

Test it

1docker login docker.example.com

Enter username god with the corresponding password. Now we’ll try to push an image.

1docker pull alpine:latest
2docker tag alpine:latest docker.example.com/alpine:latest
3docker push docker.example.com/alpine:latest

Now, if you go to https://docker.example.com/ you can browse all your images.