Test Your Docker Image in Kubernetes

Build, inspect, and deploy your Spring Boot app to a local Kubernetes cluster.

Introduction

30 Second Summary

You have written the recipe, but you have never cooked the dish. Your Dockerfile and Kubernetes manifest sit in your project folder as untested text files with no proof they actually work.

In this project, you will build your multi-stage Docker image, deploy it to a local Kubernetes cluster, and prove it is production-ready. You will also diagnose the classic image-not-found failure that every developer hits the first time they use Minikube.

What You'll Build

Picture running curl and getting a live response from your Spring Boot book catalog, served through a Kubernetes LoadBalancer service running entirely on your Mac.

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

  • A locally-built Docker image proven to be production-lean through layer inspection and size comparison against the full builder stage.
  • Your book catalog app running inside a local Kubernetes cluster on your Mac, accessible through a LoadBalancer Service you can curl from your terminal.
  • Firsthand experience diagnosing and fixing the classic Minikube image gap between your host Docker daemon and Minikube's container runtime.
  • Secret Mission: Scale your deployment to 4 replicas and verify Kubernetes load balances traffic across all pods.

Are there any prerequisites?

This project picks up from a prior session where you wrote a multi-stage Dockerfile and a deployment.yml for a Spring Boot book catalog app. You also need Docker Desktop installed on your Mac.

Before We Start

You have a Dockerfile and a deployment.yml sitting in your project, but neither has been tested yet. Before running any commands, let's lock in what you're about to build and why it matters.

Set Up Minikube

Your Dockerfile and Kubernetes manifest are sitting in your project, but to test the manifest you need an actual Kubernetes cluster. Running one locally means you can catch deployment issues before they ever reach a cloud environment or CI/CD pipeline.

Minikube creates a real, single-node Kubernetes cluster right on your Mac. It uses Docker Desktop as its backend, so you don't need a cloud account or an additional virtual machine. In this step, you'll install Minikube and start your local cluster.

In this step, get ready to:
  • Install Minikube on your Mac.
  • Start a local Kubernetes cluster using Docker Desktop.
  • Confirm the cluster is healthy and ready for deployments.
Install Minikube

Minikube is a tool that runs a single-node Kubernetes cluster inside a container on your machine. It gives you a real Kubernetes environment for testing without needing a cloud provider.

  • Check if Minikube is already installed by running this command:
minikube version

✔️ I see a version number

Minikube is already installed. You should see output starting with minikube version: followed by a version like v1.38.1. Skip ahead to the next substep.

ⓧ Command not found

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

Homebrew downloads and installs Minikube. This may take a minute.

  • After the install finishes, verify Minikube is available by running:
minikube version

You should see output starting with minikube version: followed by a version number like v1.38.1.

Still seeing command not found?

Make sure Homebrew itself is installed. If brew is also not found, visit brew.sh to install it first, then retry brew install minikube.

If Homebrew is installed but minikube still isn't found, try closing and reopening your terminal so your shell picks up the new path.

Help me troubleshoot my Minikube installation on macOS

Start your Minikube cluster

Minikube needs a driver to create its cluster environment. The Docker driver runs the cluster inside Docker Desktop, which is the simplest option since you already have Docker Desktop installed.

  • Confirm Docker Desktop is running by checking for the Docker whale icon in your menu bar at the top of your screen.
  • Start your local Kubernetes cluster by running this command:
minikube start --driver=docker

This takes 1-2 minutes on the first run as Minikube downloads its base image. You'll see progress lines as it pulls the image, creates the container, and configures Kubernetes components.

What does this command do?

The --driver=docker flag tells Minikube to run your Kubernetes cluster inside Docker containers. Since Docker Desktop is already on your Mac, this means no additional virtual machine software is needed.

Behind the scenes, Minikube creates a Docker container that acts as a Kubernetes node. This node runs all the Kubernetes components (API server, scheduler, controller manager) needed to form a working cluster.

Seeing a Docker error?

