Containerize a Python API with Kubernetes

Build a Flask API, containerize it with Docker, and deploy it to Kubernetes.

Introduction

30 Second Summary

Think about how apps like Netflix or Spotify handle millions of users without crashing. They run many copies of the same application and automatically replace any copy that stops working.

In this project, you will build a Python API, package it into a container with Docker, and deploy it to a local Kubernetes cluster. You will scale your app to multiple copies and watch Kubernetes automatically recover from failures.

What You'll Build

A Task Tracker REST API built with Flask, containerized with Docker, and orchestrated with Kubernetes on your local machine.

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

  • A working Flask API packaged into a Docker image that runs anywhere Docker does.
  • A local Kubernetes cluster running multiple replicas of your app behind a Service.
  • Hands-on experience scaling your app up and down and observing Kubernetes self-healing when a container is deleted.
  • Secret Mission: Add health check endpoints and liveness probes for production-ready resilience.

Are there any prerequisites?

You should have basic familiarity with the command line and Python syntax. No prior Docker or Kubernetes experience is needed.

Set Up Your Project and Verify Tools

Before writing any application code, you need to confirm that your tools are working. Docker will package your app into a portable container, and Kubernetes will manage multiple copies of that container in a cluster.

In this step, you'll verify that Docker and Kubernetes are both operational on your machine. Then you'll prepare a Python environment with Flask installed so you're ready to build your API in the next step.

In this step, get ready to:
  • Verify Docker Desktop is running.
  • Enable Kubernetes in Docker Desktop.
  • Set up a Python virtual environment with Flask installed.
Verify Docker Desktop is running

Docker Desktop bundles the Docker engine, CLI tools, and a built-in Kubernetes cluster into one application. Let's confirm it's running properly.

  • Open your terminal.
  • Navigate to your task-tracker project folder by running this command:
cd ~/Desktop/task-tracker
  • Verify Docker is installed and the daemon is running by running these commands:
docker --version
docker info

You should see a version number (like Docker version 27.x.x) followed by detailed server information including the number of containers and images.

What do these commands do?

docker --version confirms the Docker CLI is installed. docker info goes further by checking that the Docker daemon (the background process that manages containers) is actually running and responsive.

Seeing "Cannot connect to the Docker daemon"?

This means Docker Desktop is not running. Press Cmd+Space (macOS) or the Windows key (Windows), type Docker Desktop, and press Enter to launch it. Wait for the whale icon in your menu bar or system tray to stop animating, then try the commands again.

help me troubleshoot Docker not connecting

Enable Kubernetes in Docker Desktop

Docker Desktop includes a single-node Kubernetes cluster you can enable with one click. This gives you a local cluster to deploy to without installing any extra tools.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Docker Desktop and press Enter to open it.
  • Click the gear icon in the top-right corner to open Settings.
  • Select Kubernetes in the left sidebar.
  • Check the box labelled Enable Kubernetes.
  • Click Apply & Restart.

What is Kubernetes?

Kubernetes is a container orchestration platform. It takes your container and runs multiple copies of it, distributes traffic between them, and automatically restarts any that crash.

Docker Desktop's built-in cluster runs everything on your laptop, so you can learn Kubernetes without paying for cloud infrastructure.

Docker Desktop will download Kubernetes components and restart. This takes 2-3 minutes the first time.

  • Once Docker Desktop shows a green "Kubernetes running" indicator, verify the cluster is ready by running these commands in your terminal:
kubectl cluster-info
kubectl get nodes

You should see Kubernetes control plane running at a local address, and a node named docker-desktop with a status of Ready.

What do these commands do?

kubectl cluster-info confirms the Kubernetes API server is reachable. kubectl get nodes lists the machines in your cluster. On Docker Desktop, you'll see a single node called docker-desktop.

Seeing "connection refused" or no nodes?

Kubernetes may still be starting up. Wait until the Docker Desktop dashboard shows a green Kubernetes status icon in the bottom-left corner, then try the commands again.

If it stays stuck on "Kubernetes is starting" for more than 5 minutes, try clicking Reset Kubernetes Cluster in the Kubernetes settings panel.

help me get Kubernetes running in Docker Desktop

Create a Python virtual environment and install Flask

