I Deployed My First App Into Kubernetes
Deploying real applications into Kubernetes, Pods, Services, ReplicaSets, Deployments, none of it actually cares where the cluster lives.
So I ran the whole thing locally with minikube instead of provisioning anything.
A few small things still caught me off guard along the way.
If you’re following along and want to do the same thing, here’s what I actually ran:
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube
minikube start --driver=docker
kubectl get nodesThat last command should show you one node with a status of Ready. If it does, you've got a working cluster and you're ready to actually deploy something into it.
Starting With the Smallest Thing Kubernetes Runs
Everything in Kubernetes starts with a Pod. It wraps one or more containers and gives them their own little network space. I started with the simplest one I could write, a single nginx container.
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
labels:
app: nginx-pod
spec:
containers:
- image: nginx:latest
name: nginx-pod
ports:
- containerPort: 80
protocol: TCPkubectl apply -f nginx-pod.yaml
kubectl get podsOnce that shows Running, nginx is alive inside the cluster. To actually prove it works, I spun up a temporary Pod with curl installed and hit the nginx Pod's internal IP directly.
kubectl run curl --image=dareyregistry/curl -i --tty --rm
curl -v <nginx-pod-ip>:80That returned the default nginx welcome page. Small win. Also a trap. That Pod’s IP only works as long as that exact Pod is alive.
The moment it restarts, it gets a new IP, and anything depending on the old one breaks. Kubernetes has an entire object built specifically to solve this.
Services, and the Missing Label
A Service gives you a stable address that sits in front of whichever Pods match a label you choose.
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector:
app: nginx-pod
ports:
- protocol: TCP
port: 80
targetPort: 80kubectl apply -f nginx-service.yaml
kubectl port-forward svc/nginx-service 8089:80That second command just sat there. Timed out. error: timed out waiting for the condition.
Turns out the Service was looking for a Pod labeled app: nginx-pod. My Pod manifest never actually had that label. The Service existed, it just had nobody to talk to.
One line fixed it: adding the label back to the Pod and reapplying. localhost:8089 finally showed nginx through the Service instead of straight through the Pod.
That one missing label taught me more about how Services route traffic than getting it right the first time ever would have.
From there I changed the Service type to NodePort, which exposes it on a fixed port on the node itself.
spec:
type: NodePort
selector:
app: nginx-pod
ports:
- protocol: TCP
port: 80
targetPort: 80
nodePort: 30080No real node IP to reach locally, so I used minikube’s own shortcut instead.
minikube service nginx-service --urlThat gave me a URL I could open straight in the browser.
Pods That Replace Themselves
A standalone Pod doesn’t come back if it dies. A ReplicaSet does, it keeps a fixed number of identical Pods running no matter what happens to any one of them.
Get Lydiah’s stories in your inbox
Join Medium for free to get updates from this writer.
For the selector, I used matchLabels, the structured format the current API expects.
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: nginx-rs
spec:
replicas: 3
selector:
matchLabels:
app: nginx-pod
template:
metadata:
labels:
app: nginx-pod
spec:
containers:
- image: nginx:latest
name: nginx-pod
ports:
- containerPort: 80
protocol: TCPOnce that applied cleanly, I deleted one of the three Pods on purpose, just to watch what happened.
kubectl delete po <one-of-the-pod-names>
kubectl get podsA replacement appeared within seconds. Count back to three. That’s the entire value of a ReplicaSet in one command.
Scaling worked the same way, either straight from the command line:
kubectl scale rs nginx-rs --replicas=5or by changing replicas in the file and reapplying. That second way matters more than it sounds. Scale by command without updating the file, and the file falls out of sync with reality. Reapply it later, and it undoes your change without warning you.
Testing a LoadBalancer Locally
My course material treats the LoadBalancer Service type as something to come back to later, since it normally depends on a real cloud provider handing out an external address. I didn’t want to wait, so I tried faking it locally instead, with a tool called minikube tunnel.
spec:
type: LoadBalancer
selector:
tier: frontend
ports:
- protocol: TCP
port: 80
targetPort: 80kubectl apply -f nginx-service.yaml
minikube tunnelFirst attempt, I ran it with sudo, since it needs elevated permissions to touch network routes. Bad call. It told me my cluster's profile didn't exist at all. Turns out sudo switches your home directory to root's, and minikube went looking for the cluster there instead of where it actually lives. Dropped the sudo, let it ask for my password internally instead. Fixed.
Once it ran clean, kubectl get service showed an actual EXTERNAL-IP instead of <pending>, and the app loaded in the browser exactly the way it was supposed to.
Deployments, and a Service That Didn’t Notice the Swap
A Deployment manages a ReplicaSet the same way a ReplicaSet manages Pods. The interesting part wasn’t the Deployment itself, it was what happened to the Service already sitting in front of it.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
tier: frontend
spec:
replicas: 3
selector:
matchLabels:
tier: frontend
template:
metadata:
labels:
tier: frontend
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80I deleted the ReplicaSet the LoadBalancer Service had been pointing at, applied this Deployment instead, and refreshed the browser without touching the Service at all. It just kept working, instantly routing to the new Pods. The Service never cared what was managing the Pods behind it, only that the labels matched.
kubectl scale deployment nginx-deployment --replicas=15
kubectl exec -it <pod-name> -- bashExec’ing into a running container drops you straight into a shell inside it, same as docker exec. I poked around /etc/nginx/conf.d/default.conf just to see nginx's actual config sitting inside a container I'd deployed myself.
The Part That Actually Mattered
Everything up to this point built toward one demonstration, the one I’d tell anyone learning Kubernetes to actually do themselves instead of just reading about it.
I scaled the Deployment down to a single Pod, exec’d into it, and overwrote its default page by hand.
kubectl scale deployment nginx-deployment --replicas=1
kubectl exec -it <pod-name> -- bash
apt-get update
apt-get install vim -yWrote new content into /usr/share/nginx/html/index.html, confirmed it in the browser. Worked exactly as expected.
Then I deleted that Pod on purpose.
kubectl delete pod <pod-name>Kubernetes immediately created a replacement. Same image, same labels, same everything on paper. I refreshed the browser. My edit was gone. Back to the plain default page, like I’d never touched it.
That’s not a bug. A Deployment keeps a certain number of Pods running, nothing more. It has no concept of anything you changed by hand inside one. Whatever you write into a running container dies with that container. Data that needs to survive a Pod’s lifetime needs a Volume, next on my list.
What Actually Stuck
The missing label, the sudo mix up, none of it was planned. It’s just what happens when you actually try this yourself instead of only reading the material. Every one of those small mistakes taught me more than the version that worked on the first try ever would have.
This project was completed as part of my Cloud and DevOps apprenticeship with SteghHub, steghub.com.