If you see a message about Docker not being available or the Docker daemon not running, Docker Desktop needs to be started. Look for the Docker whale icon in your Applications folder and click it. Wait for the whale icon to appear in your menu bar, then retry the minikube start command.

If you see a timeout or resource error, Docker Desktop may not have enough memory allocated. Open Docker Desktop's Settings, go to Resources, and make sure at least 4 GB of memory is assigned.

Help me fix my Minikube startup error

Verify your cluster is ready

Before you run the next commands, take a guess. If the cluster started successfully, what status do you expect to see for the host, kubelet, and API server?

  • Check Minikube's status by running this command:
minikube status

You should see host, kubelet, and apiserver all showing Running, and kubeconfig showing Configured.

  • Verify Kubernetes recognizes the cluster node by running:
kubectl get nodes

You should see one node named minikube with status Ready. This confirms your local Kubernetes cluster is fully operational.

What is kubectl?

`kubectl` is the command-line tool for interacting with Kubernetes clusters. It comes bundled with Docker Desktop, so it should already be available on your machine.

When you started Minikube, it automatically configured kubectl to point at your new local cluster. Every kubectl command you run from now on targets this Minikube cluster.

Not seeing Running or Ready?

If minikube status shows Stopped, your cluster did not finish starting. Run minikube start --driver=docker again and wait for it to complete without errors.

If kubectl get nodes shows NotReady, the Kubernetes components are still initializing. Wait 30 seconds and run the command again.

If kubectl itself is not found, you can use Minikube's bundled version instead. Run minikube kubectl -- get nodes as a substitute.

Help me troubleshoot my Minikube cluster status

Your local Kubernetes cluster is running and ready for action. Next up, you'll build your multi-stage Docker image and see your Spring Boot app come alive inside a container.

Build and Run Your Docker Image

Your Minikube cluster is up and running with a healthy node ready for deployments. The next step is to prove that your multi-stage Dockerfile actually works.

You have a Dockerfile sitting in your project, but it has never been built. Building and running it locally is the first real proof that your Spring Boot app compiles and starts inside a container.

In this step, get ready to:
  • Build your multi-stage Docker image from the project Dockerfile.
  • Run the image as a container and map it to localhost.
  • Verify Spring Boot starts successfully inside the container.
Build the Docker image

Your project has a multi-stage Dockerfile that compiles the Spring Boot app in a builder stage, then copies only the compiled JAR into a slim runtime image. Building it turns that blueprint into a real image.

  • Confirm your terminal is in the folder containing your Dockerfile by running:
ls Dockerfile deployment.yml

You should see both files listed in the output. If not, use cd to navigate to the folder that contains them.

  • Build the Docker image by running this command:
docker build -t local-bookcatalog:latest .

What does this command do?

  • -t local-bookcatalog:latest tags the image with the name local-bookcatalog and the tag latest.
  • . tells Docker to use the current directory as the build context.
  • Docker runs both stages of your multi-stage Dockerfile. Maven compiles the JAR in the builder stage. Then only the JAR is copied into the slim runtime image.

First build takes a while

The first build takes several minutes as Maven downloads all project dependencies. You will see each Dockerfile instruction execute in order. Subsequent builds use Docker's layer cache and finish much faster.

  • Wait for the build to complete. You should see a success message with the image ID at the end of the output.

Build failing?

  • Make sure you are in the directory that contains your Dockerfile. Run ls to check.
  • If you see a network error during Maven dependency downloads, check your internet connection and try the build again.
  • Make sure Docker Desktop is running. You can check by looking for the Docker icon in your menu bar.

My Docker build is failing. How can I fix it?

Run the container

Your image is built. Time to start a container from it and map it to a port on your Mac so you can reach the app.

  • Start a container from your new image by running this command:
docker run -d -p 8080:8080 --name bookcatalog-test local-bookcatalog:latest

What do these flags mean?

  • -d runs the container in detached mode, so it runs in the background and returns you to the terminal.
  • -p 8080:8080 maps port 8080 on your Mac to port 8080 inside the container.
  • --name bookcatalog-test gives the container a specific name instead of a random one.
  • Confirm the container is running by running this command:
