Kubernetes Learning Path: Deploy Your First App
Kubernetes Learning Path: Deploy Your First App
I have been wanting to learn Kubernetes for a while now, and I came across a tool called k3d. It is a great tool that makes it easy to run Kubernetes clusters locally inside Docker containers, making it perfect for learning and development without needing complex infrastructure setup.
In this post, I’ll walk you through deploying your first application to Kubernetes using k3d. We’ll start from scratch and work our way up to a running nginx server.
Quick Setup: Install k3d
First, make sure you have Docker running on your machine. Then install k3d:
1
2
3
4
5
# macOS (using Homebrew)
brew install k3d
# Or using curl
curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash
Verify the installation:
1
k3d version
Create Your First Cluster
A cluster is a group of machines (nodes) working together to run your applications—think of it like a team of servers that can share the workload.
Creating a cluster is simple:
1
2
# Create a cluster named "learning"
k3d cluster create learning
Output:
1
2
3
4
5
6
7
8
9
10
INFO[0000] Prep: Network
INFO[0000] Created network 'k3d-learning'
INFO[0000] Created image volume k3d-learning-images
INFO[0000] Starting new tools node...
INFO[0001] Creating node 'k3d-learning-server-0'
INFO[0001] Pulling image 'ghcr.io/k3d-io/k3d-tools:5.6.0'
INFO[0002] Creating LoadBalancer 'k3d-learning-serverlb'
INFO[0003] Cluster 'learning' created successfully!
INFO[0003] You can now use it like this:
kubectl cluster-info
This takes just a few seconds! Now check that your cluster is running:
1
k3d cluster list
Output:
1
2
NAME SERVERS AGENTS LOADBALANCER
learning 1/1 0/0 true
Check the cluster nodes:
1
kubectl get nodes
Output:
1
2
NAME STATUS ROLES AGE VERSION
k3d-learning-server-0 Ready control-plane,master 30s v1.27.4+k3s1
You should see one node in “Ready” state. Now you’re ready to deploy.
Deploy Your First App
So you’ve got a local Kubernetes cluster running with k3d? Great! Let’s deploy something to it. We’ll use nginx as our first application—it’s simple, reliable, and perfect for understanding how Kubernetes deployments work.
I know that in real life you’ll be deploying much more complex applications than just a static nginx server, but we all have to start somewhere. Once you understand the basics with nginx, the same patterns apply to any containerized application. We’ll work our way up to more complex deployments in future posts.
What You’ll Deploy
We’re going to create two things:
- A Deployment that runs 2 nginx containers
- A Service that makes nginx accessible from your browser
Step 1: Create the Deployment
Create a file called nginx-deployment.yaml
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-demo
labels:
app: nginx # <--- Label for the Deployment itself (for organizing Deployments)
spec:
replicas: 2
selector:
matchLabels:
app: nginx # <--- Deployment manages pods with this label
template:
metadata:
labels:
app: nginx # <--- These labels will be stamped on each pod
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
What does this do?
- Creates 2 identical nginx pods (containers running nginx)
- Uses the lightweight
nginx:alpine
image - Exposes port 80 in each container
Understanding the Three Label Sections:
When I first saw three different places with app: nginx
, I was confused about why we needed so many labels. Here’s what each one does:
metadata.labels
- Labels for the Deployment resource itself (helps you organize Deployments with commands likekubectl get deployments -l app=nginx
)spec.selector.matchLabels
- Tells the Deployment which pods it should managetemplate.metadata.labels
- Labels that get stamped on each pod created by this Deployment
The selector and template labels must match, otherwise the Deployment won’t know which pods to manage. We’ll use these pod labels to connect the Service in Step 2.
Deploy it:
1
2
3
4
kubectl apply -f nginx-deployment.yaml
# Watch your pods start
kubectl get pods -w
First, you’ll see your 2 pods being created:
1
2
3
NAME READY STATUS RESTARTS AGE
nginx-demo-7d4c9d8c9b-4xk2p 0/1 ContainerCreating 0 2s
nginx-demo-7d4c9d8c9b-7m8qz 0/1 ContainerCreating 0 2s
The 0/1
means 0 out of 1 containers are ready yet—they’re still starting up.
Then, after a few seconds, both pods will be running:
1
2
3
NAME READY STATUS RESTARTS AGE
nginx-demo-7d4c9d8c9b-4xk2p 1/1 Running 0 5s
nginx-demo-7d4c9d8c9b-7m8qz 1/1 Running 0 5s
Now 1/1
shows both pods are fully ready and running.
The -w
flag watches for changes in real-time (similar to tail -f
), so press Ctrl+C
to stop watching and return to your command prompt.
Step 2: Expose with a Service
Your pods are running, but you can’t access them yet. So why do we need a Service anyway?
Why Services are Required
Think of it like a hotel receptionist. Guests don’t need to know which room each staff member is in—they just ask at the front desk, and the receptionist routes them to whoever can help.
In Kubernetes:
- Each pod gets its own IP address that changes when it restarts
- We have 2 nginx pods running, and more could be added or removed
- The Service gives you one stable address and automatically routes traffic to healthy pods
- When pods restart or scale up/down, the Service updates its routing automatically
Without a Service, you’d have to track pod IPs manually every time something changes.
Create nginx-service.yaml
:
1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
type: LoadBalancer
selector:
app: nginx # <--- This selector matches pods with label "app: nginx"
ports:
- port: 80
targetPort: 80
How the Service finds your Pods
See the selector: app: nginx
? The Service looks for all pods with the label app: nginx
(the same labels we set in Step 1) and automatically sends traffic to them. When you add more pods with this label, the Service finds them. When you remove pods, the Service stops sending traffic to them. It’s all based on matching labels.
Apply it:
1
2
3
4
kubectl apply -f nginx-service.yaml
# Check the service
kubectl get service nginx-service
You should see:
1
2
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-service LoadBalancer 10.43.123.456 <pending> 80:32000/TCP 5s
The service is now routing traffic to your pods. The EXTERNAL-IP
shows <pending>
in k3d—don’t worry about it, we’ll use port-forwarding to access the app.
Step 3: Access Your Application
Use port-forwarding to access nginx:
1
kubectl port-forward service/nginx-service 8080:80
Open your browser and visit http://localhost:8080
. You should see the nginx welcome page.
1
2
3
4
5
6
7
8
9
Welcome to nginx!
If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.
For online documentation and support please refer to nginx.org.
Commercial support is available at nginx.com.
Thank you for using nginx.
Your application is now running on Kubernetes and accessible from your browser.
Quick Experiments
Scale Your Application
1
2
# Scale to 5 pods
kubectl scale deployment nginx-demo --replicas=5
Output:
1
deployment.apps/nginx-demo scaled
Now watch the new pods appear:
1
kubectl get pods -w
You’ll see 3 additional pods being created:
1
2
3
4
5
6
NAME READY STATUS RESTARTS AGE
nginx-demo-7d4c9d8c9b-4xk2p 1/1 Running 0 5m
nginx-demo-7d4c9d8c9b-7m8qz 1/1 Running 0 5m
nginx-demo-7d4c9d8c9b-9n2kx 0/1 ContainerCreating 0 2s
nginx-demo-7d4c9d8c9b-6p4mw 0/1 ContainerCreating 0 2s
nginx-demo-7d4c9d8c9b-8r5tn 0/1 ContainerCreating 0 2s
Scale back to 2:
1
kubectl scale deployment nginx-demo --replicas=2
Kubernetes will automatically terminate the extra pods.
Check the Logs
1
2
# Get pod name
kubectl get pods
Output:
1
2
3
NAME READY STATUS RESTARTS AGE
nginx-demo-7d4c9d8c9b-4xk2p 1/1 Running 0 10m
nginx-demo-7d4c9d8c9b-7m8qz 1/1 Running 0 10m
Now view logs from one of the pods:
1
kubectl logs nginx-demo-7d4c9d8c9b-4xk2p
Output (nginx access logs):
1
2
3
4
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Configuration complete; ready for start up
127.0.0.1 - - [17/Oct/2025:10:30:15 +0000] "GET / HTTP/1.1" 200 615 "-" "Mozilla/5.0"
These are the nginx startup messages and any HTTP requests. Your logs might be different.
Execute Commands
1
2
# Check nginx version (replace with your actual pod name)
kubectl exec nginx-demo-7d4c9d8c9b-4xk2p -- nginx -v
Output:
1
nginx version: nginx/1.25.3
Open a shell inside the pod:
1
kubectl exec -it nginx-demo-7d4c9d8c9b-4xk2p -- /bin/sh
You’ll get an interactive shell prompt:
1
2
3
4
5
/ # pwd
/
/ # ls
bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
/ # exit
You can run any commands inside the container. Type exit
to leave the shell.
Clean Up
When you’re done experimenting, it’s good practice to clean up the resources. This also helps you practice the delete commands:
1
2
kubectl delete -f nginx-service.yaml
kubectl delete -f nginx-deployment.yaml
Output:
1
2
service "nginx-service" deleted
deployment.apps "nginx-demo" deleted
Verify everything is gone:
1
kubectl get all
Output:
1
2
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 30m
Your nginx resources are gone! The only thing remaining is the default kubernetes
service, which is a system service that’s always present.
What You Learned
✅ How to create a Kubernetes Deployment
✅ How to expose an app with a Service
✅ How to scale applications up and down
✅ How to inspect pods (check logs and execute commands)
You’ve successfully deployed your first application to Kubernetes. The same pattern works for any containerized application—just swap the nginx image for your own.
What’s Next?
In Part 2 of this series, we’ll explore ConfigMaps and Secrets to manage configuration and sensitive data in your Kubernetes applications. You’ll learn how to:
- Store configuration separately from your code
- Manage environment variables
- Handle sensitive information securely
Stay tuned!
Series Navigation
Part 1: Deploy Your First App ← You just finished this!
Part 2: ConfigMaps and Secrets
Part 3: Understanding Namespaces (Coming soon)
Part 4: Persistent Storage (Coming soon)
Part 5: Ingress and Load Balancing (Coming soon)
Found a mistake or have questions? Feel free to open an issue here.