Understand Kubernetes by deploying a custom Docker image using minikube

In the previous blog post we learnt how to create a customer docker image and build container from it.

Image courtesy Minikube.

In this blog post we will use the docker image built previously and deploy it using Kubernetes.
We will do the following-

  1. Push the image to docker hub
  2. Install minikube and Install kubectl
  3. Create deployment file and deploy the pod
  4. Deploy another pod and test connectivity from the pod
  5. Expose the pod using following methods
    a. Using port-forward method
    b. Create NodePort service and expose on port 80

Refer to previous post for steps to create a custom docker image and docker installation steps. My setup is an arm64 ubuntu server virtual machine running docker and minikube. So let’s start!

Push the image to Docker hub

Here I have an image called webpage. This is an nginx based webserver which serves a webpage that we created (refer to this blog post).

clitoapi@jarvis:~/docker_nginx$ sudo docker images
REPOSITORY    TAG       IMAGE ID       CREATED              SIZE
webpage       latest    60b81ca16d3f   About a minute ago   40.7MB
hello-world   latest    b038788ddb22   2 months ago         9.14kB

We can see that a container is running using this image.
clitoapi@jarvis:~/docker_nginx$ sudo docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                                   NAMES
d4290dbcc26f   60b81ca16d3f   "/docker-entrypoint.…"   17 seconds ago   Up 16 seconds   0.0.0.0:8080->80/tcp, :::8080->80/tcp   goofy_visvesvaraya

Create a docker account if you do not have one. Login to docker account.