docker ps

You should see bookcatalog-test listed with a status of Up.

Container not showing up?

  • If you see a port conflict error, another process is already using port 8080. Stop the other process, or remove this container with docker rm bookcatalog-test and rerun with a different port like -p 9090:8080.
  • If the container exited immediately, run docker logs bookcatalog-test to see what went wrong.

My Docker container exited immediately after starting. How can I troubleshoot it?

Test the running app

The container is running in the background. Time to check that Spring Boot actually started inside it.

  • Check the container logs by running this command:
docker logs bookcatalog-test

Look for the Spring Boot banner and startup messages in the output. These confirm the JVM launched and your app attempted to initialize.

What if I see database errors?

If your app requires a database, you may see connection errors in the logs. That is completely expected.

The key proof here is that the container started, the JVM launched, and Spring Boot attempted to initialize. The Dockerfile works. Database connectivity is a separate concern.

Before you run the next command, think about what you expect to see. Will your Spring Boot app return a web page, JSON data, or an error?

  • Hit the app from your terminal by running this command:
curl http://localhost:8080

If Spring Boot started fully, you will see a response from your app. If the app needs a database to serve content, you may see an error page or a connection refusal. Either result is fine for this step.

What counts as success here?

The real proof is in the logs, not the curl response. Seeing the Spring Boot banner and initialization messages in docker logs confirms your Dockerfile correctly compiled the JAR and the runtime image can execute it. Your container is running and the JAR executes inside the slim runtime image.

Seeing 'connection refused' from curl?

  • Spring Boot may still be starting up. Wait a few seconds and try the curl command again.
  • Run docker ps to check the container is still running. If it exited, check docker logs bookcatalog-test for the cause.
  • If you used a different port mapping earlier (like -p 9090:8080), use that port in your curl command instead.

My curl request to localhost:8080 is failing. How can I debug this?

Your container is running and Spring Boot is alive inside it. Next, you will dig into the image layers to prove your multi-stage build actually stripped out the build tools.

Prove Your Multi-Stage Build Is Production-Ready

Your container is running and Spring Boot launched inside it. That proves your Dockerfile can compile and package the app.

The question is whether the multi-stage build actually stripped out the build tools. Without evidence, you could be shipping an image with hundreds of megabytes of Maven and the JDK hiding inside.

In this step, get ready to:
  • Build the builder stage as a standalone image for comparison.
  • Compare the builder and runtime image sizes.
  • Inspect the final image layers to confirm no build tools leaked through.
Build just the builder stage

Your multi-stage Dockerfile has a builder stage that contains the full Maven toolchain and JDK for compiling your app. Building just this stage as its own image gives you a baseline to compare against the final runtime image.

  • Build only the builder stage and tag it as a separate image by running this command:
docker build --target builder -t bookcatalog-builder:latest .

What does --target do?

The --target flag tells Docker to stop at a specific stage in your multi-stage Dockerfile. Here it builds only the stage named builder (the AS builder line in your Dockerfile) and tags it as bookcatalog-builder:latest.

This gives you the full build environment as a standalone image, including Maven, the JDK, and your source code. Everything the runtime image was supposed to leave behind.

You should see build output ending with a success message showing the image was tagged. This build runs faster than your first one because Docker caches the layers it already downloaded.

Seeing 'failed to solve' or 'Dockerfile not found'?

  • Make sure you are in your project root directory where your Dockerfile lives. Run ls and confirm you can see the Dockerfile listed.
  • Check that your Dockerfile has an AS builder label on its first FROM instruction. The --target flag looks for a stage with that exact name.
  • Still stuck? Help me debug a docker build --target error for my multi-stage Dockerfile
Compare image sizes

You now have two images from the same Dockerfile. One contains the full build environment. The other contains only what your app needs at runtime.

Before you compare their sizes, make a prediction. How much bigger do you think the builder image is than the runtime image?

  • Check both image sizes by running:
docker images | grep bookcatalog

The bookcatalog-builder image weighs in at 400MB or more. The local-bookcatalog image is significantly smaller because it only contains the JRE and your compiled JAR.

