Deploy Your First App on Kubernetes

Deploy, expose, and scale a containerised app on a local Kubernetes cluster.

Introduction

30 Second Summary

Every time you open a streaming app, send a message, or check your bank balance, the software behind the scenes runs on a system that keeps it available no matter how many people use it at once. That system is Kubernetes.

In this project, you will deploy a web application to a local Kubernetes cluster, expose it to your browser, and scale it to handle more traffic. You will finish with a working multi-replica deployment that you manage entirely from your terminal.

What You'll Build

You will set up a local Kubernetes cluster, deploy a containerised application to it, and scale the application across multiple replicas while observing load balancing in action.

By the end of this project, you'll have:

  • A local Kubernetes cluster running via Minikube.
  • A scaled Deployment running multiple replicas of an application, exposed to your browser through a Service.
  • A declarative YAML manifest describing your deployment as infrastructure-as-code.
  • Secret Mission: Add a second microservice and route traffic to both using an Ingress resource.

Are there any prerequisites?

You only need basic familiarity with the command line (opening a terminal and typing commands). No prior Kubernetes or Docker experience is required.

Install Docker Desktop and Minikube

To deploy an application to Kubernetes, you need a few tools running on your machine.

In this step, you will install Docker Desktop (which runs containers), Minikube (which creates a local Kubernetes cluster), and kubectl (which lets you talk to that cluster from your terminal).

In this step, get ready to:
  • Install Docker Desktop on your Mac.
  • Install Minikube and kubectl using Homebrew.
  • Start a local Kubernetes cluster.
Install Docker Desktop

Docker Desktop provides the container runtime that Minikube uses under the hood to run your cluster.

  • Open your browser and go to the Docker Desktop download page:
  • https://www.docker.com/get-started/
  • Click Download for Mac – Apple Silicon if your Mac has an M-series chip, or Download for Mac – Intel Chip if it has an Intel processor.
  • Open the downloaded .dmg file.
  • Drag the Docker icon into the Applications folder.
  • Press Cmd+Space to open Spotlight search.
  • Type Docker and press Enter to launch Docker Desktop.
  • If prompted, grant Docker Desktop the permissions it requests to run.
  • Wait for the Docker whale icon to appear in your menu bar at the top of the screen. This means Docker is running.

How do I know which chip I have?

Click the Apple menu in the top-left corner of your screen and select About This Mac. If you see "Apple M1", "M2", "M3", or similar, you have Apple Silicon. If you see "Intel", choose the Intel download.

Now let's confirm Docker is working. Open Terminal by pressing Cmd+Space, typing Terminal, and pressing Enter.

  • Verify Docker is installed by running this command:
docker --version

✔️ I see a version number

You should see output like Docker version 27.x.x, build .... Docker is installed and running. You are ready to move on.

ⓧ Command not found

This means Docker Desktop is not running or did not install correctly.

  • Check your menu bar for the Docker whale icon. If you do not see it, open Docker Desktop from your Applications folder.
  • Wait about 30 seconds for the whale icon to stop animating. This means Docker has finished starting.
  • Try running docker --version again.

If it still does not work, help me troubleshoot Docker Desktop not being recognized in my terminal.

Install Minikube and kubectl

Minikube creates a lightweight Kubernetes cluster on your machine. The kubectl command-line tool is how you interact with that cluster.

  • Install Minikube using Homebrew by running this command:
brew install minikube

What does this command do?

Homebrew downloads and installs the minikube binary onto your Mac. It also installs kubectl as a dependency, so you get both tools with one command.

  • Once the install finishes, verify Minikube is installed by running:
minikube version
  • Verify kubectl is installed by running:
kubectl version --client

✔️ I see version numbers for both

You should see a Minikube version number and a kubectl client version number. Both tools are ready to go.

ⓧ Command not found

This usually means Homebrew did not add the tools to your PATH.

  • Close your Terminal window and open a new one. Some installs require a fresh terminal session.
  • Run minikube version and kubectl version --client again.
  • If Homebrew itself is not recognized, make sure it is installed from https://brew.sh.

