DevOps

How to Deploy Your First App to Kubernetes Without the Headache

Tech Setup5 min read
TS

Tech Setup

Published October 10, 2026 · Editorial policy

How to Deploy Your First App to Kubernetes Without the Headache

You have a Docker container that runs smoothly on your laptop, but moving it to Kubernetes feels like translating a working script into ancient Greek. If you are a backend developer who just wants to ship a simple API without spending three days configuring YAML files, RBAC policies, and ingress controllers, the learning curve is brutally steep. By the end of this guide, you will have a real web application running inside a local Kubernetes cluster, exposed to your browser, using a single manifest file that you actually understand.

The 3-Minute TL;DR

If you already have Docker Desktop installed and just want the fastest path to a running pod, run these four commands in your terminal:

kubectl create deployment web-app --image=nginx:1.25-alpine
kubectl expose deployment web-app --type=NodePort --port=80
kubectl port-forward svc/web-app 8080:80

Open http://localhost:8080 in your browser. You will see the default Nginx welcome page.

You just deployed an application to Kubernetes. The rest of this guide explains how to replace those imperative CLI commands with a repeatable declarative manifest and why things actually work under the hood.

Setting Up Your Local Sandbox

Do not practice Kubernetes on a production cloud cluster. You need a local environment that mimics a real cluster without the AWS bill.

Choosing Your Local Cluster

You have two main options on macOS, Linux, and Windows: Minikube and Docker Desktop.

If you use Docker Desktop daily, enable its built-in Kubernetes cluster. Click the gear icon in Docker Desktop, go to Kubernetes, check Enable Kubernetes, and click Apply & restart. This installs a single-node cluster and configures your kubectl context automatically.

If you prefer a lightweight, dedicated VM approach, install Minikube:

# macOS via Homebrew
brew install minikube kubectl

# Start the cluster
minikube start --driver=docker

Verify your cluster is healthy and kubectl can talk to it:

kubectl cluster-info

Expected output:

Kubernetes control plane is running at https://kubernetes.docker.internal:6443
CoreDNS is running at https://0.0.0.0:10253

If kubectl returns an error about connection refused, your context is pointing to the wrong cluster. Fix it with kubectl config use-context docker-desktop (or minikube).

Writing Your First Production-Grade Manifest

Command-line flags are great for quick tests, but real infrastructure uses declarative YAML files stored in Git. Instead of relying on kubectl create deployment, let's write a manifest that defines both a Deployment and a Service in a single file.

Create a file named app-manifest.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-service
  labels:
    app: backend-api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: backend-api
  template:
    metadata:
      labels:
        app: backend-api
    spec:
      containers:
      - name: web
        image: hashicorp/http-echo:latest
        args:
          - "-text=Hello from Kubernetes!"
        ports:
        - containerPort: 5678
        resources:
          limits:
            cpu: "100m"
            memory: "128Mi"
          requests:
            cpu: "50m"
            memory: "64Mi"
---
apiVersion: v1
kind: Service
metadata:
  name: api-service-internal
spec:
  type: ClusterIP
  selector:
    app: backend-api
  ports:
  - port: 80
    targetPort: 5678

Deconstructing the YAML

Every Kubernetes manifest requires four root fields: apiVersion, kind, metadata, and spec.

  • apiVersion: Specifies the API group and version. Deployments live in apps/v1, while core resources like Services and Pods live in v1.
  • replicas: 2: Tells Kubernetes to maintain exactly two identical instances of your container at all times. If one crashes, Kubernetes spins up a replacement immediately.
  • selector and matchLabels: This is the glue of Kubernetes. The Deployment uses matchLabels to track which pods it owns. If you change these labels manually, the deployment loses track of its pods.
  • resources (Limits and Requests): Never deploy an app without these. requests tells the scheduler how much CPU/Memory to reserve on a node. limits hard-caps resource consumption so a memory leak won't crash the entire worker node.
  • kind: Service: Pods are ephemeral; their IP addresses change every time they restart. A Service provides a stable internal IP and DNS name, load-balancing traffic across all pods matching the selector.

Deploying and Inspecting Your App

Apply your manifest to the cluster using kubectl apply:

kubectl apply -f app-manifest.yaml

Expected output:

deployment.apps/api-service created
service/api-service-internal created

Now, watch your pods spin up in real-time:

kubectl get pods -l app=backend-api -w

You will see the status transition from Pending to ContainerCreating to Running. Press Ctrl+C to exit the watch view, then check the full status of your deployment:

kubectl get deployment api-service

Expected output:

NAME          READY   UP-TO-DATE   AVAILABLE   AGE
api-service   2/2     2            2           45s

Testing Your Internal Service

Because we defined the Service type as ClusterIP, it is only reachable from inside the cluster. To test it from your local machine, use kubectl port-forward to bridge your laptop to the cluster service:

kubectl port-forward svc/api-service-internal 8080:80

Open a separate terminal window and send an HTTP request using curl:

curl http://localhost:8080

Expected output:

Hello from Kubernetes!

Switch back to your port-forwarding terminal, and you will see the access logs stream by. This proves your load-balanced service is successfully routing traffic to your replicated pods.

If This Doesn't Work: Common Failure Points

Kubernetes error messages are notoriously cryptic. When your app fails to start, check these three common culprits first.

1. ImagePullBackOff or ErrImagePull

Your cluster cannot download the container image.

  • The Cause: You either misspelled the image tag, or you are trying to pull a private image from Docker Hub, GitHub Packages, or AWS ECR without authentication.
  • The Fix: Run kubectl describe pod <pod-name> to see the exact error. If it is a private registry issue, you need to create an image pull secret and attach it to your pod spec via imagePullSecrets.

2. CrashLoopBackOff

Your container starts, but crashes immediately, and Kubernetes keeps restarting it in an infinite loop.

  • The Cause: Your application threw an unhandled exception at startup, crashed due to missing environment variables, or your container entrypoint script exited with a non-zero status.
  • The Fix: Inspect the logs of the crashing container using previous-run flags:
    kubectl logs <pod-name> --previous
    
    Fix the code or missing configuration in your manifest, then run kubectl apply -f app-manifest.yaml to update the deployment.

3. Pending Pods

Your pod status stays stuck at Pending indefinitely.

  • The Cause: Your cluster nodes do not have enough free CPU or memory to satisfy the resources.requests you specified in your YAML file.
  • The Fix: Run kubectl describe node to inspect cluster capacity, or lower the resource requests in your manifest file for local development.

Cleaning Up Your Cluster

When you are done testing, you do not need to delete pods one by one. Because you used declarative manifests, you can tear down every resource defined in that file with a single command:

kubectl delete -f app-manifest.yaml

Expected output:

deployment.apps "api-service" deleted
service "api-service-internal" deleted

Moving Beyond Local: Alternatives and Prevention

Running kubectl apply manually from your laptop is fine for learning, but it breaks down the moment you start working with a team. In a production environment, you should avoid manual cluster modifications entirely.

Instead, adopt one of these workflows as you graduate from local testing:

  • Helm Charts: Package your manifests into parameterized templates so you can deploy your app across staging and production environments with values files.
  • GitOps (ArgoCD or Flux): Store your Kubernetes manifests in a Git repository. An agent inside your cluster automatically syncs your live infrastructure to match what is checked into Git, eliminating drift and manual kubectl usage entirely.