Goal
In this lab, we will follow a very common DevOps workflow:
Developer code → Docker Image → Docker Registry → Kubernetes Deployment → Kubernetes Service → Browser
By the end of this lab, you should understand:
- how to create a small application
- how to create a Docker image
- how to test the container
- how to push the Docker image to Docker Hub
- how Kubernetes downloads the image
- how Kubernetes creates Pods
- how a Service exposes the application
- how a user opens the application in a browser
Architecture
Application files
↓
Dockerfile
↓
docker build
↓
Docker Image
↓
Docker Hub
↓
Kubernetes Deployment
↓
Pod
↓
Service
↓
Browser
Part 1 — Create the project
Do this on your local computer or EC2 instance where Docker and kubectl are installed.
Create a project directory:
mkdir docker-k8s-lab
cd docker-k8s-lab
Check where you are:
pwd
You should now be inside:
docker-k8s-lab
Part 2 — Create a simple website
Create a file:
nano index.html
Add:
<!DOCTYPE html>
<html>
<head>
<title>Docker Kubernetes Lab</title>
</head>
<body>
<h1>Hello from Docker and Kubernetes!</h1>
<p>My application is running inside Kubernetes.</p>
</body>
</html>
Save the file.
If you use nano:
CTRL + O
Enter
CTRL + X
Check the file:
cat index.html
What did we do?
We created the application.
For this lab, our application is only one HTML page.
In a real company, this could be:
React application
Java application
Python application
Node.js application
.NET application
As DevOps engineers, we usually do not write the business application itself.
Developers create the application.
Our responsibility is to package it, deploy it, automate it, monitor it, and keep it running.
Part 3 — Create the Dockerfile
Create:
nano Dockerfile
Add:
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/index.html
Save it.
Check:
cat Dockerfile
Explanation
FROM nginx:alpine
This means:
Use the Nginx image as the base image.
Nginx will work as our web server.
Then:
COPY index.html /usr/share/nginx/html/index.html
This copies our HTML page inside the Docker image.
The image will contain:
Linux
Nginx
Our index.html
This is what Docker packaging means.
Part 4 — Build the Docker image
Run:
docker build -t docker-k8s-lab:v1 .
Check the image:
docker images
You should see something similar to:
REPOSITORY TAG
docker-k8s-lab v1
What happened?
Docker read the:
Dockerfile
and created a Docker image.
Think of the image as a package or template.
We have not deployed the application yet.
We only created the package.
Part 5 — Test the Docker image locally
Before sending our image to Kubernetes, we should test it.
Run:
docker run -d -p 8080:80 --name docker-k8s-test docker-k8s-lab:v1
Check:
docker ps
Open:
http://localhost:8080
If you are using an EC2 instance, use:
http://PUBLIC-IP:8080
Make sure port 8080 is allowed in the Security Group if using EC2.
You should see:
Hello from Docker and Kubernetes!
What does this mean?
-p 8080:80
means:
Computer port 8080
↓
Container port 80
Nginx listens on port:
80
Part 6 — Stop the test container
We only used this container for testing.
Run:
docker stop docker-k8s-test
Remove it:
docker rm docker-k8s-test
Check:
docker ps
Part 7 — Push the image to Docker Hub
Kubernetes must be able to download the Docker image.
Your local image:
docker-k8s-lab:v1
exists only on your machine.
Kubernetes usually downloads images from a registry such as:
Docker Hub
AWS ECR
Azure ACR
Google Artifact Registry
For this lab we will use Docker Hub.
Part 8 — Login to Docker Hub
Run:
docker login
Enter your Docker Hub username and password/token.
Part 9 — Tag the image
Replace:
YOUR_DOCKERHUB_USERNAME
with your real username.
Run:
docker tag docker-k8s-lab:v1 YOUR_DOCKERHUB_USERNAME/docker-k8s-lab:v1
Example:
docker tag docker-k8s-lab:v1 aisalkyn/docker-k8s-lab:v1
Check:
docker images
Now you should see both names.
Part 10 — Push the image
Run:
docker push YOUR_DOCKERHUB_USERNAME/docker-k8s-lab:v1
Example:
docker push aisalkyn/docker-k8s-lab:v1
Now the image is stored in Docker Hub.
DevOps concept
This is an important production idea.
Developer code
↓
Docker build
↓
Container Registry
↓
Kubernetes
In AWS, instead of Docker Hub, we usually use:
Amazon ECR
Part 11 — Check Kubernetes
Before deployment, make sure Kubernetes works.
Run:
kubectl get nodes
You should see:
NAME STATUS
node-1 Ready
The important word is:
Ready
Also check:
kubectl cluster-info
Part 12 — Create deployment.yaml
Now we tell Kubernetes:
"Run my Docker image."
Create:
nano deployment.yaml
Add:
apiVersion: apps/v1
kind: Deployment
metadata:
name: docker-k8s-app
spec:
replicas: 2
selector:
matchLabels:
app: docker-k8s-app
template:
metadata:
labels:
app: docker-k8s-app
spec:
containers:
- name: docker-k8s-app
image: YOUR_DOCKERHUB_USERNAME/docker-k8s-lab:v1
ports:
- containerPort: 80
Replace:
YOUR_DOCKERHUB_USERNAME
with your username.
Example:
image: aisalkyn/docker-k8s-lab:v1
Part 13 — Understand deployment.yaml
The first part:
apiVersion: apps/v1
kind: Deployment
means:
Create a Kubernetes Deployment.
The name:
metadata:
name: docker-k8s-app
means our Deployment will be called:
docker-k8s-app
Then:
replicas: 2
means:
Run two copies of the application.
So Kubernetes will create approximately:
Pod 1
Pod 2
Both run the same Docker image.
Then:
image: YOUR_DOCKERHUB_USERNAME/docker-k8s-lab:v1
means:
Kubernetes will go to Docker Hub and download this image.
Then:
containerPort: 80
means:
The application inside the container listens on port 80.
Part 14 — Deploy to Kubernetes
Run:
kubectl apply -f deployment.yaml
Expected:
deployment.apps/docker-k8s-app created
Check:
kubectl get deployments
Then:
kubectl get pods
You should see something similar to:
docker-k8s-app-xxxx 1/1 Running
docker-k8s-app-yyyy 1/1 Running
What does 1/1 mean?
1/1
means:
1 container expected
1 container ready
The Pod is ready.
Part 15 — Check more details
Run:
kubectl get pods -o wide
This shows additional information:
Pod IP
Node
Status
You can also run:
kubectl describe pod POD-NAME
Example:
kubectl describe pod docker-k8s-app-xxxxx
Part 16 — Look at the logs
Run:
kubectl logs POD-NAME
Example:
kubectl logs docker-k8s-app-xxxxx
For Nginx, logs may appear after someone accesses the website.
Logs are very important for DevOps engineers because we use them when troubleshooting applications.
Part 17 — Why can't we open the Pod directly?
We now have:
Pod 1
Pod 2
But Pods are temporary.
Their IP addresses can change.
For example:
Pod dies
↓
Kubernetes creates a new Pod
↓
New Pod gets a different IP
Because of this, users should not connect directly to Pods.
We need a:
Service
Part 18 — Create service.yaml
Create:
nano service.yaml
Add:
apiVersion: v1
kind: Service
metadata:
name: docker-k8s-service
spec:
selector:
app: docker-k8s-app
ports:
- port: 80
targetPort: 80
type: LoadBalancer
Save it.
Part 19 — Understand the Service
This line:
selector:
app: docker-k8s-app
means:
Send traffic to Pods that have this label:
app: docker-k8s-app
Remember our Deployment also has:
labels:
app: docker-k8s-app
This is how the Service finds the Pods.
Part 20 — Understand the ports
port: 80
targetPort: 80
Think:
User
↓
Service port 80
↓
Pod port 80
↓
Nginx
Part 21 — Create the Service
Run:
kubectl apply -f service.yaml
Check:
kubectl get services
or:
kubectl get svc
You may see:
NAME TYPE EXTERNAL-IP
docker-k8s-service LoadBalancer ...
Part 22 — Open the application
If you are using a cloud Kubernetes cluster such as EKS, AKS, or GKE:
Run:
kubectl get svc
Find:
EXTERNAL-IP
It may look like:
abc123.us-east-1.elb.amazonaws.com
Open it in your browser:
http://EXTERNAL-IP
Now you should see:
Hello from Docker and Kubernetes!
Congratulations.
Your application flow is now:
Browser
↓
LoadBalancer
↓
Kubernetes Service
↓
Pod
↓
Container
↓
Nginx
↓
index.html
If you are using Minikube
Instead of LoadBalancer, run:
minikube service docker-k8s-service
Or:
minikube service docker-k8s-service --url
Then open the URL.
If you are using kind
LoadBalancer normally does not automatically give you a public IP.
For a simple lab, use port-forward:
kubectl port-forward service/docker-k8s-service 8080:80
Then open:
http://localhost:8080
Keep that terminal open while testing.
Part 23 — Watch how traffic reaches different Pods
Check Pods:
kubectl get pods
You should have two Pods.
The Service can send requests to either Pod.
Architecture:
┌── Pod 1
Browser → Service ┤
└── Pod 2
This is one reason Kubernetes is useful.
Part 24 — Scale the application
Change:
replicas: 2
to:
replicas: 4
Then run:
kubectl apply -f deployment.yaml
Check:
kubectl get pods
You should now have four Pods.
You can also scale using a command:
kubectl scale deployment docker-k8s-app --replicas=4
Check:
kubectl get pods
What did Kubernetes do?
You told Kubernetes:
I want 4 application instances.
Kubernetes created enough Pods to reach the desired state.
Part 25 — Test self-healing
Check Pods:
kubectl get pods
Choose one Pod.
Delete it:
kubectl delete pod POD-NAME
Immediately run:
kubectl get pods
You should see Kubernetes creating a new Pod.
Why?
Because Deployment says:
replicas: 4
Kubernetes constantly compares:
Desired state = 4 Pods
Actual state = 3 Pods
So Kubernetes creates another Pod.
This is called:
self-healing
Part 26 — Update the application
Change the website:
nano index.html
Change:
<h1>Hello from Docker and Kubernetes!</h1>
to:
<h1>Version 2 is running in Kubernetes!</h1>
Now build a new image:
docker build -t docker-k8s-lab:v2 .
Tag it:
docker tag docker-k8s-lab:v2 YOUR_DOCKERHUB_USERNAME/docker-k8s-lab:v2
Push it:
docker push YOUR_DOCKERHUB_USERNAME/docker-k8s-lab:v2
Part 27 — Update Kubernetes
Open:
nano deployment.yaml
Change:
image: YOUR_DOCKERHUB_USERNAME/docker-k8s-lab:v1
to:
image: YOUR_DOCKERHUB_USERNAME/docker-k8s-lab:v2
Apply:
kubectl apply -f deployment.yaml
Watch the rollout:
kubectl rollout status deployment/docker-k8s-app
Check Pods:
kubectl get pods
Refresh the browser.
You should now see:
Version 2 is running in Kubernetes!
This is a basic Kubernetes rolling deployment.
Part 28 — See rollout history
Run:
kubectl rollout history deployment/docker-k8s-app
This shows deployment revisions.
If the new version has a problem, DevOps engineers may need to roll back.
Part 29 — Roll back
Run:
kubectl rollout undo deployment/docker-k8s-app
Then:
kubectl rollout status deployment/docker-k8s-app
Refresh your browser.
The previous version should return.
Part 30 — Troubleshooting commands every DevOps engineer should know
Check Pods:
kubectl get pods
Check Deployment:
kubectl get deployment
Check Service:
kubectl get svc
See more information:
kubectl get pods -o wide
Describe Pod:
kubectl describe pod POD-NAME
View logs:
kubectl logs POD-NAME
View Deployment:
kubectl describe deployment docker-k8s-app
View Service:
kubectl describe service docker-k8s-service
Watch Pods:
kubectl get pods -w
Common problems
ImagePullBackOff
If you see:
ImagePullBackOff
check:
kubectl describe pod POD-NAME
Possible reasons:
Wrong Docker image name
Wrong tag
Image was not pushed
Private Docker Hub repository
Authentication problem
CrashLoopBackOff
If you see:
CrashLoopBackOff
check:
kubectl logs POD-NAME
This usually means the application starts and crashes repeatedly.
Pending
If Pod status is:
Pending
run:
kubectl describe pod POD-NAME
Possible reasons:
Not enough CPU
Not enough memory
Scheduling problem
No available node
Browser does not open
Check:
kubectl get svc
Then:
kubectl describe svc docker-k8s-service
Also check the Pods:
kubectl get pods
They should be:
Running
and:
1/1
Clean up
When you finish the lab:
kubectl delete -f service.yaml
Then:
kubectl delete -f deployment.yaml
Check:
kubectl get pods
kubectl get svc
Final DevOps workflow
Remember this flow:
1. Developer writes application
2. DevOps creates/builds Docker image
3. DevOps tests Docker image
4. Image is pushed to registry
5. Kubernetes Deployment pulls image
6. Deployment creates Pods
7. Service finds Pods using labels
8. Service exposes the application
9. User opens application in browser
10. DevOps monitors, troubleshoots, scales, updates, and rolls back
The most important architecture to remember is:
Developer
↓
GitHub
↓
CI/CD
↓
Docker Image
↓
Registry
↓
Kubernetes Deployment
↓
Pods
↓
Service
↓
Load Balancer / Ingress
↓
Browser
Top comments (0)