Deploying Your First App on Kubernetes
Ship a container with a Deployment, expose it through a Service, and learn the everyday moves for logs, debugging, and scaling.
Once Kubernetes (K8s) is up and running on your own servers or VPS cluster, deploying an app stops being "SSH in and start a process by hand." Instead, you declare the state you want in a YAML file and let the cluster keep it that way. Here's how to get your first app running end to end.
Describe your app with a Deployment
A Deployment's job is to keep the number of Pods you asked for running at all times. Create deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-web
labels:
app: hello-web
spec:
replicas: 2
selector:
matchLabels:
app: hello-web
template:
metadata:
labels:
app: hello-web
spec:
containers:
- name: hello-web
image: nginx:1.27
ports:
- containerPort: 80
The fields that matter:
- image: the container image. Pin a real version (nginx:1.27) rather than latest, so a restart doesn't quietly change what you're running.
- replicas: how many copies (instances) you want — two here.
- labels / selector.matchLabels: labels are the glue in K8s. The Deployment uses its selector to find the Pods it owns, so the two must match exactly.
- containerPort: the port your process listens on inside the container.
Apply it and check status
kubectl apply -f deployment.yaml
kubectl get deploy hello-web
kubectl get pods -l app=hello-web -o wide
When get deploy shows READY as 2/2, both replicas are up. get pods lists each Pod's status, its node, and its IP.
Expose it with a Service
A Pod's IP changes every time it's recreated, so you never target it directly. A Service gives a group of Pods one stable address. Use ClusterIP (the default) for traffic inside the cluster, or NodePort to reach the app from outside using a node's IP. Create service.yaml:
apiVersion: v1
kind: Service
metadata:
name: hello-web
spec:
type: NodePort
selector:
app: hello-web
ports:
- port: 80
targetPort: 80
nodePort: 30080
The Service's selector matches the same Pods by label. Apply it:
kubectl apply -f service.yaml
kubectl get svc hello-web
Now hit any-node-IP:30080 to reach the app (the NodePort range is usually 30000–32767). If the app only needs to be called by other services inside the cluster, drop type (defaulting to ClusterIP) and reach it by its Service name, hello-web.
Troubleshooting
When something won't start, go to the logs and events first:
kubectl logs -l app=hello-web --tail=100
kubectl describe pod <pod-name>
logs shows the container's standard output. The Events section at the bottom of describe is the most useful part — common failures like ImagePullBackOff (image can't be pulled) and CrashLoopBackOff (the process keeps exiting) spell out their cause right there.
Scaling
When traffic shifts, just change the replica count and the cluster adds or removes Pods for you:
kubectl scale deployment hello-web --replicas=4
kubectl get pods -l app=hello-web
To make it stick, set replicas: 4 back in deployment.yaml and apply once more, keeping the file and the live cluster in sync.
Summary
Deploying on Kubernetes comes down to declaring the desired state and letting the cluster hold it: a Deployment defines the image, replica count, and labels; a Service (ClusterIP for internal, NodePort for external) gives it a stable address; kubectl logs and describe handle debugging; and kubectl scale handles elasticity. Keep these YAML files in version control and your first app has a reproducible, rollback-friendly deployment baseline.