A virtual environment isolates your project's Python packages from the rest of your system. This ensures your Flask API uses the exact dependency versions you specify.

  • Make sure you're inside your task-tracker folder, then create a virtual environment by running this command:

macOS

python3 -m venv venv

Windows

python -m venv venv
  • Activate the virtual environment by running:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate

You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.

  • Install Flask by running:
pip install flask==3.1.3

You should see pip download and install Flask along with its dependencies, ending with Successfully installed.

Seeing "pip: command not found"?

Make sure you activated the virtual environment first. If (venv) is not showing in your prompt, re-run the activation command above.

On macOS, you can also try pip3 instead of pip.

help me fix pip not being found

  • Now create the requirements.txt file that records your project's dependencies.
  • Open your code editor and create a new file called requirements.txt in the task-tracker folder.
  • Add the following content:
flask==3.1.3

Why write this file manually?

requirements.txt lists only your direct dependencies with pinned versions. Docker will use this file to install Flask inside your container later.

Writing it by hand (instead of using pip freeze) keeps the file clean. pip freeze would dump every transitive sub-dependency, which adds noise you don't need.

  • Save requirements.txt.
  • Confirm the file exists by running:
ls

You should see requirements.txt and a venv folder listed in the output.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here's what your requirements.txt should look like:

flask==3.1.3

Your environment is fully set up. Docker is running, Kubernetes is ready, and Flask is installed. Next up, you'll build the Task Tracker API itself.

Build the Flask API

Your environment is set up and ready to go. Now it's time to write the actual application that you'll eventually containerize and deploy to Kubernetes.

In this step, you'll build a simple Task Tracker REST API using Flask. The API will support creating and listing tasks. You'll test it locally first to confirm everything works before packaging it into a Docker container.

In this step, get ready to:
  • Create a Flask application with three API endpoints.
  • Run the API locally and test each endpoint with curl.
  • Stop the local server in preparation for Docker.
Create the Flask application

Flask uses decorators like @app.route() to map URL paths to Python functions. Each function handles a specific request and returns a response.

  • Create a new file called app.py in your task-tracker folder.
  • Add the app setup and read-only endpoints by pasting this code:
from flask import Flask, jsonify, request

app = Flask(__name__)

# In-memory list to store tasks
tasks = []


@app.route("/")
def home():
    # Welcome endpoint to confirm the API is running
    return jsonify({"message": "Task Tracker API is running"})


@app.route("/tasks", methods=["GET"])
def get_tasks():
    # Return all tasks as a JSON list
    return jsonify({"tasks": tasks})

What does this code do?

Flask(__name__) creates your Flask application instance. The tasks list stores tasks in memory, meaning each time the app restarts, the list resets to empty.

The / route is a simple health check that confirms the API is alive. The /tasks GET route returns whatever tasks currently exist in the list.

You won't see a change yet. The next chunk adds the endpoint for creating tasks and the code that starts the server.

  • Add the POST endpoint and server entry point below the code you just pasted in app.py:

@app.route("/tasks", methods=["POST"])
def create_task():
    # Parse JSON body from the request
    data = request.get_json()
    # Validate that a title was provided
    if not data or "title" not in data:
        return jsonify({"error": "title is required"}), 400
    # Build the new task with an auto-incrementing ID
    task = {"id": len(tasks) + 1, "title": data["title"], "done": False}
    tasks.append(task)
    return jsonify(task), 201


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

What does this code do?

The POST /tasks endpoint reads JSON from the request body. If the title field is missing, it returns a 400 error. Otherwise, it creates a new task dictionary and appends it to the list.

The if __name__ == "__main__" block starts the server on all network interfaces (0.0.0.0) at port 5000. Binding to 0.0.0.0 is important because later, when the app runs inside a container, it needs to accept connections from outside the container.

  • Save app.py.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

from flask import Flask, jsonify, request

app = Flask(__name__)

tasks = []


@app.route("/")
def home():
    return jsonify({"message": "Task Tracker API is running"})


@app.route("/tasks", methods=["GET"])
def get_tasks():
    return jsonify({"tasks": tasks})


