← Back to Blog

Kubernetes for Beginners: Deploy Your First App (Step-by-Step)

Kubernetes feels overwhelming until you understand its four fundamental building blocks. Once you grasp what a Pod, Deployment, Service, and Ingress actually do - and why they are separate abstractions - the rest of Kubernetes starts to make sense. This guide skips the theory and goes straight to what you need to ship software.

Why Kubernetes Exists

When you run a container in production, several questions immediately arise: what happens when it crashes? How do you run five copies of it for load balancing? How do you update it without downtime? How do external users reach it? How do you manage secrets across dozens of services?

Kubernetes answers all of these questions through a consistent declarative API: you describe the desired state of your system in YAML, apply it to the cluster, and Kubernetes continuously works to make reality match that description. A crashed container gets restarted automatically. A node dies and its workloads are rescheduled elsewhere. You update a deployment and Kubernetes rolls it out gradually, ready to roll back if something goes wrong.

The Mental Model: Everything Is an Object

In Kubernetes, every resource is an object described in YAML with four top-level fields: apiVersion, kind, metadata, and spec. You apply objects to a cluster, and the control plane reconciles them. That's the whole model. Everything else is just knowing what kinds of objects exist and what their spec fields mean.

apiVersion: apps/v1    # API group and version
kind: Deployment       # The type of object
metadata:
  name: my-api         # Unique name in this namespace
  namespace: production
spec:                  # The desired state

Pods: The Smallest Deployable Unit

A Pod is the smallest thing Kubernetes can schedule. It contains one or more containers that share the same network namespace (same IP, same ports) and can share volumes. In practice, most Pods contain a single container. Sidecar patterns (logging agents, proxies) use multi-container Pods.

apiVersion: v1
kind: Pod
metadata:
  name: my-api-pod
spec:
  containers:
  - name: api
    image: myorg/api:v1.2.3
    ports:
    - containerPort: 8080
    resources:
      requests:
        memory: "128Mi"
        cpu: "250m"
      limits:
        memory: "256Mi"
        cpu: "500m"

You almost never create Pods directly. They are managed by higher-level controllers like Deployments. If you create a bare Pod and it crashes, it stays crashed - there is no restart logic at the Pod level. That is the Deployment's job.

Deployments: Managed, Replicated Pods

A Deployment manages a set of identical Pods. You tell it what container to run, how many replicas you want, and what update strategy to use. Kubernetes handles the rest: scheduling Pods across nodes, restarting crashed Pods, and rolling out new versions.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-api
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-api
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1        # Allow 1 extra Pod during update
      maxUnavailable: 0  # Never take a Pod down before a new one is ready
  template:
    metadata:
      labels:
        app: my-api
    spec:
      containers:
      - name: api
        image: myorg/api:v1.2.3
        ports:
        - containerPort: 8080
        env:
        - name: NODE_ENV
          value: production
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5

The selector.matchLabels and template.metadata.labels must match. This is how the Deployment knows which Pods it owns. Always use livenessProbe (restart on failure) and readinessProbe (only send traffic when ready). Without readiness probes, a rolling update will send traffic to Pods that are still starting up.

Services: Stable Networking for Pods

Pods are ephemeral. Their IPs change every time they restart or get rescheduled. A Service provides a stable IP and DNS name that always routes to the correct set of Pods (selected by label). There are four Service types:

  • ClusterIP - Internal only. Accessible within the cluster. The default.
  • NodePort - Exposes the Service on each node's IP at a static port. Reachable from outside the cluster but awkward for production.
  • LoadBalancer - Provisions a cloud load balancer (AWS ALB, GCP LB, etc.). The standard way to expose production services.
  • ExternalName - Maps a Service to a DNS name. Used to abstract external dependencies.
apiVersion: v1
kind: Service
metadata:
  name: my-api-svc
  namespace: production
spec:
  type: ClusterIP         # Internal only
  selector:
    app: my-api           # Routes to Pods with this label
  ports:
  - port: 80             # Service port (what callers use)
    targetPort: 8080     # Container port (what Pods listen on)
    protocol: TCP

Once this Service exists, any Pod in the cluster can reach your API at http://my-api-svc (within the same namespace) or http://my-api-svc.production.svc.cluster.local (across namespaces).

Ingress: HTTP Routing Into the Cluster

An Ingress routes external HTTP/HTTPS traffic to internal Services based on host name or URL path. It requires an Ingress controller (Nginx Ingress, AWS ALB Ingress Controller, Traefik) to be installed in the cluster.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-api-ingress
  namespace: production
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - api.myapp.com
    secretName: api-tls-cert
  rules:
  - host: api.myapp.com
    http:
      paths:
      - path: /v1
        pathType: Prefix
        backend:
          service:
            name: my-api-svc
            port:
              number: 80
      - path: /admin
        pathType: Prefix
        backend:
          service:
            name: admin-svc
            port:
              number: 80

