Deploy a Spring Boot API to Kubernetes
Containerize a Spring Boot API with Docker and deploy it to Kubernetes.
Introduction
30 Second Summary
When you build an app on your laptop, it runs perfectly. Hand it to a teammate or push it to a server, and suddenly nothing works because the environment is different.
In this project, you will package a Spring Boot REST API into a Docker container and deploy it to a local Kubernetes cluster. You will watch Kubernetes keep your app running even when you deliberately break things.
What You'll Build
You open your browser, hit a URL, and get a JSON greeting back from your Spring Boot API, served from inside a Kubernetes cluster running entirely on your machine.
By the end of this project, you'll have:
- A running API inside a Docker container, built with an optimized multi-stage Dockerfile that cuts image size by more than half.
- A Kubernetes Deployment that keeps your app alive. Delete a running Pod on purpose and watch Kubernetes spin up a replacement within seconds.
- A Kubernetes Service routing traffic to your app so you can access it from your browser at localhost.
- Secret Mission: Scale to multiple replicas and externalize configuration with a Kubernetes ConfigMap.
Are there any prerequisites?
This project assumes basic Spring Boot knowledge, such as building REST APIs. You should also be comfortable running commands in a terminal.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters. This step is your chance to think about the project goals before any hands-on work begins.
Set Up Docker Desktop and Kubernetes
Before you can package your Spring Boot API into a container or deploy it anywhere, you need two tools on your machine. Docker provides the container runtime that packages your app. Kubernetes provides the orchestration platform that manages it.
In this step, you will install Docker Desktop, which bundles everything you need: the Docker engine for building containers, a built-in Kubernetes cluster, and the kubectl command-line tool for talking to Kubernetes.
In this step, get ready to:
- Install Docker Desktop with the WSL 2 backend.
- Enable Kubernetes inside Docker Desktop.
- Verify both Docker and Kubernetes are running from the command line.
Install Docker Desktop
Docker Desktop is the application that runs the Docker engine and the Kubernetes cluster on your Windows machine. It also installs kubectl automatically so you can manage Kubernetes from PowerShell.
- Open your browser and go to the Docker Desktop download page.
- Click the download button for Windows to download Docker Desktop Installer.exe.
- Double-click the downloaded Docker Desktop Installer.exe to run the installer.
- The installer asks which installation mode you prefer. Select Per-user installation (recommended, no admin rights needed).
- On the Configuration page, make sure Use WSL 2 instead of Hyper-V is checked. It should be selected by default.
- Click Ok and let the installer finish.
What is WSL 2?
WSL 2 (Windows Subsystem for Linux 2) runs a full Linux kernel inside a lightweight virtual machine on your Windows machine. Docker uses this Linux kernel to run containers natively.
Enabling WSL 2 for the first time is a one-time operation that may require admin privileges. If Windows prompts you, accept the elevation request.
- Press the Windows key to open your search bar.
- Type Docker Desktop and press Enter to launch it.
- Wait for Docker Desktop to finish starting. The whale icon in the system tray stops animating when it is ready.
Docker Desktop won't start?
Make sure virtualization is enabled in your BIOS/UEFI settings. Open Task Manager, go to the Performance tab, and check that Virtualization shows Enabled.
If Docker Desktop is stuck on starting, restart your computer and try launching it again.
Help me troubleshoot Docker Desktop not starting on Windows.
Enable Kubernetes in Docker Desktop
Docker Desktop includes a built-in Kubernetes cluster, but it is not enabled by default. You need to turn it on in the settings.
- In Docker Desktop, click the gear icon to open Settings.
- Select the Kubernetes tab on the left.
- Check the box labeled Enable Kubernetes.
- Click Apply & Restart.
Wait for the green indicator to appear beside Kubernetes running at the bottom-left of the Docker Desktop window. This may take 1-2 minutes on first setup as Docker downloads the Kubernetes components.
What is happening behind the scenes?
Docker Desktop is downloading container images for the Kubernetes control plane (the API server, scheduler, and etcd database) and running them as containers on your machine. It also installs kubectl and automatically configures it to talk to this local cluster.
Kubernetes not starting?
If the green indicator does not appear after a few minutes, go to Settings > Kubernetes and click Reset Kubernetes cluster. This reverts the cluster to a clean state. Then click Apply & Restart again.
Help me troubleshoot Kubernetes not starting in Docker Desktop.
Verify Docker and Kubernetes are ready
- Press the Windows key to open your search bar.
- Type PowerShell and press Enter to open it.
- Confirm Docker is installed by running this command:
docker --version
✔️ I see a version number
Docker is installed and ready. Move on to the Kubernetes check below.
ⓧ Command not found
PowerShell cannot find the docker command. Try these fixes:
- Make sure Docker Desktop is running. Check the system tray for the whale icon.
- Close and reopen PowerShell so it picks up the updated system PATH.
- If it still does not work, restart your computer and try again.
Help me fix docker command not found in PowerShell on Windows.
ⓧ I see an older Docker version
If you see a version number but Docker Desktop was installed a while ago, open Docker Desktop and check for updates. Click the gear icon, then Software updates, and install any available updates.
Now for the key check. Before you run the next command, what do you expect to see if Kubernetes is running correctly?
- Confirm Kubernetes is running by running this command:
kubectl get nodes
✔️ I see docker-desktop in Ready status
You should see output like this:
NAME STATUS ROLES AGE VERSION
docker-desktop Ready control-plane 1m v1.x.x
The docker-desktop node in Ready status means your local Kubernetes cluster is up and running. Docker, Kubernetes, and kubectl are all set.
ⓧ I see an error or no nodes
If you see a connection error or an empty list, try these fixes:
- Make sure the green Kubernetes running indicator is showing in Docker Desktop. If it is not, go back to the previous substep and enable Kubernetes.
- If kubectl is pointing to a different cluster, switch to the Docker Desktop context by running:
kubectl config use-context docker-desktop
- Then run kubectl get nodes again.
Help me fix kubectl not connecting to my Docker Desktop Kubernetes cluster.
Your container runtime and orchestration platform are ready to go. Next up, you will build a Spring Boot REST API that you will later package into a Docker container and deploy to this Kubernetes cluster.
Build and Run Your Spring Boot API
Your Docker and Kubernetes environment is ready. Now you need something to actually containerize and deploy.
Before packaging anything into a container, you need a working application. In this step, you will generate a Spring Boot REST API, add two endpoints, and run it locally to confirm everything works outside of Docker first.
In this step, get ready to:
- Generate a Spring Boot project from Spring Initializr.
- Create a REST controller with two JSON endpoints.
- Run the API locally and verify it responds in your browser.
Generate the project from Spring Initializr
Spring Initializr is a web tool that generates ready-to-code Spring Boot projects with your chosen dependencies already wired in.
- Open https://start.spring.io in your browser.
- Under Project, select Gradle - Groovy.
- Under Language, select Java.
- In the Spring Boot dropdown, select 3.5.15.
- Under Project Metadata, set Group to com.example.
- Set Artifact to demo.
- Under Java, select 17.
Now add the two dependencies your project needs.
- Click the Add Dependencies button.
- Search for Spring Web and select it.
- Click Add Dependencies again.
- Search for Spring Boot Actuator and select it.
- Click Generate to download the ZIP file.
What are Spring Web and Actuator?
Spring Web provides everything you need to build REST APIs. It includes an embedded Tomcat server that runs your app without needing a separate web server.
Spring Boot Actuator adds production-ready monitoring endpoints like /actuator/health. You will use this later when your app runs inside Kubernetes.
- Extract the downloaded demo.zip file to your Desktop.
- Press the Windows key to open your search bar.
- Type IntelliJ IDEA and press Enter to open it.
- On the IntelliJ welcome screen, click Open.
- Navigate to the demo folder on your Desktop and click OK.
IntelliJ may ask to trust the project
If you see a dialog asking whether to trust the project, click Trust Project. IntelliJ needs this permission to load the Gradle build files and download dependencies.
You should see the project structure in IntelliJ's Project panel on the left, including build.gradle, settings.gradle, and the src directory.
Don't see the project files?
- Make sure you selected the demo folder itself, not a parent or child folder inside it.
- If the Project panel is not visible, press Alt+1 to toggle it.
- If Gradle is still downloading dependencies, wait for the progress bar at the bottom of IntelliJ to finish.
- Help me troubleshoot my IntelliJ project setup
Create a REST controller and run the API
Your project scaffold is ready. Now you will add a REST controller with two endpoints that return JSON responses, then run the API to see it in action.
- In the Project panel, expand src > main > resources and double-click application.properties to open it.
- Add this line below the existing spring.application.name=demo line:
management.endpoints.web.exposure.include=health,info
What does this property do?
This tells Spring Boot Actuator to expose the health and info endpoints over HTTP. You will use the health endpoint later when your app runs inside Kubernetes.
- Save application.properties by pressing Ctrl+S.
- In the Project panel, expand src > main > java > com.example.demo.
- Right-click the com.example.demo package.
- Select New, then Java Class.
- Type HelloController and press Enter.
- Replace all the generated code in the new file with the following:
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
// Mark this class as a REST controller that returns data directly
@RestController
public class HelloController {
// Root endpoint returns the app name
@GetMapping("/")
public Map<String, String> root() {
return Map.of("app", "Spring Boot on Kubernetes");
}
// Hello endpoint returns a greeting and status
@GetMapping("/hello")
public Map<String, String> hello() {
return Map.of(
"message", "Hello from Spring Boot!",
"status", "running"
);
}
}
What does this code do?
- @RestController marks this class as a Spring MVC controller that returns data directly. Spring automatically converts the return values to JSON.
- @GetMapping("/") maps HTTP GET requests to the root path. It returns a map with the app name.
- @GetMapping("/hello") maps GET requests to /hello. It returns a map with a greeting message and a status field.
- Map.of() creates an immutable Java map. Spring converts it to a JSON object in the HTTP response automatically.
- Save HelloController.java by pressing Ctrl+S.
✔️ Awesome, I've got everything!
Make sure you have saved both application.properties and HelloController.java.
ⓧ I'd like to double check the full code
Here is the full HelloController.java file. Make sure yours matches:
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
public class HelloController {
@GetMapping("/")
public Map<String, String> root() {
return Map.of("app", "Spring Boot on Kubernetes");
}
@GetMapping("/hello")
public Map<String, String> hello() {
return Map.of(
"message", "Hello from Spring Boot!",
"status", "running"
);
}
}
And here is the full application.properties file:
spring.application.name=demo
management.endpoints.web.exposure.include=health,info
Time to see your API in action.
- Open the IntelliJ terminal by pressing Alt+F12.
- Start the API by running this command:
gradlew bootRun
What should I see in the terminal?
Spring Boot logs will scroll in the terminal. Look for a line that includes Tomcat started on port 8080 followed by Started DemoApplication. This means your API is up and ready to receive requests.
The first run may take a minute while Gradle downloads dependencies. Subsequent runs will be faster.
Before checking the browser, take a guess. What do you think you will see when you open http://localhost:8080/hello?
- Open http://localhost:8080/hello in your browser.
You should see a JSON response: {"message":"Hello from Spring Boot!","status":"running"}. Your REST API is working.
- Switch back to the IntelliJ terminal.
- Press Ctrl+C to stop the application.
API not responding in the browser?
- Make sure the terminal shows Started DemoApplication before opening the browser. The app needs a few seconds to start up.
- Check that no other application is already using port 8080. Close any other running servers or IDEs that might occupy that port.
- If you see an error that gradlew is not recognized, try running .\gradlew.bat bootRun instead.
- Help me troubleshoot my Spring Boot API
Your API works locally. In the next step, you will package it into a Docker container so it can run the same way anywhere.
Containerize Your API with Docker
Your Spring Boot API runs perfectly on your machine. You can start it, hit the endpoints, and see JSON in your browser. But "it works on my machine" does not travel well.
Docker solves this by packaging your app and everything it needs into a single portable container image. That image runs the same way on any machine. In this step, you will build a Docker image for your API and see firsthand why image optimization matters.
In this step, get ready to:
- Build a Docker image from a simple, single-stage Dockerfile.
- Optimize the image with a multi-stage Dockerfile that cuts its size by more than half.
- Run the containerized API and verify it serves the same JSON response.
Build the JAR and create a naive Dockerfile
Before Docker can package your app, it needs something to package. A Spring Boot application compiles down to a single executable JAR file that bundles your code with all its dependencies.
- In the IntelliJ terminal, build the executable JAR by running this command:
gradlew bootJar
You should see BUILD SUCCESSFUL in the terminal output.
Build failed?
- Make sure your terminal is in the project root directory (the demo folder that contains build.gradle).
- If the app is still running from the previous step, stop it with Ctrl+C first.
- On Windows PowerShell, you may need to run .\gradlew bootJar instead.
Help me fix my Gradle build error.
With the JAR built, you can now write a Dockerfile. A Dockerfile is a recipe that tells Docker how to assemble an image for your app.
- Create a new file called Dockerfile in the project root (the same folder as build.gradle). No file extension needed.
- Paste the following code into Dockerfile:
# Use the full JDK as the base image
FROM eclipse-temurin:17-jdk
WORKDIR /app
# Copy the pre-built JAR into the container
COPY build/libs/*.jar app.jar
# Start the Spring Boot application
ENTRYPOINT ["java", "-jar", "app.jar"]
What does this Dockerfile do?
- FROM eclipse-temurin:17-jdk starts from a base image that has Java 17 pre-installed.
- COPY build/libs/*.jar app.jar copies the JAR you just built into the container.
- ENTRYPOINT tells Docker to run your JAR when the container starts.
- Save Dockerfile.
- Build your first Docker image by running this command in the IntelliJ terminal:
docker build -t spring-boot-k8s:naive .
What does this command do?
- docker build reads the Dockerfile and creates a container image.
- -t spring-boot-k8s:naive tags the image with a name and a label. The naive tag marks this as the unoptimized version.
- The . at the end tells Docker to use the current directory as the build context.
The build takes a moment. Once it finishes, check how large the image turned out.
- Check the image size by running:
docker images spring-boot-k8s
You will see the spring-boot-k8s:naive image listed at over 500 MB. That is a lot of weight for a small REST API.
Why is the image so large?
The eclipse-temurin:17-jdk base image includes the full Java Development Kit. That means compilers, debugging tools, and development utilities that your app never uses at runtime.
A bloated image wastes storage, slows down deployments, and increases the attack surface in production. You can do much better.
Optimize with a multi-stage Dockerfile
A multi-stage build splits the Dockerfile into two stages. The first stage uses the full JDK to compile your code. The second stage starts from a tiny runtime-only image and copies just the finished JAR.
- Open Dockerfile and replace all of its contents with the following:
# Stage 1: Build the JAR using the full JDK
FROM eclipse-temurin:17-jdk AS builder
WORKDIR /app
# Copy Gradle wrapper and config files first for dependency caching
COPY gradle gradle
COPY gradlew build.gradle settings.gradle ./
RUN chmod +x gradlew
# Download dependencies in a separate layer (cached on rebuild)
RUN ./gradlew dependencies --no-daemon
# Copy source code and build the JAR
COPY src src
RUN ./gradlew bootJar --no-daemon
# Stage 2: Run using a minimal JRE image
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
# Copy only the built JAR from the builder stage
COPY --from=builder /app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
What does this multi-stage Dockerfile do?
Stage 1 (builder) does the heavy lifting:
- It copies the Gradle wrapper and config files first, then downloads dependencies. This creates a cached layer so Docker skips re-downloading dependencies when only your source code changes.
- chmod +x gradlew makes the Gradle wrapper executable inside the Linux container. Windows file permissions do not carry over.
- It then copies the source code and runs bootJar to compile the JAR inside the container.
Stage 2 starts fresh from eclipse-temurin:17-jre-alpine, a minimal image with only the Java Runtime Environment. It copies the finished JAR from Stage 1 and nothing else. No compiler, no build tools, no source code.
- Save Dockerfile.
Before rebuilding, you should also tell Docker which files to ignore during the build. A .dockerignore file works like .gitignore. It keeps unnecessary files out of the build context.
- Create a new file called .dockerignore in the project root (same folder as Dockerfile).
- Paste the following into .dockerignore:
.gradle
build
.idea
*.iml
What does .dockerignore do?
This keeps the .gradle cache, the build output folder, and IntelliJ project files (.idea, *.iml) out of the Docker build context. Smaller context means faster builds and no risk of stale files leaking into the image.
- Save .dockerignore.
- Rebuild the Docker image with the optimized Dockerfile by running:
docker build -t spring-boot-k8s:latest .
This build takes longer because Docker is compiling your code inside the container from scratch. Wait for it to finish.
Before you check the result, take a guess. How large do you think this optimized image will be compared to the 500+ MB naive version?
- Compare both image sizes by running:
docker images spring-boot-k8s
The spring-boot-k8s:latest image should be roughly 200-250 MB. That is less than half the size of the naive image, with zero changes to your application code.
Image size still over 400 MB?
- Make sure you replaced the entire Dockerfile contents. The second FROM line must use eclipse-temurin:17-jre-alpine (JRE, not JDK).
- Check that .dockerignore is saved in the project root, not inside a subfolder.
Help me troubleshoot my Docker image size.
✔️ Awesome, I've got everything!
Great. Make sure you have saved both Dockerfile and .dockerignore.
ⓧ I'd like to double check the full code
Here is the complete Dockerfile:
FROM eclipse-temurin:17-jdk AS builder
WORKDIR /app
COPY gradle gradle
COPY gradlew build.gradle settings.gradle ./
RUN chmod +x gradlew
RUN ./gradlew dependencies --no-daemon
COPY src src
RUN ./gradlew bootJar --no-daemon
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
And here is .dockerignore:
.gradle
build
.idea
*.iml
Run the container and test
Your optimized image is built. Time to run it and confirm the API works inside a container.
- Start a container from your image by running:
docker run -p 8080:8080 spring-boot-k8s:latest
What does -p 8080:8080 do?
The -p 8080:8080 flag maps port 8080 on your machine to port 8080 inside the container. Without this, the container runs but you cannot reach it from your browser.
Wait for the Spring Boot startup log to finish. You will see a line in the terminal indicating the application has started successfully.
Do you think the API will respond exactly the same way as when you ran it directly on your machine?
- Open your browser and go to http://localhost:8080/hello.
You should see the same JSON response as before, with a greeting message and a status. The difference is that this time, your app is running inside a Docker container, not directly on your machine.
- Return to the IntelliJ terminal and press Ctrl+C to stop the container.
Page not loading at localhost:8080?
- Make sure the previous local run of the app (from the last step) is stopped. If port 8080 is already in use, Docker will show an error like Bind for 0.0.0.0:8080 failed: port is already allocated. Stop the other process first.
- Check the terminal for error messages. If the container exits immediately, there may be a problem with the JAR inside the image. Try rebuilding with docker build -t spring-boot-k8s:latest ..
- Make sure Spring Boot has finished starting before you open the browser. It needs a few seconds to initialize.
Help me troubleshoot my Docker container.
Your API is now packaged as a lean, portable Docker image. Next up, you will deploy this container to a real Kubernetes cluster running on your machine and see what happens when you try to access it.
Deploy Your Container to Kubernetes
Your Spring Boot API is packaged into a lean Docker image that runs the same way anywhere. In the last step, you cut the image size by more than half using a multi-stage build.
Docker is great for running a single container on your machine. Kubernetes takes it further by managing containers at scale, handling restarts, and orchestrating deployments across clusters. In this step, you will hand your Docker image to Kubernetes and see it running as a managed Pod.
In this step, get ready to:
- Write a Kubernetes Deployment manifest that describes how to run your container.
- Deploy the manifest to your local cluster and check the results.
Write a Kubernetes Deployment manifest
A Deployment is a Kubernetes resource that tells the cluster what container to run, how many copies to keep alive, and how to update them. You describe the desired state in a YAML file, and Kubernetes makes it happen.
- Create a folder for your Kubernetes manifests by running this command in the IntelliJ terminal:
mkdir k8s
- Confirm the folder exists by running:
ls
You should see k8s listed alongside your existing project files like Dockerfile and build.gradle.
- In IntelliJ, right-click the k8s folder in the Project sidebar.
- Select New, then File.
- Name the file deployment.yaml and press Enter.
- Copy the following YAML into the file:
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-boot-app
labels:
app: spring-boot-app
spec:
replicas: 1
selector:
matchLabels:
app: spring-boot-app
template:
metadata:
labels:
app: spring-boot-app
spec:
containers:
- name: spring-boot-app
image: spring-boot-k8s:latest
imagePullPolicy: Never
ports:
- containerPort: 8080
What does this manifest do?
- replicas: 1 tells Kubernetes to run exactly one instance of your app.
- selector and matchLabels connect the Deployment to the Pods it manages. Kubernetes uses the label app: spring-boot-app to track which Pods belong to this Deployment.
- image: spring-boot-k8s:latest points to the Docker image you built in the previous step.
- imagePullPolicy: Never tells Kubernetes to use your local Docker image instead of trying to download it from a remote registry.
- containerPort: 8080 declares the port your Spring Boot app listens on inside the container.
Why imagePullPolicy: Never?
Your Docker image exists only on your local machine. Without imagePullPolicy: Never, Kubernetes tries to pull the image from Docker Hub and fails because spring-boot-k8s:latest does not exist in any remote registry.
- Save deployment.yaml by pressing Ctrl+S.
- Confirm the file exists by running this in the IntelliJ terminal:
ls k8s
You should see deployment.yaml listed in the output.
Don't see deployment.yaml?
- Make sure you created the file inside the k8s folder, not in the project root.
- Check that your terminal is in the project root directory. Run ls and confirm you can see the k8s folder.
Still stuck? Help me find my deployment.yaml file
✔️ Awesome, I've got everything!
Great. Make sure you have saved k8s/deployment.yaml before moving on.
ⓧ I'd like to double check the full code
Here is the complete k8s/deployment.yaml file. Compare it against yours line by line.
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-boot-app
labels:
app: spring-boot-app
spec:
replicas: 1
selector:
matchLabels:
app: spring-boot-app
template:
metadata:
labels:
app: spring-boot-app
spec:
containers:
- name: spring-boot-app
image: spring-boot-k8s:latest
imagePullPolicy: Never
ports:
- containerPort: 8080
Deploy and observe
Your manifest describes the desired state. Now you will hand it to Kubernetes and let the cluster make it real.
- Apply the Deployment to your cluster by running this command:
kubectl apply -f k8s/deployment.yaml
You should see output confirming the Deployment was created.
- Check that the Deployment is ready by running:
kubectl get deployments
You should see spring-boot-app with 1/1 under the READY column.
- Check the Pod status by running:
kubectl get pods
You should see one Pod with a name starting with spring-boot-app- and a STATUS of Running.
Pod not reaching Running status?
- If the Pod shows an image-related error, check that imagePullPolicy: Never is set in your deployment.yaml. Without it, Kubernetes tries to pull the image from a remote registry and fails because your image only exists locally.
- Make sure the image name spring-boot-k8s:latest matches exactly. Run docker images spring-boot-k8s to confirm the image exists.
- Check your YAML indentation. YAML is whitespace-sensitive. Every nested level uses exactly two spaces, not tabs.
Need more help? Help me debug my Kubernetes Pod
Your app is running inside Kubernetes. Before you try to access it, what do you think will happen when you open http://localhost:8080/hello in your browser?
- Open your browser and go to http://localhost:8080/hello.
The page does not load. Your API is running inside Kubernetes, but you cannot reach it from your browser.
Why can't I access my app?
Kubernetes isolates Pods from external traffic by default. A Deployment manages the lifecycle of your Pods, keeping them running and restarting them if they crash. It does not handle network access.
To make your app reachable from outside the cluster, you need a separate Kubernetes resource called a Service. A Service routes incoming traffic to the right Pods. Without one, your app is sealed off from the outside world.
Your app is alive inside Kubernetes, but sealed off from the outside world. In the next step, you will create a Kubernetes Service to punch a hole through and make the API reachable from your browser. Then you will break things on purpose and watch Kubernetes fix them automatically.
Expose Your App and Watch Kubernetes Self-Heal
Your containerized API is deployed to Kubernetes and the Pod is running. But when you tried to open it in the browser at the end of Step 4, nothing loaded.
A Deployment keeps your Pods running, but a Service is what makes them reachable. In this step, you will create a Service to bridge that gap, then deliberately break your app and watch Kubernetes fix it automatically.
In this step, get ready to:
- Create a Kubernetes Service to expose your app on port 30080.
- Delete a running Pod and watch Kubernetes self-heal.
- Check the Spring Boot Actuator health endpoint through Kubernetes.
Create a Kubernetes Service
Deployments and network access are separate concerns in Kubernetes. Your Deployment ensures the Pod stays alive, but it does not expose it to the outside world. A Service is a separate resource that routes incoming traffic to the right Pods.
- Create a new file called k8s/service.yaml inside the k8s folder you created in Step 4, and paste the following code:
# Define a Service to route traffic to your Spring Boot Pods
apiVersion: v1
kind: Service
metadata:
name: spring-boot-service
spec:
# NodePort exposes the Service on a static port on your machine
type: NodePort
# Route traffic to any Pod with this label
selector:
app: spring-boot-app
ports:
# Map internal cluster port to an external port on localhost
- port: 8080
targetPort: 8080
nodePort: 30080
What does this code do?
- type: NodePort makes the Service accessible from outside the cluster on a fixed port. NodePort is ideal for local development on Docker Desktop.
- selector: app: spring-boot-app tells the Service to route traffic to any Pod carrying that label. This matches the label in your k8s/deployment.yaml.
- port: 8080 is the port inside the cluster. targetPort: 8080 is the port on the Pod. nodePort: 30080 is the port exposed on your machine, so you will open localhost:30080 in your browser.
- Save k8s/service.yaml.
- Apply the Service to your Kubernetes cluster by running this command in PowerShell:
kubectl apply -f k8s/service.yaml
You should see output confirming the Service was created.
Before you open the browser, what do you think will happen this time? In Step 4, opening http://localhost:8080/hello returned nothing because no Service existed.
- Open http://localhost:30080/hello in your browser.
You should see the same JSON response as before: {"message":"Hello from Spring Boot!","status":"running"}. Your API is now accessible through Kubernetes.
Can't see the JSON response?
- Make sure you used port 30080 in the browser URL, not 8080. Kubernetes routes through the NodePort, not the Pod's internal port.
- Verify the selector in k8s/service.yaml reads app: spring-boot-app. This must match the label in your k8s/deployment.yaml exactly.
- Confirm your Pod is still running. If it crashed, check the Deployment from Step 4.
- Why can't I access my Spring Boot API through the Kubernetes NodePort Service?
✔️ Awesome, I've got everything!
Great. Double check you have saved k8s/service.yaml and that the Service is applied to your cluster.
ⓧ I'd like to double check the full code
Here is the complete k8s/service.yaml file. Compare it against yours:
apiVersion: v1
kind: Service
metadata:
name: spring-boot-service
spec:
type: NodePort
selector:
app: spring-boot-app
ports:
- port: 8080
targetPort: 8080
nodePort: 30080
Delete a Pod and watch Kubernetes recover
Kubernetes does not just start your app. It continuously compares the actual state of the cluster to the desired state you declared in your Deployment. If a Pod disappears, Kubernetes notices and creates a replacement.
You are about to test this by deleting the only running Pod. This is called self-healing.
- Check which Pod is currently running by running this command:
kubectl get pods
You should see one Pod with a name like spring-boot-app-xxxxx-xxxxx in Running status. Copy the full Pod name from the output.
Before you delete the Pod, what do you think will happen? Your Deployment declared 1 replica. If you destroy the only running Pod, will the API go down permanently, or will something else happen?
- Delete the running Pod by running this command, replacing the pod name with the one you copied:
kubectl delete pod [[POD_NAME="your pod name, e.g. spring-boot-app-xxxxx-xxxxx"]]
- Immediately check the Pods again by running:
kubectl get pods
You should see the old Pod in Terminating status and a brand-new Pod being created. Within seconds, the new Pod reaches Running status.
What just happened?
This is Kubernetes self-healing in action. The Deployment continuously watches its Pods. When you deleted the Pod, the actual state (0 running replicas) no longer matched the desired state (1 replica).
Kubernetes detected the mismatch and automatically created a new Pod to restore the desired state. You did not have to intervene. This is one of the biggest reasons teams use Kubernetes for production workloads.
- Refresh http://localhost:30080/hello in your browser.
The API is back, now served by the brand-new Pod that Kubernetes created.
Pod not recovering?
- Wait a few seconds and try kubectl get pods again. Kubernetes needs a moment to schedule the replacement Pod.
- Make sure imagePullPolicy: Never is still set in k8s/deployment.yaml. Without it, Kubernetes tries to pull the image from a remote registry and fails.
- Why isn't Kubernetes automatically replacing my deleted Pod?
Check the Actuator health endpoint
Your API includes Spring Boot Actuator, which you added as a dependency back in Step 2. Actuator exposes operational endpoints like health checks that are essential for running apps in Kubernetes.
- Open http://localhost:30080/actuator/health in your browser.
You should see {"status":"UP"}. This confirms your Spring Boot app is healthy and responding through the Kubernetes Service.
Why does this health endpoint matter?
In a production Kubernetes setup, you would configure liveness and readiness probes pointing to this endpoint. Kubernetes uses liveness probes to detect when an app is stuck and needs a restart. Readiness probes tell Kubernetes when a Pod is ready to receive traffic.
The /actuator/health endpoint gives Kubernetes a reliable signal to automate both of these checks. The self-healing you just witnessed was triggered by a missing Pod. Health probes would let Kubernetes detect and fix a running-but-broken Pod too.
Secret mission
Scale to Multiple Replicas and Externalize Config
Your API is running as a single instance with a hardcoded greeting. Scale your app to 3 replicas for high availability, and learn to manage configuration externally using a Kubernetes ConfigMap. No image rebuilds needed just to change a setting.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Kubernetes resources (Deployment, Service, ConfigMap) on the local cluster.
- Docker images (spring-boot-k8s:latest and spring-boot-k8s:naive).
- Local Kubernetes cluster (enabled through Docker Desktop).
- Docker Desktop application.
- Project files (the demo folder).
Keep everything running
No action needed. Choose this if you are still actively building or plan to extend this project further.
- Docker Desktop and Kubernetes continue running in the background.
- They consume some CPU and memory while active.
- Your Docker images, Kubernetes resources, and project files all remain in place.
Pause - I'll come back to this later
Shut down the Kubernetes cluster to free up CPU and memory. Your manifest files and Docker images are preserved so you can pick up where you left off.
- Open Docker Desktop and click the gear icon to open Settings.
- Select the Kubernetes tab.
- Uncheck Enable Kubernetes and click Apply & Restart.
- Your YAML manifests in the k8s/ folder and Docker images stay on disk. Re-enable Kubernetes and reapply the manifests when you are ready to continue.
Delete - I don't want to use this again
Remove all project resources and start fresh. This deletes your Kubernetes resources, Docker images, and project files from your machine.
- Delete all Kubernetes resources by running this command in PowerShell:
kubectl delete -f k8s/
- Remove both Docker images by running:
docker rmi spring-boot-k8s:latest spring-boot-k8s:naive
- Disable Kubernetes in Docker Desktop. Open Settings (gear icon), select the Kubernetes tab, uncheck Enable Kubernetes, and click Apply & Restart.
- To uninstall Docker Desktop, press the Windows key and search for Add or remove programs. Find Docker Desktop in the list and click Uninstall.
- Close the project in IntelliJ IDEA, then delete the demo folder from wherever you extracted it.
Nice Work!
Nice Work!
Nice work! You just took a Spring Boot API from running on your machine to running inside a production-style Kubernetes cluster. That is the exact workflow backend teams use to ship Java applications.
Here's what you accomplished:
- Wrote a multi-stage Dockerfile that builds your app with the full JDK and runs it on a minimal JRE-alpine image, cutting the container size by more than half.
- Deployed your containerized API to a local Kubernetes cluster using a Deployment manifest and exposed it to your browser through a NodePort Service.
- Witnessed Kubernetes self-healing by deleting a running Pod and watching Kubernetes automatically spin up a replacement within seconds.
- Secret Mission: Scaled to 3 replicas for availability and externalized configuration using a Kubernetes ConfigMap, proving you can manage application config without rebuilding the image.
Ready to quiz yourself?