If things still are not working, help me troubleshoot Minikube and kubectl installation on macOS with Homebrew.

Start your cluster

With Docker running and Minikube installed, you can now create your local Kubernetes cluster.

  • Start a local single-node Kubernetes cluster by running this command:
minikube start

What does this command do?

Minikube creates a virtual machine (using Docker as the driver) and installs a single-node Kubernetes cluster inside it. This gives you a fully functional Kubernetes environment on your own laptop.

The first time you run this, it downloads the Kubernetes components. This may take a minute or two depending on your internet speed.

  • Once minikube start finishes, verify your cluster node is running by running:
kubectl get nodes
  • You should see one node named minikube with a status of Ready.

✔️ I see minikube with Ready status

Your cluster is up and running. You have a fully working local Kubernetes environment.

ⓧ I see an error or the node is NotReady

The most common cause is Docker Desktop not running.

  • Check that the Docker whale icon is in your menu bar and not animating.
  • If Docker was not running, start it, wait for it to finish loading, then run minikube start again.
  • If the node shows NotReady, wait 30 seconds and run kubectl get nodes again. Kubernetes sometimes needs a moment to finish starting.

If you are still stuck, help me troubleshoot minikube start failing on macOS.

What is a node?

A node is a machine (physical or virtual) that runs your containerized applications. In production, a Kubernetes cluster has many nodes. Your Minikube cluster has just one node, which is perfect for learning.

Your local Kubernetes cluster is live. Next up, you will deploy your first application to it and see it running inside a Pod.

Deploy Your First Application

Your cluster is running and ready for its first workload. Now it is time to put something on it.

In this step, you will deploy an application to your Kubernetes cluster, inspect what happens under the hood, and check the logs to confirm everything started correctly.

In this step, get ready to:
  • Create a Deployment running the echo-server container image.
  • Inspect what Kubernetes did behind the scenes.
  • View application logs to confirm the app is running.
Create a Deployment

A Deployment is a Kubernetes resource that tells the cluster which container image to run and how many copies to keep alive. Kubernetes then handles scheduling the container onto a node, pulling the image, and restarting it if anything goes wrong.

  • Create your Deployment by running this command:
kubectl create deployment echo-server --image=kicbase/echo-server:1.0

What does this command do?

This command creates a Deployment named echo-server that runs the kicbase/echo-server:1.0 container image.

Kubernetes will create a ReplicaSet to manage the desired number of copies (1 by default), and the ReplicaSet creates a Pod that actually runs the container.

  • Verify your Pod is running by running this command:
kubectl get pods

You should see one Pod with a name starting with echo-server- and a STATUS of Running.

Pod stuck in ContainerCreating or ImagePullBackOff?

  • Make sure Docker Desktop is running. Kubernetes needs Docker to pull container images.
  • Check your internet connection. The cluster needs to download the image the first time.
  • Wait 30 seconds and run kubectl get pods again. The first image pull can take a moment.

Help me troubleshoot my Pod that is not entering Running status.

Inspect your Deployment

Your application is running, but what actually happened when you created the Deployment? Kubernetes performed several steps behind the scenes to get your container up and running.

  • View the full details of your Deployment by running this command:
kubectl describe deployment echo-server

What does this output show?

The output shows the Deployment's configuration and current state. Look for these key sections:

  • Replicas tells you how many copies are desired, updated, and available.
  • Pod Template shows the container image and ports configured for each Pod.
  • Events at the bottom lists the actions Kubernetes took, like scaling up the ReplicaSet.
  • View the cluster-wide event log by running this command:
kubectl get events

What are events?

Events are Kubernetes' activity log. They record every significant action the cluster takes.

You should see entries showing that Kubernetes created a ReplicaSet, scheduled the Pod onto your node, pulled the container image, and started the container. This is the chain of events that happens every time you deploy an application.

View application logs