@app.route("/tasks", methods=["POST"])
def create_task():
    data = request.get_json()
    if not data or "title" not in data:
        return jsonify({"error": "title is required"}), 400
    task = {"id": len(tasks) + 1, "title": data["title"], "done": False}
    tasks.append(task)
    return jsonify(task), 201


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)
Run and test the API locally

Before containerizing anything, you want to confirm the app works on its own. This saves you from debugging both your code and your Docker setup at the same time.

  • Start the Flask server by running this command:
python app.py

You should see output similar to:

 * Serving Flask app 'app'
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:5000
 * Running on http://192.168.x.x:5000

Why does the server block my terminal?

Flask's development server runs in the foreground, which means your terminal is now occupied listening for requests. You'll need a second terminal window to send test requests with curl.

  • Open a new terminal window by pressing Cmd+T (macOS) or right-clicking the terminal icon in the taskbar and selecting New Window (Windows).
  • Test the welcome endpoint by running this command:
curl http://localhost:5000/

You should see this JSON response:

{"message":"Task Tracker API is running"}
  • Test creating a new task by running:
curl -X POST http://localhost:5000/tasks -H "Content-Type: application/json" -d '{"title": "Learn Docker"}'

You should see the newly created task returned with a 201 status:

{"done":false,"id":1,"title":"Learn Docker"}
  • Test listing all tasks by running:
curl http://localhost:5000/tasks

You should see your task in the list:

{"tasks":[{"done":false,"id":1,"title":"Learn Docker"}]}

Seeing "Connection refused"?

  • Make sure the Flask server is still running in your first terminal window. You should see the Running on output there.
  • Check that you're using port 5000 in the URL. If something else is already using port 5000, you'll see an Address already in use error in the server terminal.
  • If you see a syntax error in the server terminal, double check your app.py matches the full code in the tab above.

Help me debug my Flask server not responding to curl requests.

Stop the local server

The API works. In the next step, you'll run it inside a Docker container instead of directly on your machine.

  • Switch back to the terminal window where Flask is running.
  • Press Ctrl+C to stop the server.

You should see the server shut down and your terminal prompt return.

Why stop the server?

In the next step, Docker will run the same app inside a container on the same port (5000). If your local server is still running, the port will be occupied and the container won't be able to start.

Your API is confirmed working. Next up, you'll write a Dockerfile to package this app into a portable container image that runs anywhere Docker does.

Containerize the App with Docker

Your Flask API works on your machine. But what happens when you need to run it on a server, a colleague's laptop, or inside a cluster? Every environment has different Python versions, different system libraries, and different configurations.

This is the problem Docker solves. By packaging your app, its dependencies, and its runtime into a single container image, you guarantee it runs identically everywhere. In this step, you will write a Dockerfile, build an image, and run your API inside a container.

In this step, get ready to:
  • Write a Dockerfile that packages your Flask app.
  • Build a Docker image from the Dockerfile.
  • Run and test the containerized API, then clean up.
Create the Dockerfile

A Dockerfile is a recipe that tells Docker how to build your image. Each line is an instruction that adds a layer to the image, starting from a base operating system all the way up to your application code.

  • Create a new file called Dockerfile inside your task-tracker folder (no file extension) by running this command:
touch Dockerfile
  • Open Dockerfile in your code editor and paste the following code:
# Use a lightweight Python base image
FROM python:3.12-slim

# Set the working directory inside the container
WORKDIR /app

# Copy dependencies file first (for efficient caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy the application code
COPY app.py .

# Document which port the app uses
EXPOSE 5000

# Start the Flask app when the container runs
CMD ["python", "app.py"]

What does this Dockerfile do?

  • FROM python:3.12-slim starts from a minimal Python image. The slim variant is much smaller than the full Python image, which speeds up builds.
  • WORKDIR /app creates and switches to a /app directory inside the container. All subsequent commands run from here.
  • COPY requirements.txt . is copied first so Docker can cache the installed packages. If your code changes but dependencies don't, Docker skips reinstalling them.
  • RUN pip install --no-cache-dir -r requirements.txt installs Flask inside the image without storing pip's download cache (keeping the image smaller).
  • EXPOSE 5000 documents that the app listens on port 5000. It does not publish the port. That happens at runtime.
  • CMD ["python", "app.py"] is the command that runs when the container starts.
  • Save Dockerfile.
  • Confirm the file exists by running:
