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.