The final way to confirm your application is healthy is to check what it printed to the console when it started. Container logs are the first place to look when debugging issues in production.

  • View the logs from your running container by running this command:
kubectl logs deploy/echo-server

What does this command do?

The deploy/echo-server syntax tells kubectl to fetch logs from the first Pod belonging to the echo-server Deployment. This saves you from having to copy the full Pod name.

You should see output confirming the echo server started and is listening for requests.

Seeing 'no logs' or an empty output?

  • Some lightweight containers produce minimal startup output. If you see no errors, that is a good sign.
  • Make sure the Pod is in Running status by running kubectl get pods again.

Help me understand why my kubectl logs command shows no output.

Your application is deployed, running, and confirmed healthy. Next up, you will expose it to your browser so you can actually see it in action.

Expose Your App with a Service

Your echo-server application is running inside the cluster, but right now there is no way to reach it from your browser.

By default, Kubernetes keeps Pods isolated on an internal network. To make your app accessible, you need to create a Service that routes external traffic to your Pod.

In this step, get ready to:
  • Create a NodePort Service to expose your application on a port.
  • Access the running application in your browser.
  • Inspect the Service networking to understand how traffic flows.
Create a NodePort Service

A NodePort Service exposes your application on a specific port of the cluster node. This makes it reachable from outside the cluster's internal network.

  • Expose your echo-server deployment by running this command:
kubectl expose deployment echo-server --type=NodePort --port=8080

What does this command do?

  • kubectl expose deployment echo-server tells Kubernetes to create a new Service for the echo-server deployment.
  • --type=NodePort makes the Service accessible from outside the cluster by assigning a port on the node itself.
  • --port=8080 sets the port that the Service listens on, matching the port the echo-server container uses internally.
  • You should see a confirmation message that the service was exposed.

Seeing an error about the service already existing?

If you already ran this command before, delete the existing service first with kubectl delete service echo-server and then try again.

Make sure the deployment name matches exactly. Run kubectl get deployments to verify the name is echo-server.

Help me fix this kubectl expose error.

Access the app in your browser

On macOS with the Docker driver, containers run in an isolated network. The minikube service command creates a tunnel from your host machine to the Service and opens it in your default browser.

  • Open the echo-server application in your browser by running:
minikube service echo-server

What does this command do?

This command looks up the echo-server Service, finds its NodePort, and creates a tunnel so your browser can reach the application inside the cluster. It automatically opens the URL in your default browser.

Because you are using the Docker driver on macOS, the terminal window needs to stay open to keep the tunnel running.

  • Your browser should open automatically with a page showing request metadata from the echo server (headers, path, host information).
  • Return to your terminal. Press Ctrl+C to close the tunnel once you have confirmed the page loads.

Browser did not open?