ls

You should see Dockerfile, app.py, and requirements.txt in the output.

Don't see the Dockerfile?

Make sure the file is named exactly Dockerfile with a capital D and no extension. It should not be Dockerfile.txt or dockerfile.

Make sure you are in the task-tracker directory when you run ls.

Help me troubleshoot my Dockerfile creation.

Build the Docker image

The Dockerfile is just a blueprint. To turn it into something you can run, you need to build it into a Docker image. The image is a snapshot of your entire app environment, frozen and ready to launch.

  • Build your Docker image by running this command:
docker build -t task-tracker:1.0 .

What does this command do?

  • docker build reads your Dockerfile and executes each instruction to create the image.
  • -t task-tracker:1.0 tags the image with the name task-tracker and version 1.0. Tags help you identify different versions of your image.
  • . tells Docker to look for the Dockerfile in the current directory.

The build takes 30-60 seconds the first time because Docker downloads the python:3.12-slim base image. You should see output ending with a line like Successfully tagged task-tracker:1.0.

  • Verify the image was created by running:
docker images

You should see task-tracker with the tag 1.0 in the list of images.

Seeing a build error?

If you see failed to read dockerfile, make sure you are in the task-tracker directory (the one containing your Dockerfile).

If Docker cannot connect, make sure Docker Desktop is running. You can check its icon in your system tray or menu bar.

Help me fix my Docker build error.

Run and test the container

Your image is built. Now you will run it as a container and confirm that your API works exactly the same way it did locally. The key difference is that the app is now running inside an isolated environment, completely independent of your machine's Python installation.

  • Start a container from your image by running this command:
docker run -d -p 5000:5000 --name task-tracker task-tracker:1.0

What does this command do?

  • docker run creates and starts a new container from the specified image.
  • -d runs the container in detached mode (in the background), so your terminal stays free.
  • -p 5000:5000 maps port 5000 on your machine to port 5000 inside the container. This is called port mapping. Without it, the container's network is isolated and you cannot reach the API.
  • --name task-tracker gives the container a friendly name so you can reference it easily later.

Docker prints a long string of characters (the container ID). This confirms the container started.

  • Test the welcome endpoint by running:
curl http://localhost:5000/

You should see {"message":"Task Tracker API is running"} in the response.

  • Test creating a task by running:
curl -X POST http://localhost:5000/tasks -H "Content-Type: application/json" -d '{"title": "Learn Docker"}'

You should see a JSON response with the new task, including an id of 1 and done set to false.

  • Verify the task was stored by listing all tasks:
curl http://localhost:5000/tasks

You should see the task you just created inside the tasks array.

Getting "Connection refused"?

Check that the container is actually running with docker ps. If you do not see task-tracker in the list, the container may have crashed on startup.

Run docker logs task-tracker to see the container's output. Look for Python errors that might indicate a problem with your app.py or missing dependencies.