Scan Your Site for Free

Our Exposure Checker runs 19 parallel security checks - SSL, headers, exposed paths, DNS, open ports, and more.

Run Free Security Scan

Namespaces: Logical Isolation

Namespaces divide a single cluster into virtual sub-clusters. They scope resource names and are used for environment separation (production, staging), team separation, or access control. Resource quotas and RBAC policies apply per namespace.

# Create a namespace
kubectl create namespace staging

# Apply a manifest to a specific namespace
kubectl apply -f deployment.yaml -n staging

# Set a default namespace for your session
kubectl config set-context --current --namespace=production

# List all namespaces
kubectl get namespaces

Essential kubectl Commands

You will use these every day:

# Apply or update resources from YAML
kubectl apply -f deployment.yaml

# Get resources
kubectl get pods                    # List pods in current namespace
kubectl get pods -n production      # List pods in specific namespace
kubectl get pods -A                 # All namespaces
kubectl get pods -o wide            # Include node, IP info
kubectl get all                     # Pods, services, deployments

# Describe a resource (events, conditions, full config)
kubectl describe pod my-api-pod-xxx
kubectl describe deployment my-api

# Logs
kubectl logs my-api-pod-xxx
kubectl logs my-api-pod-xxx -f        # Follow (tail)
kubectl logs my-api-pod-xxx --previous # Logs from crashed previous container

# Execute a command in a running container
kubectl exec -it my-api-pod-xxx -- sh
kubectl exec -it my-api-pod-xxx -- curl localhost:8080/health

# Scale a deployment
kubectl scale deployment my-api --replicas=5

# Rolling restart (picks up new ConfigMap/Secret values)
kubectl rollout restart deployment/my-api

# Check rollout status
kubectl rollout status deployment/my-api

# Rollback to previous version
kubectl rollout undo deployment/my-api

# Delete resources
kubectl delete pod my-api-pod-xxx
kubectl delete -f deployment.yaml

Resource Requests and Limits: Why They Matter

Every container should declare CPU and memory requests (minimum guaranteed) and limits (maximum allowed). Without requests, the scheduler cannot place your Pod intelligently and nodes get overloaded. Without limits, a runaway process can starve other containers.

resources:
  requests:
    memory: "128Mi"    # Kubernetes guarantees this much
    cpu: "100m"        # 100 millicores = 0.1 CPU cores
  limits:
    memory: "256Mi"    # Container is OOM-killed if it exceeds this
    cpu: "500m"        # CPU is throttled (not killed) at this cap

Frequently Asked Questions

What is the difference between a Pod and a container?

A container is a Linux process in an isolated namespace, managed by a container runtime (containerd, Docker). A Pod is a Kubernetes abstraction that wraps one or more containers, giving them a shared network identity (IP address) and optionally shared storage. Kubernetes schedules and manages Pods, not individual containers.

Why does Kubernetes use labels and selectors instead of names?

Names are unique per namespace - you can only have one resource with a given name. Labels are arbitrary key-value pairs that can be shared across many resources. A Service selects all Pods with app: my-api regardless of how many there are or what their names are. This decouples the Service from specific Pod instances, which is essential for dynamic scaling.

What happens when a Pod crashes?

If the Pod is managed by a Deployment, Kubernetes restarts it automatically. The restart count is tracked and visible in kubectl get pods. If a Pod restarts too quickly too many times, Kubernetes applies exponential backoff (CrashLoopBackOff state) to avoid a restart storm. The Deployment's desired replica count is always maintained.

How do I expose my application to the internet?

The standard production approach is: Deployment → ClusterIP Service → Ingress. The Deployment runs your containers, the Service gives them a stable internal address, and the Ingress routes external traffic to the Service based on host/path rules. The Ingress controller handles TLS termination and load balancing.

What is a ReplicaSet and how is it different from a Deployment?

A ReplicaSet ensures that a specified number of identical Pods are running at all times. A Deployment manages ReplicaSets and adds rolling update and rollback capabilities on top. When you update a Deployment's container image, it creates a new ReplicaSet and gradually shifts traffic from the old one to the new one. You almost never interact with ReplicaSets directly.

Next Steps

With Pods, Deployments, Services, and Ingress understood, the next topics to learn are ConfigMaps and Secrets for managing configuration, Horizontal Pod Autoscaling for automatic scaling, PersistentVolumeClaims for storage, and RBAC for access control.

Use our free YAML Validator to catch syntax errors in your Kubernetes manifests before applying them to your cluster. Explore our 70+ free developer tools for more DevOps utilities.

Use our free tool here → YAML Validator

UK
Written by Usman Khan
DevOps Engineer | MSc Cybersecurity | CEH | AWS Solutions Architect

Usman has 10+ years of experience securing enterprise infrastructure, managing high-traffic servers, and building zero-knowledge security tools. Read more about the author.