Installing Docker and a Private Registry
Install a private docker registry and configure our cluster to accept it as an image source.
Now that we can expose our services outside the cluster it is time to look into setting up a build pipeline for our own images in earnest.
In this article we are going to install Docker and a private Image Registry. Convincing containerd to accept images from that registry will take some persuasion, but don’t worry, we will succeed.
- Installing Docker on k3s
- Verifying successful installation
- Installing a private Docker Registry
- Configuring Kubernetes for the Private Registry
- Copy an image from docker hub to our private registry
- Conclusion
Installing Docker on k3s
I will look into options to create docker images by running the build process inside a container at a later time. For the moment i am content to use a docker installation on the master node. After trying (and failing) to install from the OS packages i decided to use the install script from get.docker.com. The script requires sudo entitlements, the installation process is straightforward.
N.B. I looked into running docker root-less, but since it is a temporary solution i decided it was not worth the trouble. If you want to go ahead with rootless operation, follow the instructions at the end of the installation log. The required scripts are present in /usr/bin.
# curl -fsSL https://get.docker.com -o get-docker.sh
# ./get-docker.sh
# Executing docker install script, commit: e5543d473431b782227f8908005543bb4389b8de
+ sh -c apt-get update -qq >/dev/null
+ sh -c DEBIAN_FRONTEND=noninteractive apt-get install -y -qq apt-transport-https ca-certificates curl >/dev/null
+ sh -c install -m 0755 -d /etc/apt/keyrings
+ sh -c curl -fsSL "https://download.docker.com/linux/debian/gpg" | gpg --dearmor --yes -o /etc/apt/keyrings/docker.gpg
+ sh -c chmod a+r /etc/apt/keyrings/docker.gpg
+ sh -c echo "deb [arch=arm64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian bullseye stable" > /etc/apt/sources.list.d/docker.list
+ sh -c apt-get update -qq >/dev/null
+ sh -c DEBIAN_FRONTEND=noninteractive apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin docker-ce-rootless-extras docker-buildx-plugin >/dev/null
+ sh -c docker version
Client: Docker Engine - Community
Version: 24.0.6
API version: 1.43
Go version: go1.20.7
Git commit: ed223bc
Built: Mon Sep 4 12:31:36 2023
OS/Arch: linux/arm64
Context: default
Server: Docker Engine - Community
Engine:
Version: 24.0.6
API version: 1.43 (minimum version 1.12)
Go version: go1.20.7
Git commit: 1a79695
Built: Mon Sep 4 12:31:36 2023
OS/Arch: linux/arm64
Experimental: false
containerd:
Version: 1.6.24
GitCommit: 61f9fd88f79f081d64d6fa3bb1a0dc71ec870523
runc:
Version: 1.1.9
GitCommit: v1.1.9-0-gccaecfc
docker-init:
Version: 0.19.0
GitCommit: de40ad0
================================================================================
To run Docker as a non-privileged user, consider setting up the
Docker daemon in rootless mode for your user:
dockerd-rootless-setuptool.sh install
Visit https://docs.docker.com/go/rootless/ to learn about rootless mode.
To run the Docker daemon as a fully privileged service, but granting non-root
users access, refer to https://docs.docker.com/go/daemon-access/
WARNING: Access to the remote API on a privileged Docker daemon is equivalent
to root access on the host. Refer to the 'Docker daemon attack surface'
documentation for details: https://docs.docker.com/go/attack-surface/
================================================================================
Verifying successful installation
Running the ‘docker version’ command should get you output similar to the one below.
# docker version
Client: Docker Engine - Community
Version: 24.0.7
API version: 1.43
Go version: go1.20.10
Git commit: afdd53b
Built: Thu Oct 26 09:08:29 2023
OS/Arch: linux/arm64
Context: default
Server: Docker Engine - Community
Engine:
Version: 24.0.7
API version: 1.43 (minimum version 1.12)
Go version: go1.20.10
Git commit: 311b9ff
Built: Thu Oct 26 09:08:29 2023
OS/Arch: linux/arm64
Experimental: false
containerd:
Version: 1.6.25
GitCommit: d8f198a4ed8892c764191ef7b3b06d8a2eeb5c7f
runc:
Version: 1.1.10
GitCommit: v1.1.10-0-g18a0cb0
docker-init:
Version: 0.19.0
GitCommit: de40ad0
Installing a private Docker Registry
We are going to install the registry straight from a docker hub image, so we have to define the required Kubernetes objects on our own. The .yaml file below takes care of that. When you apply it it will create :
- the ‘docker-registry’ namespace
- a Longhorn Persistent Volume Claim ‘docker-registry’
- a Service exposing the registry port 5000
- a Deployment configuration that identifies the image to pull (v2) and sets a number of environment variables.
- a MetalLB Load Balancer service that exposes the registry with a cluster-external IP address
#File: 'docker-registry.yaml'
cat > ./docker-registry.yaml << EOT
---
apiVersion: v1
kind: Namespace
metadata:
name: docker-registry
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: docker-registry
namespace: docker-registry
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 15Gi
---
apiVersion: v1
kind: Service
metadata:
name: docker-registry-service
namespace: docker-registry
labels:
run: docker-registry
spec:
selector:
app: docker-registry
ports:
- protocol: TCP
port: 5000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: docker-registry
namespace: docker-registry
labels:
app: docker-registry
spec:
replicas: 1
selector:
matchLabels:
app: docker-registry
template:
metadata:
labels:
app: docker-registry
spec:
containers:
- name: docker-registry
image: registry:2
ports:
- containerPort: 5000
protocol: TCP
volumeMounts:
- name: storage
mountPath: /var/lib/registry
env:
- name: REGISTRY_HTTP_ADDR
value: :5000
- name: REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY
value: /var/lib/registry
volumes:
- name: storage
persistentVolumeClaim:
claimName: docker-registry
---
apiVersion: v1
kind: Service
metadata:
labels:
app: docker-registry
name: docker-registry-lb
namespace: docker-registry
spec:
type: LoadBalancer
ports:
- port: 5000
protocol: TCP
targetPort: 5000
selector:
app: docker-registry
EOT
kubectl apply -f docker-registry.yaml
Verifying successful installation
List the kubernetes resources in the ‘docker-registry’ namespace :
$ kubectl -n docker-registry get all -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod/docker-registry-7596d688dd-x574l 1/1 Running 0 4d6h 10.42.1.121 rbpic0n2 <none> <none>
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
service/docker-registry-service ClusterIP 10.43.90.47 <none> 5000/TCP 28d app=docker-registry
service/docker-registry-lb LoadBalancer 10.43.241.23 192.168.100.153 5000:31881/TCP 28d app=docker-registry
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
deployment.apps/docker-registry 1/1 1 1 28d docker-registry registry:2 app=docker-registry
NAME DESIRED CURRENT READY AGE CONTAINERS IMAGES SELECTOR
replicaset.apps/docker-registry-7596d688dd 1 1 1 28d docker-registry registry:2 app=docker-registry,pod-template-hash=7596d688dd
Looks good. Let’s verify that the persistent volume claim has been provisioned.
$ kubectl -n docker-registry get pvc -o wide
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE VOLUMEMODE
docker-registry Bound pvc-3900fd18-80ad-420d-9734-9b03e945c6cc 15Gi RWO longhorn 28d Filesystem
Ja, looks good as well. Finally let’s check the registry log.
$ kubectl logs -n docker-registry pod/docker-registry-7596d688dd-x574l
time="2023-12-11T21:55:03.258861361Z" level=info msg="Starting upload purge in 41m0s" go.version=go1.20.8 instance.id=790fd057-56ea-4fc4-a1ce-498828a91271 service=registry version=2.8.3 ...
time="2023-12-11T21:55:03.258670013Z" level=warning msg="No HTTP secret provided - generated random secret. This may cause problems with uploads if multiple registries are behind a load-...
time="2023-12-11T21:55:03.259068523Z" level=info msg="redis not configured" go.version=go1.20.8 instance.id=790fd057-56ea-4fc4-a1ce-498828a91271 service=registry version=2.8.3 ...
time="2023-12-11T21:55:03.259448312Z" level=info msg="using inmemory blob descriptor cache" go.version=go1.20.8 instance.id=790fd057-56ea-4fc4-a1ce-498828a91271 service=registry version=...
time="2023-12-11T21:55:03.260292794Z" level=info msg="restricting TLS version to tls1.2 or higher" go.version=go1.20.8 instance.id=790fd057-56ea-4fc4-a1ce-498828a91271 service=registry v...
time="2023-12-11T21:55:03.260384699Z" level=info msg="restricting TLS cipher suites to: TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH...
time="2023-12-11T21:55:03.264072343Z" level=info msg="listening on [::]:5000, tls" go.version=go1.20.8 instance.id=790fd057-56ea-4fc4-a1ce-498828a91271 service=registry version=2.8.3 ...
time="2023-12-11T21:55:42.13543949Z" level=info msg="response completed" go.version=go1.20.8 http.request.host="registry.k3s.kippel.de:5000" http.request.id=b5764d68-624b-4249-8343-8c743...
Ok. The registry is up. Let’s see if we can talk to it. The registry should be empty, right ?
$ curl -s -X GET http://registry.k3s.kippel.de:5000/v2/_catalog
{"repositories":[]}
Well, it is. We can tick that box.
CAVEAT: Remember the Customizing a k3s Kubernetes Cluster blog ? At the end i mentioned that i had added a number of cluster-external ip addresses to the /etc/dnsmasq.hosts configuration file of my dnsmasq service. ‘registry.k3s.kippel.de’ was one of them. That is the reason i can use a host name in that curl command.
Configuring Kubernetes for the Private Registry
There are 2 configuration actions left that have to be completed before Docker and Kubernetes can access our private registry.
Docker requires a configuration file that instructs it to accept an ‘insecure registry’, i.e. a registry that is exposed via http, not https.
#File: '/etc/docker/daemon.json'
# cat > /etc/docker/daemon.json << EOT
{ "insecure-registries":["registry.k3s.kippel.de:5000","192.168.100.153:5000"] }
EOT
For Kubernetes we use the registry not just for storing our homegrown images but also as a mirror for Internet registries. This is what the ‘mirrors’ clause is for.
#File: '/etc/rancher/k3s/registries.yaml'
# cat > /etc/rancher/k3s/registries.yaml << EOT
mirrors:
"192.168.100.153":
endpoint:
- "http://192.168.100.153:5000"
docker.io:
endpoint:
- "http://registry.k3s.kippel.de:5000"
"registry.k3s.kippel.de":
endpoint:
- "http://registry.k3s.kippel.de:5000"
EOT
Copy an image from docker hub to our private registry
One way to have images available in our registry (apart from building them from scratch) is to download them from an internet registry and then push them to our private registry. This is quite useful for scenarios where we want to customize an existing image (e.g. add a spring boot application to a base image with an installed jdk). Here is how it works :
- pull the image from docker.io into the docker registry
# docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
2c6d21737d83: Pull complete
0bf6824a0232: Pull complete
f47d5fcfb558: Pull complete
ecba2628ac35: Pull complete
1d082d8e4ce1: Pull complete
b2b90333fd43: Pull complete
0ef920b5aa7f: Pull complete
Digest: sha256:10d1f5b58f74683ad34eb29287e07dab1e90f10af243f151bb50aa5dbb4d62ee
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest
- tag the image for our private registry
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 5628e5ea3c17 3 weeks ago 192MB
# docker tag docker.io/nginx:latest registry.k3s.kippel.de:5000/nginx:latest
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 5628e5ea3c17 3 weeks ago 192MB
registry.k3s.kippel.de:5000/nginx latest 5628e5ea3c17 3 weeks ago 192MB
- push the image to our private registry and remove it from the docker registry (saves some disk space)
# docker push registry.k3s.kippel.de:5000/nginx:latest
The push refers to repository [registry.k3s.kippel.de:5000/nginx]
3033c8a8ba59: Pushed
cb1c835f32b9: Pushed
94677b587bab: Pushed
0367492d0972: Pushed
21bb29481bb4: Pushed
66544fa913ad: Pushed
f4e4d9391e13: Pushed
latest: digest: sha256:736342e81e97220f954b8c33846ba80d2d95f59b30225a5c63d063c8b250b0ab size: 1778
# docker rmi nginx:latest
Untagged: nginx:latest
Untagged: nginx@sha256:10d1f5b58f74683ad34eb29287e07dab1e90f10af243f151bb50aa5dbb4d62ee
The ‘curl’ command from above should now show the nginx image.
$ curl -s -X GET http://registry.k3s.kippel.de:5000/v2/_catalog
{"repositories":["nginx"]}
Bless me ! So it does.
Conclusion
Slowly but surely we are getting there. Now that we have Docker and our Private Registry available it is time to roll our own and start building images. I will tackle this in the next Blog.
Stay tuned !