Why such a big difference?

The builder image packs in Maven, the full JDK, your source code, and every downloaded dependency. All of that is needed to compile your app, but none of it is needed to run it.

The runtime image starts fresh from a slim Alpine base with just the JRE. Only the compiled JAR file crosses the stage boundary through the COPY instruction. That size gap is your multi-stage build doing its job.

Inspect the final image layers

Image sizes show the overall difference, but inspecting individual layers shows you exactly what made it into the production image. Each layer corresponds to an instruction in your Dockerfile.

Before you look, what do you expect to find? Maven install commands, or just the JRE and your JAR?

  • Inspect the layers of your runtime image by running:
docker history local-bookcatalog:latest

Each row is a layer. You should see layers for the Alpine base, the JRE, user creation (addgroup/adduser), the COPY of the JAR from the builder stage, and the ENTRYPOINT. No Maven commands, no source code copies, no mvn calls.

What does this prove?

Every layer in the final image comes from either the Alpine base image or your Dockerfile's runtime stage instructions. The build tools stayed behind in the builder stage.

This is the whole point of a multi-stage build. Your production image carries only what the app needs to run. A smaller image means faster deploys, a smaller attack surface, and less wasted storage.

Seeing Maven or JDK layers in the output?

  • Make sure you ran docker history local-bookcatalog:latest (the runtime image), not docker history bookcatalog-builder:latest (the builder image).
  • Check that your Dockerfile has two FROM instructions. The second FROM should start from a slim runtime base image, not the builder.
  • Need more help? Help me understand the docker history output for my multi-stage build

Your Docker image is proven lean. The multi-stage build stripped out Maven, the JDK, and your source code, leaving only what the app needs to run. Next up, you'll deploy your Kubernetes manifest to the Minikube cluster and discover what happens when Kubernetes tries to find your image.

Deploy to Minikube

Your Docker image is built, running, and proven lean. The multi-stage build strips out Maven, the JDK, and all source code. Time to put it into a real Kubernetes cluster.

In this step, you'll apply your deployment.yml manifest to the Minikube cluster and check whether your pods come up.

In this step, get ready to:
  • Clean up the local Docker container from the previous step.
  • Apply your Kubernetes manifest to the Minikube cluster.
  • Check your pod status and investigate the result.
Clean up the local container

The bookcatalog-test container from the previous step is still running on port 8080. You need to stop and remove it before deploying to Kubernetes so the port is free.

  • Stop and remove the container by running these commands:
docker stop bookcatalog-test
docker rm bookcatalog-test

What do these commands do?

  • docker stop gracefully shuts down the running container.
  • docker rm removes the stopped container from Docker entirely, freeing up the name and resources.
  • Each command prints bookcatalog-test to confirm it was stopped and removed.
  • Confirm no containers are running by running:
docker ps

You should see no bookcatalog-test container in the list. Minikube's own containers may still appear, and that is expected.

Apply your Kubernetes manifest

Your deployment.yml has been sitting in your project since you wrote it. Now it's time to send it to the Minikube cluster and let Kubernetes create the resources it defines.

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

You should see two lines of output confirming the resources were created:

deployment.apps/bookcatalog-deployment created
service/bookcatalog-service created

What did this create?

Your manifest defines two Kubernetes resources.

  • A Deployment named bookcatalog-deployment with 2 replicas. Kubernetes will try to start 2 pods running your Spring Boot app.
  • A Service named bookcatalog-service of type LoadBalancer. This gives your app a single entry point that distributes traffic across the pods.

Seeing 'error: the path deployment.yml does not exist'?

  • Make sure your terminal is in your project root, the same folder that contains your Dockerfile and deployment.yml.
  • Run ls to list the files in your current directory and confirm both files are there.

Why can't kubectl find my deployment.yml?

Check your pod status

Kubernetes accepted your manifest and confirmed both resources were created. Before you check on the pods, take a moment to predict: do you expect them to be running?

  • Check the pod status by running:
kubectl get pods

The pods are not running. They are stuck in ErrImagePull or ImagePullBackOff status.

