self hosted Docker Image Registry mit UI

Warum eine eigene Registry?

In dieser Anleitung wird eine Docker Registry eingerichtet und mittels Frontend zugängig gemacht.
Gründe hierfür können vielschichtig sein, zum Beispiel:

  • eigene Images sind nicht öffentlich erreichbar (z.B. für selbstentwickelte Software)
  • Cache um vom öffentlichen Netzwerk unabhängig zu sein
  • keine Veränderung der Images, weil man selbst diue Kontrolle über die Registry hat

Idee

Es werden 2 Container gestartet, die Registry selbst und das Frontend. Das Frontend selbst benötigt keine Persistenz und die nötigen Informationen werden vom Browser beschafft.
Die UI verhält sich somit wie eine Clientanwendung, die selbst nicht mit der Registry spricht, sondern lediglich das visualisiert, was der Browser anfragt.

Die Registry und UI werden im selben nginx vHost laufen, können aber auch getrennt werden, hierzu sind weitere Einstellungen nötig, siehe Hinweise.

Zusätzlich kann das Frontend und die Registry mit Basic Auth geschützt werden, dies wird ebenfalls konfiguriert.

Konfiguration

Docker

Die REGISTRY_URL muss auf den Hostname, unter dem die Registry erreichbar ist, gesetzt werden (z.B. myregistry.mydomain.com).
Ob Images gelöscht werden können, stellt REGISTRY_STORAGE_DELETE_ENABLED sicher, das Frontend bietet die Option an, wenn DELETE_IMAGES gesetzt ist.
Weil die Aktionen selbst vom Browser ausgeführt werden und nicht vom Frontend, sind beide Einstellungen auf true zu setzen.

Weitere Einstellungen können der Doku entnommen werden: https://github.com/Joxit/docker-registry-ui

Docker Compose
version: "3"
services:
  registry:
    container_name: registry
    image: registry:2
    restart: unless-stopped
    volumes:
      - /srv/docker/registry/data/:/var/lib/registry
    environment:
      REGISTRY_STORAGE_DELETE_ENABLED: "true"
    networks:
      default:
        ipv4_address: 172.16.2.2

  registry-ui:
    container_name: registry-ui
    image: joxit/docker-registry-ui:latest
    restart: unless-stopped
    environment:
      REGISTRY_URL: "https://<REGISTRY_HOSTNAME>"
      SINGLE_REGISTRY: "true"
      DELETE_IMAGES: "true"
    networks:
      default:
        ipv4_address: 172.16.2.3

networks:
  default:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 172.16.2.0/24
    driver_opts:
      com.docker.network.bridge.name: br_registry

Danach können die Container gestartet werden.

HTTP Proxy (nginx)

Hier kann auch Apache, Traefik oder ein anderer Proxy der persönlichen Wahl genutzt werden.
Nginx ist aber bekannt und deswegen hier die spezifische Konfiguration für die beiden Dienste.
Um die Konfiguration einfach darstellen zu können, verzichte ich auf den ganzen Binding, SSL, Logging Overhead (siehe TODO)!

Basic Auth File generieren

Hier gibt es verschiedene Wege, Beispiele findest du hier: Anleitung zu Salted Password Hashes

Der Inhalt muss dann in die Datei /etc/nginx/htpasswd/myregistry.htpasswd gepackt werden und hat den folgenden Syntax:

username:<PASSWORD_HASH>
test:<PASSWORD_HASH>
# siehe add_header 'Docker-Distribution-Api-Version'
map $upstream_http_docker_distribution_api_version $docker_distribution_api_version {
    '' 'registry/2.0';
}

server {

    ################## TODO ###############################
    ADD NGINX CONFIGURATION (LISTEN, LOG, SERVERNAME, ....)
    #######################################################


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

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

    auth_basic              "Registry realm";
    auth_basic_user_file    /etc/nginx/htpasswd/myregistry.htpasswd;

    location / {
        proxy_pass          http://172.16.2.3:80;
        proxy_set_header    Host                $http_host;
        proxy_set_header    X-Real-IP           $remote_addr;
        proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
        proxy_set_header    X-Forwarded-Proto   $scheme;
        proxy_read_timeout  900;
    }

    location /v2/ {
        # Do not allow connections from docker 1.5 and earlier
        # docker pre-1.6.0 did not properly set the user agent on ping, catch "Go *" user agents
        if ($http_user_agent ~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go ).*$" ) {
            return 404;
        }
        # To add basic authentication to v2 use auth_basic setting.
        satisfy any;
        # disable basic auth for specific IP
        # allow x.x.x.x;

        ## If $docker_distribution_api_version is empty, the header is not added.
        ## See the map directive above where this variable is defined.
        add_header 'Docker-Distribution-Api-Version' $docker_distribution_api_version always;

        proxy_pass                              http:/172.16.2.3:5000;
        proxy_set_header    Host                $http_host;  # required for docker client's sake
        proxy_set_header    X-Real-IP           $remote_addr; # pass on real client's IP
        proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
        proxy_set_header    X-Forwarded-Proto   $scheme;
        proxy_read_timeout  900;
    }
}

Zugriff

Wenn man sich das Passwort gemerkt hat (…), sollte ein Login möglich sein:

docker login myregistry.domain.com

Der Login kann auch automatisch passieren, wenn in der Datei PASSWORD_FILE das Klartextpasswort steht:

cat PASSWORD_FILE | docker login --username testuser

Hinweise

unterschiedliche Hostnames für Registry & Frontend

Soll das Frontend unter einer anderen URL als die Registry verfügbar gemacht werden, kann es nötig sein CORS Einstellungenin nginx vorzunehmen um dem Browser gültige URLs für API Calls mitzugeben. Links zum Thema: