Docker – die eigene Registry

Ich habe mittlerweile einige Docker-Container und Images erstellt. Um diese auch auf anderen Systemen einsetzen zu können, gibt es zwei Möglichkeiten: das eigene Konto auf dem Docker-Hub oder die Einrichtung einer privaten Registry.

Für die eigene Registry spricht vor allem, dass man auch Images hochladen kann, die für fremde Nutzer nicht zugänglich sein sollen.

Ziel dieser kleinen Anleitung ist daher die verschlüsselte und passwortgeschützte Erreichbarkeit der eigenen, lokal auf dem Server gespeicherten Registry. Um dies zu erreichen, sind folgende Schritte notwendig:

  • Installation des Docker-Repository
  • Erstellung des Zertifikates für die Verschlüsselung des Zugriffs
  • Installation von NGINX als Reverse-Proxy, um den abgesicherten Zugriff auf den Docker-Dienst zu ermöglichen

Grundlage für dieses Tutorial ist ein Debian Wheezy, die Installation funktioniert unverändert auch auf einem aktuellen Ubuntu LTS (14.4).

Einrichtung der Docker Registry

Die Docker-Registry selbst ist in wenigen Schritten aufgesetzt:

root@meinserver:~$ apt-get install build-essential python-dev libevent-dev liblzma-dev python-pip
root@meinserver:~$ pip install docker-registry
root@meinserver:~$ mkdir /etc/docker
root@meinserver:~$ cp /usr/local/lib/python2.7/dist-packages/config/config_sample.yml /etc/docker/config.yml

Damit steht der Dienst mit einer Beispiel-Konfiguration zur Verfügung. Um eine lokal unter /var/lib/docker erreichbare „produktive“ Registry zu erhalten, muss zum einen die entsprechende Verzeichnis-Struktur existieren:

root@meinserver:~$ mkdir -p /var/lib/docker/registry_prod

Zum anderen sollte der Abschnitt „prod“ in der /etc/docker/config.yml wie folgt aussehen:

prod:
    <<: *local
    storage_path: _env:STORAGE_PATH:/var/lib/docker/registry_prod
    search_backend: _env:SEARCH_BACKEND:sqlalchemy

Ich habe in der Konfigurationsdatei außerdem die Zeile für das SQLite search backend angepasst:

    # SQLite search backend
    sqlalchemy_index_database: _env:SQLALCHEMY_INDEX_DATABASE:sqlite:////var/lib/docker/docker-registry.db

Mit dem Befehl:

root@meinserver:~$ SETTINGS_FLAVOR=prod DOCKER_REGISTRY_CONFIG=/etc/docker/config.yml gunicorn -k gevent -b localhost:5000 —max-requests 100 —graceful-timeout 3600 -t 3600 -w 8 docker_registry.wsgi:application

ließe sich der konfigurierte Daemon schon in Betrieb nehmen.

Ein testweise auf dem gleichen Server abgesetztes „curl localhost:5000“ sollte dann eine Antwort wie: „\“docker-registry server\““ zurückgeben.

Fehlt noch, dass die Registry auch beim Server-Start automatisch zur Verfügung steht. Dazu müssen wir nur das folgende Upstart-Script als /etc/init/docker-registry.conf erstellen:

description "Docker Registry"
version "0.9.0"
author "Docker, Inc."
start on runlevel [2345]
stop on runlevel [016]
respawn
respawn limit 10 5
# set environment variables
env SETTINGS_FLAVOR=prod
env DOCKER_REGISTRY_CONFIG=/etc/docker/config.yml
script
exec gunicorn -k gevent -b localhost:5000 --max-requests 100 --graceful-timeout 3600 -t 3600 -w 8 --access-logfile /var/log/docker-registry-access.log --error-logfile /var/log/docker-registry-server.log docker_registry.wsgi:application
end script

Nach dem Start des Dienstes mit „start docker-registry“ ist der erste Schritt abgeschlossen, auf localhost:5000 hört die Registry ab sofort auf unsere Zugriffe.

Erstellung eines Webserver-SSL-Zertifikates