If nothing opens automatically, look in the terminal output for a URL (it looks like http://127.0.0.1:<port>). Copy and paste that URL into your browser manually.

Make sure the Service was created successfully by running kubectl get services and confirming echo-server appears in the list.

Help me access my minikube service in the browser.

Understand the networking

Now that your app is accessible, take a look at how Kubernetes routes traffic from your browser to the Pod.

  • View your cluster's Services by running:
kubectl get services

What do these columns mean?

The output shows a table with your echo-server Service and the default kubernetes Service.

  • CLUSTER-IP is the internal IP address that other Pods use to reach the Service inside the cluster.
  • PORT(S) shows the mapping in the format 8080:<nodePort>/TCP. The first number (8080) is the Service port. The second number is the NodePort that Kubernetes assigned automatically.
  • Traffic flows like this: your browser hits the NodePort on the cluster node, which forwards to the Service on port 8080, which then routes to the Pod's container.

Your application is now exposed and accessible from your browser through a Kubernetes Service. Next up, you will scale the application to multiple replicas and watch Kubernetes distribute traffic across them.

Scale Your Application

Your application is running and accessible in the browser, but it is only backed by a single Pod. If that one Pod crashes or gets overloaded, your app goes down.

In this step, you will scale your Deployment to multiple replicas and watch Kubernetes automatically distribute traffic and heal failures. This is the core value of container orchestration.

In this step, get ready to:
  • Scale your Deployment to 3 replicas.
  • Observe load balancing across Pods.
  • Test Kubernetes self-healing by deleting a Pod.
Scale to 3 replicas

Right now your Deployment has a single replica. That means one Pod handles all traffic. Scaling up adds more Pods behind the same Service, spreading the load across them.

  • Scale your echo-server Deployment to 3 replicas by running this command:
kubectl scale deployment echo-server --replicas=3
  • Watch the new Pods come online by running:
kubectl get pods -w

What do these commands do?

  • kubectl scale deployment echo-server --replicas=3 tells Kubernetes to update the desired replica count for your Deployment to 3. Kubernetes then creates 2 additional Pods to match that target.
  • kubectl get pods -w watches your Pods in real time. You will see new Pods transition from Pending to ContainerCreating to Running.
  • Wait until all 3 Pods show Running in the STATUS column, then press Ctrl+C to stop watching.
  • Confirm all 3 replicas are ready by running:
kubectl get pods

You should see 3 Pods listed, each with a unique name like echo-server-xxxxx-yyyyy, all showing Running status.

Pods stuck in Pending or ContainerCreating?

  • Make sure Docker Desktop is still running. Minikube uses Docker to run containers.
  • If a Pod is stuck, run kubectl describe pod <pod-name> to see why. The Events section will show the exact issue.
  • help me debug why my Pods are stuck and not reaching Running state
Test load balancing

Your Service is still pointing to the same Deployment, but now there are 3 Pods behind it. Kubernetes distributes incoming requests across all healthy Pods automatically.

  • Open the echo-server app in your browser by running:
minikube service echo-server
  • Refresh the browser page several times.
  • Look at the response each time. The Pod name shown in the output changes between refreshes, proving that the Service is distributing traffic across your 3 replicas.

How does Kubernetes load balance?

The NodePort Service you created in the previous step uses the label selector app: echo-server to find matching Pods. When you scaled from 1 to 3 replicas, all 3 Pods have that same label. The Service automatically includes them in its endpoint list and round-robins traffic across them.

Seeing the same Pod name every time?

  • Your browser might be caching the page. Try a hard refresh with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows).
  • Alternatively, open an incognito window or use curl in your terminal with the URL shown by the minikube service command. Run it several times to see different Pod names in the response.
  • help me verify that Kubernetes is distributing traffic across my 3 replicas
Understand self-healing

Kubernetes does not just run your Pods. It actively monitors them and recreates any that disappear. You told it to maintain 3 replicas, and it will fight to keep that promise.

  • List your Pods to pick one to delete by running:
kubectl get pods
  • Copy the full name of any one of the 3 Pods (e.g., echo-server-xxxxx-yyyyy).
  • Delete that Pod by running:
kubectl delete pod [[PODNAME="your-pod-name-here"]]
  • Immediately check the Pod list again by running:
kubectl get pods

You should see that the Pod you deleted is gone, but a brand new Pod has already been created in its place. The total count is still 3.

What is self-healing?

When you created a Deployment with 3 replicas, you declared your desired state. The Deployment controller constantly compares the actual number of running Pods to the desired count. When you deleted a Pod, the actual count dropped to 2. Kubernetes detected the mismatch and immediately scheduled a replacement Pod to bring the count back to 3.

This is what makes Kubernetes reliable in production. If a node fails or a container crashes, the system automatically recovers without human intervention.

New Pod not appearing?

  • Make sure you deleted a Pod, not the Deployment itself. Run kubectl get deployments to confirm your echo-server Deployment still exists.
  • The replacement Pod might still be starting up. Run kubectl get pods again after a few seconds.
  • help me understand why Kubernetes is not recreating my deleted Pod

Your application is now running with 3 replicas, load balanced and self-healing. Next up, you will write a declarative YAML manifest that captures this entire setup as code.

Write a Declarative YAML Manifest