clitoapi@jarvis:~/docker_nginx$ sudo docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to [https://hub.docker.com](https://hub.docker.com/) to create one.
Username: 
Password:
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded

My docker repository is called clitoapi/aclitoapi_webpage, so I will tag the image accordingly.

Here clitoapi is the namespace created on Docker hub and aclitoapi_webpage is the name of repository. If repository does not exist then push will create a new public repository, tagging will not be required.

clitoapi@jarvis:~/docker_nginx$ sudo docker images
REPOSITORY    TAG       IMAGE ID       CREATED          SIZE
webpage       latest    60b81ca16d3f   15 minutes ago   40.7MB
hello-world   latest    b038788ddb22   2 months ago     9.14kB

clitoapi@jarvis:~/docker_nginx$ sudo docker tag webpage clitoapi/aclitoapi_webpage

Now we are ready to push the image to docker hub.

clitoapi@jarvis:~/docker_nginx$ sudo docker images
REPOSITORY               TAG       IMAGE ID       CREATED          SIZE
clitoapi/aclitoapi_webpage   latest    60b81ca16d3f   16 minutes ago   40.7MB
webpage                  latest    60b81ca16d3f   16 minutes ago   40.7MB
hello-world              latest    b038788ddb22   2 months ago     9.14kB

clitoapi@jarvis:~/docker_nginx$ sudo docker push clitoapi/clitoapi_repo
Using default tag: latest
The push refers to repository [[docker.io/clitoapi/clitoapi_repo](http://docker.io/clitoapi/clitoapi_repo)]
a986c8ee190a: Pushed
e6f9fb9edcd1: Mounted from arm64v8/nginx
e3222cfd0239: Mounted from arm64v8/nginx
d5ad83434521: Mounted from arm64v8/nginx
a06f0fc55ea6: Mounted from arm64v8/nginx
b684d67dbe6f: Mounted from arm64v8/nginx
180c71c90053: Mounted from arm64v8/nginx
9386262d7a74: Mounted from arm64v8/nginx
latest: digest: sha256:771abac75928c16fc77acd27bdf0aae5d72985838417130ad69fcc1c6f11b375 size: 1988

Let’s see if we can pull this image and run container. Let’s stop the container and remove all images. Pull this image and run container.

clitoapi@jarvis:~/docker_nginx$ sudo docker images
REPOSITORY                   TAG       IMAGE ID       CREATED        SIZE
clitoapi/aclitoapi_webpage   latest    60b81ca16d3f   3 hours ago    40.7MB
webpage                      latest    60b81ca16d3f   3 hours ago    40.7MB
hello-world                  latest    b038788ddb22   2 months ago   9.14kB

clitoapi@jarvis:~/docker_nginx$ sudo docker rmi clitoapi/aclitoapi_webpage
Untagged: clitoapi/aclitoapi_webpage:latest
Untagged: clitoapi/aclitoapi_webpage@sha256:771abac75928c16fc77acd27bdf0aae5d72985838417130ad69fcc1c6f11b375

clitoapi@jarvis:~/docker_nginx$ sudo docker images
REPOSITORY    TAG       IMAGE ID       CREATED        SIZE
webpage       latest    60b81ca16d3f   3 hours ago    40.7MB
hello-world   latest    b038788ddb22   2 months ago   9.14kB

clitoapi@jarvis:~/docker_nginx$ sudo docker images
REPOSITORY    TAG       IMAGE ID       CREATED        SIZE
webpage       latest    60b81ca16d3f   3 hours ago    40.7MB
hello-world   latest    b038788ddb22   2 months ago   9.14kB

clitoapi@jarvis:~/docker_nginx$ sudo docker ps -a
CONTAINER ID   IMAGE          COMMAND                  CREATED       STATUS       PORTS                                   NAMES
d4290dbcc26f   60b81ca16d3f   "/docker-entrypoint.…"   3 hours ago   Up 3 hours   0.0.0.0:8080->80/tcp, :::8080->80/tcp   goofy_visvesvaraya

clitoapi@jarvis:~/docker_nginx$ sudo docker stop goofy_visvesvaraya
goofy_visvesvaraya
clitoapi@jarvis:~/docker_nginx$ sudo docker ps -a
CONTAINER ID   IMAGE          COMMAND                  CREATED       STATUS                     PORTS     NAMES
d4290dbcc26f   60b81ca16d3f   "/docker-entrypoint.…"   3 hours ago   Exited (0) 6 seconds ago             goofy_visvesvaraya

clitoapi@jarvis:~/docker_nginx$ sudo docker rm goofy_visvesvaraya
goofy_visvesvaraya

clitoapi@jarvis:~/docker_nginx$ sudo docker ps -a
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
clitoapi@jarvis:~/docker_nginx$ sudo docker images
REPOSITORY    TAG       IMAGE ID       CREATED        SIZE
webpage       latest    60b81ca16d3f   3 hours ago    40.7MB
hello-world   latest    b038788ddb22   2 months ago   9.14kB

clitoapi@jarvis:~/docker_nginx$ sudo docker rmi 60b81ca16d3f
Untagged: webpage:latest
Deleted: sha256:60b81ca16d3fd67b9ae88927b74b8ce6b6984048678b085852021168c62d0831
clitoapi@jarvis:~/docker_nginx$

Pull the image from docker hub.

clitoapi@jarvis:~/docker_nginx$ sudo docker pull clitoapi/aclitoapi_webpage:latest
latest: Pulling from clitoapi/aclitoapi_webpage
edb6bdbacee9: Already exists
4c7f12338fe3: Already exists
002a136ea5c5: Already exists
6d407d2ad632: Already exists
d1543f6e84d3: Already exists
ad428fb17e98: Already exists
bacb1bf71fa0: Already exists
374e6c54001d: Already exists
Digest: sha256:771abac75928c16fc77acd27bdf0aae5d72985838417130ad69fcc1c6f11b375
Status: Downloaded newer image for clitoapi/aclitoapi_webpage:latest
[docker.io/clitoapi/aclitoapi_webpage:latest](http://docker.io/clitoapi/aclitoapi_webpage:latest)
clitoapi@jarvis:~/docker_nginx$

clitoapi@jarvis:~/docker_nginx$ sudo docker images
REPOSITORY                   TAG       IMAGE ID       CREATED        SIZE
clitoapi/aclitoapi_webpage   latest    60b81ca16d3f   3 hours ago    40.7MB
hello-world                  latest    b038788ddb22   2 months ago   9.14kB

We have the image. Lets run the container.

clitoapi@jarvis:~/docker_nginx$ sudo docker run -d -p 8080:80 60b81ca16d3f
a187aee82da210f2761779d3fa09f6c3c4251d8fc363ce5d2b261c5bf6fb53be
clitoapi@jarvis:~/docker_nginx$ sudo docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS        PORTS                                   NAMES
a187aee82da2   60b81ca16d3f   "/docker-entrypoint.…"   2 seconds ago   Up 1 second   0.0.0.0:8080->80/tcp, :::8080->80/tcp   distracted_hopper

2. Install minikube and kubctl

According to Kubernetes documentation “In order for kubectl to find and access a Kubernetes cluster, it needs a kubeconfig file, which is created automatically when you create a cluster using kube-up.sh or successfully deploy a Minikube cluster. By default, kubectl configuration is located at ~/.kube/config.”

First install minikube then install kubectl. You might have to do some troubleshooting during minikube and kubectl installation but don’t give up you will get there. I followed the steps documented here.

Steps to install minikube
https://minikube.sigs.k8s.io/docs/start/
Steps to install kubectl
https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/#install-using-other-package-management

After installation is successful, start minikube.

litoapi@jarvis:~$ minikube version
minikube version: v1.31.1
commit: fd3f3801765d093a485d255043149f92ec0a695f

clitoapi@jarvis:~$ minikube start
😄  minikube v1.31.1 on Ubuntu 22.04 (arm64)
✨  Using the docker driver based on existing profile
👍  Starting control plane node minikube in cluster minikube
🚜  Pulling base image ...
🔄  Restarting existing docker container for "minikube" ...

🧯  Docker is nearly out of disk space, which may cause deployments to fail! (91% of capacity). You can pass '--force' to skip this check.
💡  Suggestion: 

    Try one or more of the following to free up space on the device:
    
    1. Run "docker system prune" to remove unused Docker data (optionally with "-a")
    2. Increase the storage allocated to Docker for Desktop by clicking on:
    Docker icon > Preferences > Resources > Disk Image Size
    3. Run "minikube ssh -- docker system prune" if using the Docker container runtime
🍿  Related issue: https://github.com/kubernetes/minikube/issues/9024

🐳  Preparing Kubernetes v1.27.3 on Docker 24.0.4 ...
🔗  Configuring bridge CNI (Container Networking Interface) ...
🔎  Verifying Kubernetes components...
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
    ▪ Using image docker.io/kubernetesui/dashboard:v2.7.0
    ▪ Using image docker.io/kubernetesui/metrics-scraper:v1.0.8
💡  Some dashboard features require the metrics-server addon. To enable all features please run:

	minikube addons enable metrics-server	


🌟  Enabled addons: default-storageclass, storage-provisioner, dashboard
🏄  Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default

Check kubectl is installed properly.

clitoapi@jarvis:~$ kubectl version
WARNING: This version information is deprecated and will be replaced with the output from kubectl version --short.  Use --output=yaml|json to get the full version.
Client Version: version.Info{Major:"1", Minor:"27", GitVersion:"v1.27.4", GitCommit:"fa3d7990104d7c1f16943a67f11b154b71f6a132", GitTreeState:"clean", BuildDate:"2023-07-19T12:20:54Z", GoVersion:"go1.20.6", Compiler:"gc", Platform:"linux/arm64"}
Kustomize Version: v5.0.1
Server Version: version.Info{Major:"1", Minor:"27", GitVersion:"v1.27.3", GitCommit:"25b4e43193bcda6c7328a6d147b1fb73a33f1598", GitTreeState:"clean", BuildDate:"2023-06-14T09:47:40Z", GoVersion:"go1.20.5", Compiler:"gc", Platform:"linux/arm64"}

Create deployment file and deploy the pod

We want to create a pod that uses image from our docker repository. Let’s do a dry-run to preview the config and then save the output to a yaml file.

clitoapi@jarvis:~/docker_nginx$ kubectl run nginx-webpage --image=clitoapi/aclitoapi_webpage --dry-run -o yaml
W0729 08:14:55.382908   27000 helpers.go:692] --dry-run is deprecated and can be replaced with --dry-run=client.
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: nginx-webpage
  name: nginx-webpage
spec:
  containers:
  - image: clitoapi/aclitoapi_webpage
    name: nginx-webpage
    resources: {}
  dnsPolicy: ClusterFirst
  restartPolicy: Always
status: {}
clitoapi@jarvis:~/docker_nginx$ kubectl run nginx-webpage --image=clitoapi/aclitoapi_webpage --dry-run -o yaml > clitoapiwebpage.yml
W0729 08:15:26.435366   27372 helpers.go:692] --dry-run is deprecated and can be replaced with --dry-run=client.

clitoapi@jarvis:~/docker_nginx$ cat clitoapiwebpage.yml 
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: nginx-webpage
  name: nginx-webpage
spec:
  containers:
  - image: clitoapi/aclitoapi_webpage
    name: nginx-webpage
    resources: {}
  dnsPolicy: ClusterFirst
  restartPolicy: Always
status: {}
clitoapi@jarvis:~/docker_nginx$

Let’s create our pod now.

clitoapi@jarvis:~/docker_nginx$ kubectl apply -f clitoapiwebpage.yml 
pod/nginx-webpage created

clitoapi@jarvis:~/docker_nginx$ kubectl get all -o wide
NAME                READY   STATUS    RESTARTS   AGE     IP            NODE       NOMINATED NODE   READINESS GATES
pod/nginx-webpage   1/1     Running   0          3m29s   10.244.0.11   minikube   <none>           <none>

NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE   SELECTOR
service/kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   13h   <none>

And it is done. The pod is up. Now how do we test it?

Deploy another pod and test connectivity from the pod

Let’s deploy one more pod, this time we will use Ubuntu image and use it to test connectivity to our webserver.

clitoapi@jarvis:~/docker_nginx$ kubectl run ubuntu --image=ubuntu -- sleep 600
pod/ubuntu created

clitoapi@jarvis:~/docker_nginx$ kubectl get all -o wide
NAME                READY   STATUS    RESTARTS   AGE     IP            NODE       NOMINATED NODE   READINESS GATES
pod/nginx-webpage   1/1     Running   0          5m48s   10.244.0.11   minikube   <none>           <none>
pod/ubuntu          1/1     Running   0          15s     10.244.0.12   minikube   <none>           <none>

NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE   SELECTOR
service/kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   13h   <none>

We will have to login to Ubuntu and install curl. We will use these commands.

clitoapi@jarvis:~/docker_nginx$ kubectl exec -it ubuntu /bin/bash
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.

root@ubuntu:/# apt-get update

root@ubuntu:/# apt-get install curl

Yay! our webserver is accessible.

root@ubuntu:/# curl http://10.244.0.11
<html>
<body>
	<h1> "There's some good in this world, Mr. Frodo, and it's worth fighting for."  </h1>
</body>
</html>
root@ubuntu:/# 

Expose the pod and connect to the application from host Ubuntu server

We will try to connect to this application from our host Ubuntu server. We us the following methods,

a. Using port-forward

clitoapi@jarvis:~/docker_nginx$ kubectl get pods
NAME            READY   STATUS    RESTARTS        AGE
nginx-webpage   1/1     Running   0               3h4m
ubuntu          1/1     Running   8 (8m38s ago)   178m

clitoapi@jarvis:~/docker_nginx$ kubectl port-forward nginx-webpage 8888:80
Forwarding from 127.0.0.1:8888 -> 80
Forwarding from [::1]:8888 -> 80
Handling connection for 8888
clitoapi@jarvis:~/docker_nginx$

Now let’s test connectivity using curl. It is successful.

clitoapi@jarvis:~$ curl -v http://localhost:8888
*   Trying 127.0.0.1:8888...
* Connected to localhost (127.0.0.1) port 8888 (#0)
> GET / HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.81.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: nginx/1.24.0
< Date: Sat, 29 Jul 2023 11:21:49 GMT
< Content-Type: text/html
< Content-Length: 118
< Last-Modified: Fri, 28 Jul 2023 14:57:40 GMT
< Connection: keep-alive
< ETag: "64c3d764-76"
< Accept-Ranges: bytes
< 
<html>
<body>
  <h1> "There's some good in this world, Mr. Frodo, and it's worth fighting for."  </h1>
</body>
</html>
* Connection #0 to host localhost left intact


b. Create NodePort service and expose on port 80

clitoapi@jarvis:~/docker_nginx$ kubectl expose pod nginx-webpage --name=nginx-svc --type=NodePort --port=80
service/nginx-svc exposed
clitoapi@jarvis:~/docker_nginx$ kubectl get svc
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP        16h
nginx-svc    NodePort    10.98.39.170   <none>        80:31475/TCP   25s

clitoapi@jarvis:~/docker_nginx$ minikube ip
192.168.49.2
clitoapi@jarvis:~/docker_nginx$ kubectl get svc
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP        16h
nginx-svc    NodePort    10.98.39.170   <none>        80:31475/TCP   5m37s
clitoapi@jarvis:~/docker_nginx$ 

clitoapi@jarvis:~/docker_nginx$ minikube service nginx-svc --url
http://192.168.49.2:31475
clitoapi@jarvis:~/docker_nginx$

Let’s test again. GET request is successful.

clitoapi@jarvis:~$ curl -v http://192.168.49.2:31475
*   Trying 192.168.49.2:31475...
* Connected to 192.168.49.2 (192.168.49.2) port 31475 (#0)
> GET / HTTP/1.1
> Host: 192.168.49.2:31475
> User-Agent: curl/7.81.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: nginx/1.24.0
< Date: Sat, 29 Jul 2023 11:55:14 GMT
< Content-Type: text/html
< Content-Length: 118
< Last-Modified: Fri, 28 Jul 2023 14:57:40 GMT
< Connection: keep-alive
< ETag: "64c3d764-76"
< Accept-Ranges: bytes
< 
<html>
<body>
  <h1> "There's some good in this world, Mr. Frodo, and it's worth fighting for."  </h1>
</body>
</html>
* Connection #0 to host 192.168.49.2 left intact
clitoapi@jarvis:~$