Als Zertifikat für die Verschlüselung unserer Registry-Zugriffe sind drei Varianten vorstellbar:

  • das selbstsignierte Zertifikat
  • ein Zertifikat, das über eine eigene PKI erzeugt und signiert wird
  • ein Zertifikat einer öffentlichen Zertifizierungsstelle

Option eins ist imho zu unsicher und auch zu umständlich umzusetzen, Variante zwei ist nur in einer unternehmensinternen Netzwerk-Infrastruktur sinnvoll. Bliebe Variante drei.

Wem dabei ein kommerzielles Zertifikat zu teuer ist, der kann sich z.B. über startssl.com ein kostenloses erstellen lassen (mit Let’s Encrypt steht noch eine gemeinnützige Variante zur Vergabe von SSL-Zertifikaten in den Startlöchern, hier ist aber erst im Sommer 2015 mit dem Start des Dienstes zu rechnen). Immer noch nicht das Optimum, aber ein guter Kompromiss.

Ich erspare mir zu erklären, wie man auf startssl.com ein Konto erstellt, um für eine Domäne eigene Zertfikate auzustellen. Wir beginnen bei der eigentlichen Erstellung des Zertifikates selbst. Dazu klicken wir im „Control Panel“ auf „Certficates Wizard“.

Als „Certificate Target“ wählen wir das „Web Server SSL/TLS Certificate“.

Und nachdem auf der nächsten Seite ein Kennwort für das Zertifikat eingetragen ist – die Keysize von 2048bit und der Hash-Algorithmus als SHA2 können so übernommen werden, wird ein privater SSL-Key zum Ausschneiden angeboten.

Diesen Schlüssel fügen wir über einen Editor in die Datei /tmp/ssl.key ein. Wichtig: bitte am Ende des Schlüssels keinen weiteren Zeilenwechsel einfügen!

Jetzt muss der Key noch entschlüsselt werden:

root@meinserver:~$ openssel rsa -in /tmp/ssl.key -out /tmp/ssl.key

Das einzugebende Passwort entspricht dem gerade im Wizard definierten.

Jetzt fehlt noch das eigentliche Zertifikat. Dazu fragt der Wizard nun nach der Domain, für die dieses erstellt werden soll, und nach der Angabe der Subdomain kann die crt-Datei als /tmp/ssl.crt auf dem Server abgelegt werden.

Später benötigen wir außerdem noch das Intermediate-Zertifikat von startssl.com:

root@meinserver:~$ cd /tmp
root@meinserver:~$ wget http://www.startssl.com/certs/sub.class1.server.ca.pem

Danach steht der Einrichtung eines verschlüsselten NGINX-Proxy Zugriff nichts mehr im Wege.

NGINX einrichten

Schlussendlich kann NGINX als Proxy vor die Registry geschaltet werden, um zum einen die Verschlüsselung und zum anderen die Nuetzerauthentifizierung zu erreichen:

root@meinserver:~$ aptitude install nginx-extras apache2-utils
root@meinserver:~$ mkdir /etc/nginx/certs
root@meinserver:~$ cp /tmp/ssl.key /etc/nginx/docker.meinserver.com.key
root@meinserver:~$ cat /tmp/ssl.crt /tmp/sub.class1.server.ca.pem >/etc/nginx/docker.meinsever.com.key
root@meinserver:~$ htpasswd -c /etc/nginx/docker-registry.htpasswd benutzername
root@meinserver:~$ rm /etc/nginx/sites-enabled/default

Kommen wir zur Konfiguration der Proxy-Site. Dazu muss folgende Datei /etc/nginx/sites-available/docker-registry angelegt werden:

upstream docker-registry {
   server localhost:5000;
}