This is not a mistake. This is the single most common failure developers hit when deploying to Minikube for the first time.

Why can't Kubernetes find your image?

Minikube runs its own container runtime, completely separate from the Docker daemon on your Mac. When you ran docker build earlier, the image was stored in your Mac's Docker. Minikube's Docker has never seen it.

Kubernetes inside Minikube tries to find local-bookcatalog:latest in its own runtime, fails, then tries to pull it from a remote registry. Since local-bookcatalog does not exist on any registry, the pull fails too.

  • Dig into the failure details by running this command, replacing the pod name with one from your kubectl get pods output:
kubectl describe pod [[POD_NAME="a pod name from kubectl get pods output"]]

Scroll to the Events section at the bottom of the output. You will see events confirming the image pull failure, including messages about failing to pull the image and back-off attempts.

What is ImagePullBackOff?

When Kubernetes fails to pull an image, it retries with increasing delays. ErrImagePull is the initial failure. ImagePullBackOff means Kubernetes is waiting before trying again. The pod cycles between these two states until the image becomes available.

Your image exists on your Mac, but Minikube's container runtime is a completely separate world. In the next step, you'll bridge that gap and get your app running inside Kubernetes.

Load the Image and Access Your App

Your pods are stuck and Kubernetes is throwing ErrImagePull errors.

You proved in the last step that Minikube runs its own isolated Docker daemon, so your host Docker image is invisible to the cluster. The fix is a single command called minikube image load. It copies your image from your Mac's Docker daemon into Minikube's container runtime, making it visible to Kubernetes.

In this step, get ready to:
  • Load your Docker image into Minikube's container runtime.
  • Expose the LoadBalancer service with minikube tunnel.
  • Access your running app through Kubernetes.
Load the image into Minikube and restart pods

Your image exists in your Mac's Docker daemon but not inside Minikube's container runtime. The minikube image load command bridges this gap by copying the image across.

  • Copy your image into Minikube's container runtime by running this command:
minikube image load local-bookcatalog:latest

What does this command do?

minikube image load takes a Docker image from your host machine and copies it into Minikube's internal container runtime. After this, Kubernetes can find local-bookcatalog:latest locally and no longer needs to pull from a remote registry.

  • Restart the deployment so the pods pick up the newly available image by running:
kubectl rollout restart deployment bookcatalog-deployment
  • Watch the pods come up by running:
kubectl get pods -w

Wait until both pods show Running status. Press Ctrl+C to stop watching.

Pods still not running?

  • Make sure minikube image load completed without errors. If it timed out, run it again.
  • If pods show CrashLoopBackOff instead of Running, the image loaded successfully but the app is crashing inside the container. Run kubectl logs <pod-name> to check the Spring Boot startup logs.
  • Why are my Kubernetes pods not reaching Running status?
Expose the LoadBalancer service

Your manifest defines a LoadBalancer service, but Minikube needs a tunnel to assign an external IP. Without it, kubectl get services shows <pending> for the external IP.

  • In your Terminal, press Cmd+N to open a new window.
  • In the new terminal window, start the tunnel by running this command:
minikube tunnel

It may prompt for your password. Enter it and keep this terminal window open.

What does minikube tunnel do?

minikube tunnel creates a network route that lets your Mac reach Kubernetes services of type LoadBalancer.

Without it, the service has no external IP and you cannot access your app from outside the cluster. With it running, Minikube assigns 127.0.0.1 as the external IP for Docker Desktop users.

  • Switch back to your first terminal window.
  • Check the service by running:
kubectl get services

You should see bookcatalog-service with an EXTERNAL-IP of 127.0.0.1 instead of <pending>.

Still seeing pending?

  • Make sure minikube tunnel is still running in the other terminal window. If it exited, run it again.
  • If you see an error about conflicting tunnels, stop any existing tunnel with Ctrl+C and run minikube tunnel again.
  • Why is my Kubernetes service external IP still pending?
Curl your app through Kubernetes

This is the moment of truth. Your Spring Boot app should now be running inside Kubernetes, behind a LoadBalancer service.