Your application is running, scaling, and self-healing. Everything you have built so far was created using imperative commands. You typed individual kubectl commands to create each resource one at a time.

Production teams do not manage Kubernetes this way. Instead, they write YAML manifest files that describe the desired state of their infrastructure. This approach is called Infrastructure as Code. In this step, you will export your running resources into a clean YAML file and redeploy them declaratively.

In this step, get ready to:
  • Export your current Deployment to YAML and create a manifest file.
  • Delete the imperative resources and redeploy with the manifest.
  • Verify the declarative deployment works identically to the imperative one.
Export your deployment to YAML

Before writing a manifest from scratch, it helps to see what Kubernetes stores internally for your running Deployment. The -o yaml flag exports any resource as a full YAML document.

  • View the full YAML representation of your running Deployment by running this command:
kubectl get deployment echo-server -o yaml

What does this output show?

You will see a large YAML document with many system-generated fields like creationTimestamp, uid, resourceVersion, and a full status section.

You do not need any of these in your manifest. Kubernetes adds them automatically when it creates the resource. Your manifest only needs the fields that describe what you want, not the current runtime state.

Now you will create a clean manifest file containing just the essential fields for both your Deployment and Service.

  • Create the deployment.yaml file by running this command:
cat > deployment.yaml << 'EOF'
# Deployment: runs 3 replicas of the echo-server container
apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo-server
  labels:
    app: echo-server
spec:
  replicas: 3
  selector:
    matchLabels:
      app: echo-server
  template:
    metadata:
      labels:
        app: echo-server
    spec:
      containers:
        - name: echo-server
          image: kicbase/echo-server:1.0
          ports:
            - containerPort: 8080
---
# Service: exposes the Deployment outside the cluster
apiVersion: v1
kind: Service
metadata:
  name: echo-server
spec:
  type: NodePort
  selector:
    app: echo-server
  ports:
    - port: 8080
      targetPort: 8080
EOF

What does this manifest do?

This single file defines two resources separated by ---. The Deployment tells Kubernetes to run 3 replicas of the kicbase/echo-server:1.0 image, each listening on port 8080.

The Service creates a NodePort that routes external traffic to any Pod matching the app: echo-server label. Together, these two resources reproduce everything you built with imperative commands.

  • Confirm the file was created by running:
ls deployment.yaml

You should see deployment.yaml listed in the output.

Don't see the file?

Make sure you copied the entire cat command including the EOF at the end. The heredoc syntax requires the closing EOF to appear on its own line with no leading spaces.

If you see a > prompt in your terminal, the heredoc did not close. Press Ctrl+C and try pasting the full command again.

help me create the deployment.yaml file

✔️ Awesome, I've got everything!

Great. Make sure you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete deployment.yaml file for reference:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo-server
  labels:
    app: echo-server
spec:
  replicas: 3
  selector:
    matchLabels:
      app: echo-server
  template:
    metadata:
      labels:
        app: echo-server
    spec:
      containers:
        - name: echo-server
          image: kicbase/echo-server:1.0
          ports:
            - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: echo-server
spec:
  type: NodePort
  selector:
    app: echo-server
  ports:
    - port: 8080
      targetPort: 8080
Apply the manifest

To prove the manifest works on its own, you will delete the existing resources and recreate them entirely from the file. This is how production teams deploy. They check a YAML file into version control and apply it to any cluster.

  • Delete the existing Deployment and Service by running these commands:
kubectl delete deployment echo-server
kubectl delete service echo-server

Why delete first?

Deleting the imperative resources lets you prove that the YAML file alone is enough to recreate everything. In a real workflow, you would start with the manifest from the beginning and never use imperative commands for production resources.

  • You should see confirmation messages like deployment.apps "echo-server" deleted and service "echo-server" deleted.
  • Recreate everything from your manifest by running:
kubectl apply -f deployment.yaml

What does kubectl apply do?

The kubectl apply command reads the YAML file, sends each resource definition to the Kubernetes API, and creates (or updates) the resources to match the desired state described in the file.