server {
   listen 8080;
   server_name docker.meinserver.com;

   ssl on;
   ssl_certificate /etc/nginx/certs/docker.meinserver.com.crt;
   ssl_certificate_key /etc/nginx/certs/docker.meinserver.com.key;

   proxy_set_header Host $http_host; # required for Docker client sake
   proxy_set_header X-Real-IP $remote_addr; # pass on real client IP
   proxy_set_header Authorization ""; 

   client_max_body_size 0; # disable any limits to avoid HTTP 413 for large image uploads

   # required to avoid HTTP 411: see Issue #1486 (https://github.com/dotcloud/docker/issues/1486)
   chunkin on;

   error_page 411 = @my_411_error;
   location @my_411_error {
      chunkin_resume;
   }

   location / {
      # let Nginx know about our auth file
      auth_basic "Restricted";
      auth_basic_user_file docker-registry.htpasswd;

      proxy_pass http://docker-registry;
   }
   location /_ping {
      auth_basic off;
      proxy_pass http://docker-registry;
   }
   location /v1/_ping {
      auth_basic off;
      proxy_pass http://docker-registry;
   }
}

Mit einem abschließenden

root@meinserver:~$ ln -s /etc/nginx/sites-available/docker-registry /etc/nginx/sites-enabled/docker-registry
root@meinserver:~$ /etc/init.d/nginx start

steht unser Service dann auf docker.meinserver.com:8080 zur Verfügung. Testen lässt sich das bspw. mit:

root@spielwiese:~$ curl -u benutzername:passwort https://docker.meinserver.com:8080

Hier sollte das altbekannte „\“docker-registry server\““ ausgegeben werden.

Auf geht’s zum Testlauf:

Funktioniert auch wirklich alles?

Wir hatten ja im letzten Tutorial schon ein eigenes Docker-Image erstellt. Dies dient nun als Upload-Test für unsere private Registry.

Als kleine Abkürzung, hier die Schritte bis zum eigenen Image noch einmal zusammengefasst:

root@spielwiese:~$ docker pull debian:jessie
root@spielwiese:~$ run -it debian:jessie /bin/bash
... den eigenen container konfigurieren...
root@e9213719e2c0:~$exit
root@spielwiese:~$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e9213719e2c0 debian:jessie "/bin/bash" 8 minutes ago Exited (0) 6 seconds ago determined_poincare
root@spielwiese:~$ docker commit e9213719e2c0 meinservice
root@spielwiese:~$ docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
meinservice latest a44935d36f61 About a minute ago 340.9 MB
debian jessie aaabd2b41e22 3 weeks ago 154.7 MB

Auf diese Weise haben wir das Image meinservice erstellt. Dieses können wir nun taggen und nach der erfolgreichen Anmeldung auf das Docker-Repository pushen:

root@spielwiese:~$ docker login docker.meinserver.com:8080
Username: benutzername
Password:
Email: meine@mail-adresse.com
Login Succeeded
root@spielwiese:~$ docker tag meinservice docker.meinserver.com:8080/meinservice
root@spielwiese:~$ docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
meinservice latest a44935d36f61 2 minutes ago 340.9 MB
docker.meinserver.com:8080/meinservice latest a44935d36f61 2 minutes ago 340.9 MB
debian jessie aaabd2b41e22 3 weeks ago 154.7 MB
root@spielwiese:~$ docker push docker.meinserver.com:8080/meinservice
The push refers to a repository [docker.meinserver.com:8080/meinservice] (len: 1)
Sending image list
Pushing repository docker.meinserver.com:8080/meinservice (1 tags)
Image 511136ea3c5a already pushed, skipping
36fd425d7d8a: Image successfully pushed
aaabd2b41e22: Image successfully pushed
a44935d36f61: Image successfully pushed
Pushing tag for rev [a44935d36f61] on {https://docker.meinserver.com:8080/v1/repositories/meinservice/tags/latest}

Die Images des eigenen Repository lassen sich mit curl auch auflisten:

root@spielwiese:~$ curl -u benutzername:passwort https://docker.meinserver.com:8080/v1/search

Epilog

Meine kleine Anleitung folgt im Groben dem wirklich gut gemachten Tutorial auf digitalocean.com. Chapeau dafür.

Einige Anpassungen waren jedoch nötig – beispielsweise funktionierte der Aufruf des docker-repository nicht, außerdem brach der Push ohne Fehlermeldung ab. Bemerkbar machte sich dann beim Anschließenden Pull-Versuch mit der Fehlermeldung: „Tag latest not found in repository docker.meinserver.com:8080/meinservice“.