Before you run the next command, what do you expect to see? Will you get a response from your app, or an error?

  • Hit the service by running:
curl http://127.0.0.1:80

You should see a response from your Spring Boot app. Your book catalog is now running inside a real Kubernetes cluster, behind a Deployment with 2 replicas and a LoadBalancer Service, all on your Mac.

What just happened?

  • Your request hit 127.0.0.1:80, which minikube tunnel routed to the bookcatalog-service inside the cluster.
  • The Service forwarded the request to one of the two pods running your local-bookcatalog container.
  • The Spring Boot app inside the pod processed the request and returned the response back through the same chain.

Getting connection refused?

  • Confirm minikube tunnel is still running in the other terminal window.
  • Run kubectl get pods and make sure both pods show Running status.
  • Run kubectl logs <pod-name> to check if Spring Boot started successfully inside the pods. If the app needs a database, you may see a connection error in the logs. That is expected for this project.
  • Why can't I curl my Kubernetes service at 127.0.0.1?

Secret mission

Scale and Watch Kubernetes Load Balance

Your deployment runs 2 replicas, but is Kubernetes actually distributing traffic across them? Scale up to 4 replicas and prove that the LoadBalancer service sends requests to different pods, not just one.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing cloud costs. Minikube does consume Docker Desktop resources (CPU and memory) while running, so decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • minikube tunnel process running in your second terminal.
  • Minikube cluster (running as Docker containers via Docker Desktop).
  • Kubernetes Deployment (bookcatalog-deployment) and Service (bookcatalog-service) inside the Minikube cluster.
  • Docker images on your Mac: local-bookcatalog:latest and bookcatalog-builder:latest.

Keep everything running

No action needed. Choose this if you are still actively building or experimenting with your Minikube cluster.

  • Your Minikube cluster stays running and continues to use Docker Desktop CPU and memory.
  • Keep the minikube tunnel terminal open so your LoadBalancer service remains accessible at 127.0.0.1:80.
  • Your Docker images (local-bookcatalog:latest and bookcatalog-builder:latest) remain stored on your Mac.

Pause - I'll come back to this later

Shut down your Minikube cluster to free up Docker Desktop resources, but keep the cluster state so you can pick up where you left off.

  • Switch to the terminal running minikube tunnel and press Ctrl+C to stop it.
  • Stop the Minikube cluster by running:
minikube stop
  • Your cluster state, Deployment, and Service are preserved. When you are ready to continue, restart with minikube start.
  • Your Docker images remain on your Mac and do not need to be rebuilt.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes the Minikube cluster (which also removes the Kubernetes Deployment and Service running inside it) and cleans up your Docker images.

  • Switch to the terminal running minikube tunnel and press Ctrl+C to stop it.
  • Delete the Minikube cluster by running:
minikube delete
  • This removes the entire cluster, including bookcatalog-deployment, bookcatalog-service, and the copy of local-bookcatalog:latest loaded into Minikube.
  • Remove the Docker images from your Mac by running:
docker rmi local-bookcatalog:latest bookcatalog-builder:latest
  • Confirm the images are removed by running docker images | grep bookcatalog. You should see no results.
  • Your Spring Boot project files, Dockerfile, and deployment.yml remain on disk. Delete them manually if you no longer need them.

Nice Work!

Nice Work!

You just took a Dockerfile and a Kubernetes manifest from untested files on disk to a fully running app inside a local cluster you can curl from your browser.

You've learned how to:

  • Build and validate a multi-stage Docker image, proving the production image is lean by comparing it against the builder stage and inspecting layers with docker history to confirm no build tools leaked through.
  • Diagnose and fix the classic Minikube "image not found" failure by loading your image into Minikube's isolated container runtime with minikube image load.
  • Deploy a Kubernetes manifest to a local cluster and access your Spring Boot app through a LoadBalancer service using minikube tunnel.
  • Secret Mission: Scaled the deployment to 4 replicas and verified Kubernetes distributes traffic across all pod replicas.

Ready to quiz yourself?