This is the declarative workflow. You tell Kubernetes what you want, and it figures out how to get there.

  • You should see output confirming both resources were created:
deployment.apps/echo-server created
service/echo-server created

Seeing a validation error?

If you see an error about invalid YAML, check your deployment.yaml file for indentation issues. YAML is whitespace-sensitive. Every nested level must use exactly 2 spaces, not tabs.

You can view the file contents with cat deployment.yaml and compare against the reference in the double-check tab above.

help me fix my deployment.yaml YAML validation error

Verify everything works
  • Check that all 3 Pods are running by running:
kubectl get pods
  • You should see 3 Pods with the status Running. It may take a few seconds for all Pods to reach this state.
  • Confirm the Service is active by running:
kubectl get services
  • You should see the echo-server Service listed with type NodePort and port 8080.
  • Open the application in your browser by running:
minikube service echo-server

You should see the echo server responding with request metadata in your browser, exactly as before. Your application is now fully managed by a YAML manifest file.

Browser not opening?

If minikube service does not open a browser window, look for a URL in the terminal output and open it manually.

If the Service is not found, wait a few seconds and run kubectl get services to confirm the Service exists.

help me access my echo-server Service in the browser

Why declarative manifests matter

You now have a single file that fully describes your application's infrastructure. You can check it into Git, share it with teammates, apply it to a different cluster, or use it in a CI/CD pipeline.

If anything goes wrong, you can always redeploy the exact same state by running kubectl apply -f deployment.yaml again. This repeatability is what makes Kubernetes powerful at scale.

Secret mission

Add a Second Microservice with Ingress Routing

Your cluster currently runs a single application. In production, most systems run multiple services that need to share a single entry point. Can you deploy a second microservice and use a Kubernetes Ingress to route traffic to both based on the URL path?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

This project runs entirely on your local machine, so there are no ongoing cloud costs. However, the Minikube cluster uses approximately 2GB of RAM while running.

Resources you used:

  • A local Minikube cluster (uses ~2GB RAM while running).
  • Docker Desktop (running in the background).
  • Project files: deployment.yaml and ingress.yaml.

Keep everything running

No action needed. Choose this if you are still actively experimenting with your cluster or want to try more Kubernetes commands.

  • Your Minikube cluster stays up and uses ~2GB RAM.
  • Docker Desktop continues running in your menu bar.
  • The minikube tunnel process remains active in its terminal window.

Pause - I'll come back to this later

Freeze your cluster without losing any state. This reduces CPU usage to near zero while keeping all your resources intact for when you return.

  • Switch to the terminal window running minikube tunnel and press Ctrl+C to stop it.
  • Pause your cluster by running:
minikube pause
  • When you are ready to resume, run minikube unpause and your cluster picks up exactly where you left off.

Delete - I don't want to use this again

Remove all project resources completely and free up your machine's memory and disk space.

  • Switch to the terminal window running minikube tunnel and press Ctrl+C to stop it.
  • Delete the Minikube cluster and all its resources by running:
minikube delete --all
  • Delete your project files by running:
rm deployment.yaml ingress.yaml
  • Quit Docker Desktop by clicking the Docker icon in your menu bar and selecting Quit Docker Desktop.
  • To fully uninstall Docker Desktop, open the app, click the Troubleshoot icon (bug icon in the top-right), then select Uninstall.
  • To uninstall the Minikube and kubectl CLI tools, run:
brew uninstall minikube
brew uninstall kubectl

Nice Work!

Nice Work!

You just deployed your first application to Kubernetes. You now have a running, scaled, load-balanced web app on a local cluster that you built and managed yourself.

You've learned how to:

  • Deploy a containerised application to a local Kubernetes cluster using Minikube and kubectl.
  • Scale an application to multiple replicas and observe self-healing when a Pod is deleted.
  • Write a declarative YAML manifest to manage your infrastructure as code.
  • Added Ingress-based routing to direct traffic to multiple microservices by URL path.

Ready to quiz yourself?