Episode 0x0C: Private Container Registry

Table of Contents

NOTE: Many commands in this post make use of specific constants tied to my own setup. Make sure to tailor these to your own needs. These examples should serve as a guide, not as direct instructions to copy and paste.

NOTE: Check out the final code at homelab repo on my Github account.

Introduction

A simplistic view of Continuous Integration and Continuous Deployment (CI/CD) is as follows: you code your application, test it, build it, create a container image that includes all the dependencies required for your application to run, and then push that image into a container registry. Up to this part is Continuous Integration (CI). Continuous Deployment (CD) involves pulling this image, deploying it in a cluster, ensuring it remains operational, scaling it as necessary, and other related tasks.

When we self-host our server, we might prefer not to use external container registries like Docker Hub. Instead, we can leverage the object storage we configured in the previous episode to set up our own container registry.

For this purpose, we will use the official Docker registry. Let’s get started.

Registry Deployment

Similar to our Minio deployment, we will deploy our registry. Begin by generating authentication credentials.

htpasswd -Bc .htpasswd <your-username>

You will be prompted for a password. Once entered, a .htpasswd file will be created containing your <your-username>:<your-password-hash>. Next, create a sealed secret from this file.

kubectl create secret generic -n registry registry-auth-secret --dry-run=client --from-file=.htpasswd --output yaml | kubeseal -o yaml

Save the output in a YAML file, such as secrets.yaml. Now, create the configuration file for our registry, which we will also define as a sealed secret:

kubeseal apply --dry-run=client -o yaml -n registry -f - <<EOF | kubeseal -o yaml
apiVersion: v1
kind: Secret
metadata:
  name: registry-config-sec
  namespace: registry
stringData:
  config.yml: |
    version: 0.1
    log:
      level: debug
      formatter: json
      fields:
        service: registry
      accesslog:
        disabled: false
    storage:
      s3:
        accesskey: <minio-access-key>
        secretkey: <minio-secret-key>
        region: us-east-1
        bucket: container-repo
        regionendpoint: minio-svc.minio.svc.homelab-k8s:9000 # Minio address
        secure: false
        v4auth: true
        chunksize: 5242880
        rootdirectory: /
      delete:
        enabled: true
      maintenance:
        readonly:
          enabled: false
      redirect:
        disable: true
    http:
      addr: :5000
      secret: <a-local-deployment-secret>
      host: https://docker.registry.<your-host-name>.com
      headers:
        X-Content-Type-Options: [nosniff]
EOF

This configuration allows the registry to access Minio to store images. Also, checkout the other available options by checking out the official documentations. Ensure you have an access token by logging into Minio and navigating to access keys to create one:

Access

Next, create an alias for this access key using the Minio Client and create a bucket for storing container images, named container-repo, or another name of your choosing; just update the configuration accordingly.

mc alias set registry https://minio.api.<your-domain>.com <access-key> <secret-key>
mc mb registry/container-repo

Also, set the regionendpoint to your Minio instance using the internal FQDN format DNS name in the format servicename.namespace.svc.clustername. svc.clustername can also be omitted.

With all configurations in place, define the deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: registry-dpl
  namespace: registry
spec:
  replicas: 1
  selector:
    matchLabels:
      app: registry-app
  template:
    metadata:
      labels:
        app: registry-app
    spec:
      containers:
        - name: registry
          image: registry:2
          ports:
            - containerPort: 5000
          volumeMounts:
            - name: config-volume
              mountPath: /etc/docker/registry
            - name: htpasswd
              mountPath: /auth
          env:
            - name: REGISTRY_AUTH
              value: htpasswd
            - name: REGISTRY_AUTH_HTPASSWD_PATH
              value: /auth/.htpasswd
            - name: REGISTRY_AUTH_HTPASSWD_REALM
              value: Registry Realm
      volumes:
        - name: config-volume
          secret:
            secretName: registry-config-sec
        - name: htpasswd
          secret:
            secretName: registry-auth-secret

And then, the service and ingress configurations:

---
apiVersion: v1
kind: Service
metadata:
  namespace: registry
  name: registry-svc
spec:
  ports:
    - port: 80
      targetPort: 5000
      protocol: TCP
      name: http-port
  selector:
    app: registry-app
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  namespace: registry
  name: registry
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    acme.cert-manager.io/http01-edit-in-place: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "500m"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - docker.registry.<your-domain>.com
      secretName: registry-tls-ingress
  rules:
    - host: docker.registry.<your-domain>.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: registry-svc
                port:
                  name: http-port

We have configured ingress multiple times, so I won’t delve into those details. Just ensure that the proxy-body-size is set to allow uploading large image files.

ArgoCD Application

Just as we did with Minio, we’ll create an ArgoCD application for our registry. Append the following configuration to apps.yaml:

- apiVersion: argoproj.io/v1alpha1
  kind: Application
  metadata:
    name: registry
    namespace: argocd
  spec:
    destination:
      namespace: registry
      server: https://kubernetes.default.svc
    project: default
    source:
      path: applications/registry
      repoURL: https://github.com/<your-github-repo>.git
    syncPolicy:
      automated:
        prune: true
      syncOptions:
        - CreateNamespace=true

This configuration creates an ArgoCD application for our deployment files, assuming they are in the applications/registry directory. Remember to replace <your-github-repo> with the correct registry URL.

Apply this configuration by running:

kubectl apply -f app.yaml

And watch Argo CD magic.

Argocd

Testing the Container Registry

While configuring my desktop Docker client to recognize this registry proved challenging (this is Chat GPTs way of telling I couldn’t do it!), trust me, Kubernetes can pull images from it just fine. You can manually test the API as shown below:

UPDATE: After several weeks of debugging, I finally discovered the root of the issue. By design, the registry redirects requests to blobs directly to the bucket storage, bypassing the container registry to reduce unnecessary traffic. This works seamlessly with cloud-based object storage like S3 buckets. However, in our setup, requests were being redirected to minio-svc.minio.svc.homelab-k8s:9000, the address of our Minio instance. Since minio-svc.minio.svc.homelab-k8s:9000 is an FQDN specific to our Kubernetes cluster, it couldn’t be resolved from our desktop PCs, causing the requests to fail. Frustratingly, Docker Desktop for Mac did not display a useful error message to diagnose this issue initially.

To fix this, we added the following configuration to our registry to disable the redirect:

redirect:
  disable: true

With this change, everything worked as expected.

Let’s now run a test to ensure our container registry is functioning correctly. Create a Simple Dockerfile:

FROM alpine:latest

Build and tag the Dockerfile for your registry:

docker build -f Dockerfile -t docker.registry.<your-domain-name>.com/alpine:test .

Login to your private container registry and push the image:

docker login docker.registry.<your-domain-name>.com -u <your-username> -p <your-password>
docker push docker.registry.<your-domain-name>.com/alpine:test

Now you should be able to curl your image in the registry

curl -u <your-username>:<your-password> -X GET https://docker.registry.<your-domain>.com/v2/alpine/tags/list

Conclustion

With ArgoCD, Minio, Sealed Secrets, and a container registry in place, our setup is well-equipped to deploy applications. In the next episode, we’ll introduce a workflow engine to orchestrate our CI steps, specifically exploring Argo Workflows. Stay tuned! 🚀