If port 5000 is already in use (from the previous step's local server), make sure you stopped that process first.

Help me debug my Docker container.

Your API is running identically inside the container. The next step deploys this same image to Kubernetes, so you need to clean up this standalone container first.

  • Stop the running container:
docker stop task-tracker
  • Remove the stopped container:
docker rm task-tracker

Why stop and remove?

Stopping a container halts the process but keeps the container definition on disk. Removing it frees the name so you can reuse --name task-tracker later. The image itself is still available for Kubernetes to use in the next step.

✔️ Awesome, I've got everything!

Great. Double check you have saved your Dockerfile, then move on to the next step.

ⓧ I'd like to double check the full code

Here is the complete Dockerfile:

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

EXPOSE 5000

CMD ["python", "app.py"]

Your app.py and requirements.txt are unchanged from the previous step.

Your app is now a portable Docker image. Next up, you will deploy it to a local Kubernetes cluster and let Kubernetes manage multiple copies of it for you.

Deploy to Kubernetes

Your app runs perfectly in a single Docker container. But in the real world, you need multiple copies of your app running simultaneously to handle traffic and recover from failures.

This is where Kubernetes comes in. You will write two manifest files that tell Kubernetes exactly how to run and expose your app, then apply them to your local cluster.

In this step, get ready to:
  • Create a Deployment manifest that runs 2 replicas of your API.
  • Create a Service manifest that exposes your pods through a single endpoint.
  • Apply both manifests and confirm your pods are running.
Create the Deployment manifest

A Kubernetes Deployment is a configuration file that tells the cluster how many copies (replicas) of your app to run and which container image to use. Kubernetes reads this file and creates the requested number of Pods automatically.

  • Create a new file called deployment.yaml in your task-tracker project folder.
  • Paste the following YAML into the file:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: task-tracker
  labels:
    app: task-tracker
spec:
  replicas: 2
  selector:
    matchLabels:
      app: task-tracker
  template:
    metadata:
      labels:
        app: task-tracker
    spec:
      containers:
        - name: task-tracker
          image: task-tracker:1.0
          imagePullPolicy: Never
          ports:
            - containerPort: 5000

What does this manifest do?

This YAML file is a blueprint that Kubernetes uses to manage your app.

  • replicas: 2 tells Kubernetes to always keep 2 copies of your app running.
  • selector and template.metadata.labels connect the Deployment to its Pods using the label app: task-tracker.
  • imagePullPolicy: Never tells Kubernetes to use the Docker image you already built locally, instead of trying to download it from the internet.
  • containerPort: 5000 declares that the container listens on port 5000.
  • Save deployment.yaml.

YAML giving you trouble?

YAML is very strict about indentation. Every level of nesting uses exactly 2 spaces. Tabs will break the file.

Make sure the containers: line is indented under spec: (the inner one inside template), and that the dash before name: task-tracker lines up correctly.

help me fix indentation in my deployment.yaml

Create the Service manifest

Your Pods are running, but they each have their own temporary IP address that changes every time they restart. A Kubernetes Service gives you a single, stable endpoint that automatically routes traffic across all your Pods.

  • Create a new file called service.yaml in your task-tracker project folder.
  • Paste the following YAML into the file:
apiVersion: v1
kind: Service
metadata:
  name: task-tracker-service
spec:
  type: NodePort
  selector:
    app: task-tracker
  ports:
    - protocol: TCP
      port: 5000
      targetPort: 5000
      nodePort: 30080

What does this Service do?

  • type: NodePort exposes the Service on a specific port on your machine, so you can access it from your browser or curl.
  • selector: app: task-tracker tells the Service to route traffic only to Pods with that matching label.
  • nodePort: 30080 is the port you will use to reach the API from localhost:30080. Kubernetes requires NodePorts to be in the range 30000-32767.

Why NodePort instead of LoadBalancer?

On Docker Desktop's built-in Kubernetes, NodePort is universally supported and predictable. LoadBalancer services can behave differently depending on your OS and Docker Desktop version. NodePort gives you a reliable way to reach your app at localhost:30080 every time.

  • Save service.yaml.
Apply manifests and verify your pods are running

With both manifest files saved, you can now tell Kubernetes to create the resources they describe. The kubectl apply command reads your YAML files and creates (or updates) the corresponding resources in your cluster.

  • Apply the Deployment to your cluster by running this command:
kubectl apply -f deployment.yaml

You should see output similar to: deployment.apps/task-tracker created.

  • Apply the Service to your cluster by running this command:
kubectl apply -f service.yaml

You should see: service/task-tracker-service created.

  • Check that your pods are running by running this command:
kubectl get pods

You should see 2 pods with names starting with task-tracker- and a STATUS of Running.

  • Test the API through the Kubernetes Service by running this command:
curl http://localhost:30080/

You should see: {"message":"Task Tracker API is running"}.

What just happened?

Kubernetes read your Deployment manifest and created 2 Pods, each running your task-tracker:1.0 container image. The Service then created a stable network endpoint on port 30080 that load-balances requests across both Pods.

Your API is no longer a single container. It is now an orchestrated application with built-in redundancy.

Pods stuck in ImagePullBackOff or ErrImagePull?

This means Kubernetes is trying to pull the image from the internet instead of using your local copy. Confirm that your deployment.yaml includes imagePullPolicy: Never and that the image name matches exactly: task-tracker:1.0.

Also verify the image exists locally by running docker images | grep task-tracker. If it is missing, go back and rebuild it with docker build -t task-tracker:1.0 ..

help me fix ImagePullBackOff in my Kubernetes pods

Curl returns connection refused on port 30080?

Make sure your Service was created successfully by running kubectl get services. You should see task-tracker-service listed with type NodePort and port 30080.

If the pods are not yet in Running state, wait a few seconds and try kubectl get pods again. The Service cannot route traffic until at least one pod is ready.

help me debug why curl cannot reach my Kubernetes service

✔️ Awesome, I've got everything!

Great. Double check you have both files saved and both pods in Running status.

ⓧ I'd like to double check the full code

Here is the complete content of both manifest files:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: task-tracker
  labels:
    app: task-tracker
spec:
  replicas: 2
  selector:
    matchLabels:
      app: task-tracker
  template:
    metadata:
      labels:
        app: task-tracker
    spec:
      containers:
        - name: task-tracker
          image: task-tracker:1.0
          imagePullPolicy: Never
          ports:
            - containerPort: 5000
apiVersion: v1
kind: Service
metadata:
  name: task-tracker-service
spec:
  type: NodePort
  selector:
    app: task-tracker
  ports:
    - protocol: TCP
      port: 5000
      targetPort: 5000
      nodePort: 30080

Your API is now running as a multi-replica deployment inside Kubernetes. Next up, you will scale your app and watch Kubernetes heal itself when things go wrong.

Scale and Observe Kubernetes in Action

Your Task Tracker API is running in Kubernetes with two pods behind a Service. But what happens when traffic spikes and two copies aren't enough?

This is where orchestration earns its name. In this step, you'll scale your app up, break things on purpose, and watch Kubernetes heal itself automatically.

In this step, get ready to:
  • Scale your deployment from 2 to 4 replicas.
  • Delete a pod and observe Kubernetes self-healing.
  • Access the API through the NodePort service and scale back down.
Scale up to 4 replicas

Right now your Deployment runs 2 replicas. Kubernetes makes it trivial to add more copies of your app without touching any code or config files.

  • Scale your deployment to 4 replicas by running this command:
kubectl scale deployment task-tracker --replicas=4

What does this command do?

This tells Kubernetes to update the desired state of the task-tracker Deployment to 4 replicas. Kubernetes immediately starts creating 2 new pods to match that desired state.

  • Watch the new pods come up in real time by running:
kubectl get pods -w

You should see 4 pods with names starting with task-tracker- and their status transitioning to Running.

  • Press Ctrl+C to stop watching once all 4 pods show Running.

What does the -w flag do?

The -w flag stands for "watch." Instead of printing the pod list once and exiting, kubectl streams live updates as pod statuses change. This is useful for observing scaling events in real time.

Not seeing 4 pods?

Make sure you typed the deployment name exactly as task-tracker. You can check your deployment exists by running kubectl get deployments.

If pods are stuck in Pending or ContainerCreating for more than 30 seconds, Docker Desktop may need more resources. Check that Docker Desktop has at least 4 CPU cores and 6 GB memory allocated in its settings.

help me troubleshoot why my Kubernetes pods aren't starting after scaling

Delete a pod and watch Kubernetes self-heal

A core promise of Kubernetes is self-healing. If a pod crashes or disappears, Kubernetes notices the actual state no longer matches the desired state and creates a replacement automatically.

  • First, get the list of running pods by running:
kubectl get pods
  • Copy the full name of any one pod from the output. It will look something like task-tracker-6b8d5f7c4a-x9k2m.
  • Delete that pod by running this command (replacing the pod name with yours):
kubectl delete pod [[POD_NAME="your pod name from the list above"]]
  • Immediately check the pods again by running:
kubectl get pods

You should see a brand new pod with a different name already starting up or running. Kubernetes detected that only 3 pods existed (one fewer than the desired 4) and created a replacement.

Why did Kubernetes create a new pod?

The Deployment controller constantly compares the actual number of running pods to the desired replica count. When you deleted a pod, the count dropped to 3. Kubernetes immediately scheduled a new pod to bring it back to 4.

This is the self-healing loop in action. In production, this means crashed containers are replaced automatically without human intervention.

Don't see a replacement pod?

Make sure you deleted a pod managed by the Deployment (its name starts with task-tracker-). If you accidentally created a standalone pod with a different name, Kubernetes won't replace it.

Run kubectl get pods again after 10 seconds. The replacement may take a moment to reach Running status.

help me understand why my deleted pod wasn't replaced

Access the API and scale back down

Your NodePort Service has been routing traffic to your pods this whole time. Let's confirm the API responds through the load-balanced endpoint, then scale back down to 2 replicas.

  • Hit the API through the NodePort service by running:
curl http://localhost:30080/

You should see the JSON response {"message":"Task Tracker API is running"}.

  • Test the tasks endpoint as well by running:
curl http://localhost:30080/tasks

You should see {"tasks":[]} since no tasks have been created on this pod yet.

How does the Service route traffic?

The Service uses the label selector app: task-tracker to find all matching pods. It load-balances incoming requests across all 4 replicas. You don't need to know which specific pod handles each request.

Port 30080 is the NodePort exposed on your machine. Traffic flows from localhost:30080 to the Service on port 5000, then to a pod's container on port 5000.

  • Scale the deployment back down to 2 replicas by running:
kubectl scale deployment task-tracker --replicas=2
  • Confirm only 2 pods remain by running:
kubectl get pods

You should see 2 pods in Running state. Kubernetes terminated the extra pods to match the new desired count.

Still seeing more than 2 pods?

Pods in Terminating state are shutting down. Wait a few seconds and run kubectl get pods again. They should disappear shortly.

If pods are stuck in Terminating for more than 60 seconds, you can force delete them with kubectl delete pod <pod-name> --force.

help me figure out why my pods are stuck terminating

You just experienced the core of container orchestration: scaling up, self-healing, and scaling down. Next up, the Secret Mission challenges you to add health checks that make your deployment production-ready.

Secret mission

Add Liveness Probes

Your app is running in Kubernetes, but what happens if it becomes unresponsive? Right now, Kubernetes has no way to tell if your container is healthy or just frozen. In this mission, you'll add a health check endpoint and configure Kubernetes liveness probes to automatically restart unhealthy containers.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally using Docker Desktop, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Docker image (task-tracker:1.0).
  • Kubernetes Deployment (task-tracker).
  • Kubernetes Service (task-tracker-service).
  • Python virtual environment (venv folder).
  • Project files (app.py, Dockerfile, requirements.txt, deployment.yaml, service.yaml).
  • Docker Desktop Kubernetes cluster (enabled in settings).

Keep everything running

No action needed. Choose this if you are still actively building or want to experiment further with Kubernetes commands.

  • Your pods, services, and Docker image stay available for immediate use.
  • Docker Desktop continues running Kubernetes in the background, using some CPU and memory.

Pause - I'll come back to this later

Shut down running processes to free up memory while keeping your project files intact.

  • Delete the Kubernetes resources (Deployment and Service) by running:
kubectl delete -f deployment.yaml
kubectl delete -f service.yaml
  • Optionally, disable Kubernetes in Docker Desktop by going to Settings > Kubernetes and unchecking Enable Kubernetes.
  • Your project folder, Docker image, and all manifest files remain on disk for when you return.
  • To resume, re-enable Kubernetes and run kubectl apply -f deployment.yaml and kubectl apply -f service.yaml again.

Delete - I don't want to use this again

Remove all project resources and start fresh.

  • Delete the Kubernetes resources by running:
kubectl delete -f deployment.yaml
kubectl delete -f service.yaml
  • Remove the Docker image by running:
docker rmi task-tracker:1.0
  • Delete the project folder from your Desktop.
  • Optionally, disable Kubernetes in Docker Desktop if you do not plan to use it for other projects.

That's a Wrap!

That's a Wrap!

Nice work. You just took a Python script all the way from local code to a fully orchestrated, self-healing application running in Kubernetes.

You've learned how to:

  • Built a Flask REST API, wrote a Dockerfile, and packaged the app into a Docker container image.
  • Created Kubernetes Deployment and Service manifests from scratch and deployed your app to a local cluster.
  • Scaled replicas up and down with kubectl and observed Kubernetes self-healing automatically replace deleted pods.
  • Added a health check endpoint and configured liveness probes for production-ready container resilience.

Ready to quiz yourself?