Deploy Microservices to Kubernetes Local Environment
New to Kubernetes? Check out our companion article Understanding Kubernetes Fundamentals to learn the core concepts before diving into this practical guide.
Prerequisites
Make sure the following tools are installed before proceeding:
- Java 21+
- Maven 3.9+
- Docker
- Kind (Kubernetes in Docker)
- kubectl
Note: Kind is a lightweight tool that runs Kubernetes clusters inside Docker containers. It is ideal for local development and CI pipelines where a full cloud cluster is not needed.
FreightFlow Nexus Services Overview
The application consists of four Spring Boot microservices:
- Config Server (8071) - Centralized configuration management using Spring Cloud Config
- API Service (8080) - Core business logic for freight operations (shipments, orders, customers)
- Tracking Service (8090) - Real-time shipment tracking and GPS location updates
- Gateway (8072) - API Gateway for routing, authentication, and load balancing
Dockerfile Example
freightflow-api/Dockerfile
FROM eclipse-temurin:21-jre-alpine
# Set working directory
WORKDIR /app
# Copy the jar file
COPY target/*.jar app.jar
# Expose the application port
EXPOSE 8080
# Run the application
ENTRYPOINT ["java", "-jar", "app.jar"]All services use the
eclipse-temurin:21-jre-alpine base image for a lightweight footprint (~170MB). The pattern is identical across services, with only port numbers varying.Create the Kind Cluster
A Kubernetes cluster is where your applications run. Kind creates this cluster using Docker containers instead of virtual machines, making it lightweight and fast to start. We'll create a cluster named
nexus:kind create cluster --name nexusAfter a few seconds, your cluster will be ready. Verify it's up and running:
kubectl cluster-info --context kind-nexusYou should see output showing the Kubernetes control plane is running at a local address. This confirms
kubectl (the Kubernetes command-line tool) is pointing at your new Kind cluster and can communicate with it. The cluster is now ready to accept deployments.Build the Services
Before we can run our services in Kubernetes, we need to compile them into executable JAR files. Maven will build all four Spring Boot microservices in one command:
mvn clean packageThis command cleans previous builds and creates fresh JAR files in each service's
target/ directory. These JARs contain everything needed to run the services, including dependencies. The build also runs all tests to ensure code quality before deployment.Build Docker Images
Kubernetes runs applications in containers. We need to package each JAR file into a Docker image, which includes the Java runtime, the application code, and all dependencies. This makes the application portable and consistent across environments.
docker build -t nexus-config:v0.0.4 ./freightflow-config
docker build -t nexus-api:v0.0.4 ./freightflow-api
docker build -t nexus-tracking:v0.0.4 ./freightflow-tracking
docker build -t nexus-gateway:v0.0.4 ./freightflow-gatewayEach command builds an image tagged with version
v0.0.4. The tag helps track different versions of your application. After building, you can verify the images exist by running docker images.Load Images into Kind
Here's a critical step unique to Kind: the cluster runs inside Docker containers with its own isolated image registry. Even though you built images locally with Docker, Kind can't see them yet. You must explicitly transfer each image into the Kind cluster:
kind load docker-image nexus-config:v0.0.4 --name nexus
kind load docker-image nexus-api:v0.0.4 --name nexus
kind load docker-image nexus-tracking:v0.0.4 --name nexus
kind load docker-image nexus-gateway:v0.0.4 --name nexusThis copies each image from your local Docker daemon into Kind's internal registry. Each load takes a few seconds depending on image size. You'll see a progress indicator as the image is transferred.
Common Mistake: Forgetting this step causes Kubernetes pods to fail with
ImagePullBackOfferrors. The pods try to pull images from Docker Hub or other registries, but the images only exist locally. Loading them into Kind makes them available to the cluster.Apply Kubernetes Manifests
Now we tell Kubernetes what to deploy. Instead of plain YAML files, this project uses Kustomize, a built-in Kubernetes tool that lets you customize configurations for different environments (local, UAT, production) without duplicating files.
One command applies all resources for the local environment:
kubectl apply -k k8s/overlays/local/This creates namespaces, service accounts, deployments, services, and secrets. You'll see output like
namespace/nexus created, deployment.apps/api created, etc., as Kubernetes processes each resource.To preview what will be deployed without actually applying it:
kubectl kustomize k8s/overlays/local/This shows the fully merged YAML that Kubernetes will receive, helpful for debugging configuration issues.
The Kustomize directory layout looks like this:
k8s/
├── base/ # Shared manifests
│ ├── kustomization.yml
│ ├── namespace.yml
│ ├── rbac.yml
│ ├── freightflow-config/
│ ├── freightflow-api/
│ ├── freightflow-tracking/
│ └── freightflow-gateway/
└── overlays/
├── local/ # Kind (local development)
│ ├── kustomization.yml
│ ├── secrets.yml
│ └── patches/
└── aks/
├── uat/ # AKS UAT
└── prod/ # AKS ProdThe
base/ folder contains shared configurations used everywhere. The overlays/local/folder contains local-specific patches like imagePullPolicy: Never (telling Kubernetes not to pull from remote registries) and local database URLs.Kustomize Configuration Deep Dive
Let's look inside the Kustomize configuration to understand how it works. This section shows the actual YAML files that define your deployment. Understanding these helps you customize settings for your own projects or troubleshoot issues.
Why show this? Most tutorials skip the configuration details, leaving you unable to adapt them to your needs. These examples provide a working template you can modify for your applications.
Base Configuration
The base
kustomization.yml defines shared resources used across all environments:k8s/base/kustomization.yml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: nexus
resources:
- namespace.yml
- rbac.yml
- freightflow-config/deployment.yml
- freightflow-config/service.yml
- freightflow-api/deployment.yml
- freightflow-api/service.yml
- freightflow-tracking/deployment.yml
- freightflow-tracking/service.yml
- freightflow-gateway/deployment.yml
- freightflow-gateway/service.ymlThe
namespace.yml creates an isolated namespace for the application:k8s/base/namespace.yml
apiVersion: v1
kind: Namespace
metadata:
name: nexusThe
rbac.yml sets up permissions for service discovery. Spring Cloud Kubernetes needs permission to query the Kubernetes API to find other services:k8s/base/rbac.yml
apiVersion: v1
kind: ServiceAccount
metadata:
name: nexus-service-account
namespace: nexus
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: service-discovery-role
namespace: nexus
rules:
- apiGroups: [""]
resources: ["services", "endpoints", "pods"]
verbs: ["get", "list", "watch"]
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: service-discovery-rolebinding
namespace: nexus
subjects:
- kind: ServiceAccount
name: nexus-service-account
namespace: nexus
roleRef:
kind: Role
name: service-discovery-role
apiGroup: rbac.authorization.k8s.ioThis creates a service account (like a user account for apps), a role defining permissions (can read services/endpoints/pods), and a binding connecting them. Without this, Spring Cloud Kubernetes can't discover services and will fail to start.
Sample Deployment & Service
A Deployment tells Kubernetes how to run your application. Here's the API service deployment:
k8s/base/freightflow-api/deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
labels:
app: api
spec:
replicas: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
serviceAccountName: nexus-service-account
containers:
- name: api
image: nexus-api:v0.0.4
ports:
- containerPort: 8080
env:
- name: SPRING_CLOUD_CONFIG_URI
value: "http://config:8071"
- name: ENVIRONMENT
value: "default"
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"Key parts explained:
replicas: 1- Run one instance (pod) of this serviceimage: nexus-api:v0.0.4- Use the Docker image we built and loaded earlierports- Expose port 8080 for HTTP trafficenv- Environment variables (config server URL, environment name)resources- Memory and CPU limits prevent one service from hogging all cluster resources
A Service makes your pods accessible on the network. Here's the API service definition:
k8s/base/freightflow-api/service.yml
apiVersion: v1
kind: Service
metadata:
name: api
labels:
app: api
spec:
type: ClusterIP
selector:
app: api
ports:
- name: http
port: 8080
targetPort: 8080
protocol: TCPThis creates a stable network endpoint. Even if pods restart and get new IP addresses, other services can always reach the API at
api:8080. The ClusterIP type means it's only accessible within the cluster (perfect for internal microservice communication).Local Overlay Configuration
The local overlay
kustomization.yml references base resources and applies environment-specific patches:k8s/overlays/local/kustomization.yml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
- secrets.yml
patches:
- path: patches/config-deployment-patch.yml
- path: patches/api-deployment-patch.yml
- path: patches/tracking-deployment-patch.yml
- path: patches/gateway-deployment-patch.ymlSecrets are defined in
secrets.yml (never commit real secrets to Git):k8s/overlays/local/secrets.yml
apiVersion: v1
kind: Secret
metadata:
name: nexus-secrets
namespace: nexus
type: Opaque
stringData:
CONFIG_REPO_URL: "https://github.com/your-org/nexus-config"
GITHUB_TOKEN: "<YOUR_GITHUB_TOKEN>"
DB_URL: "jdbc:postgresql://localhost:5432/nexus"
DB_USERNAME: "postgres"
DB_PASSWORD: "<YOUR_DB_PASSWORD>"Patches modify the base deployment for specific environments. Here's the local environment patch:
k8s/overlays/local/patches/api-deployment-patch.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
template:
spec:
containers:
- name: api
imagePullPolicy: Never # Critical for Kind
env:
- name: ENVIRONMENT
value: "local"
- name: SPRING_DATASOURCE_URL
valueFrom:
secretKeyRef:
name: nexus-secrets
key: DB_URL
- name: SPRING_DATASOURCE_USERNAME
valueFrom:
secretKeyRef:
name: nexus-secrets
key: DB_USERNAME
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef:
name: nexus-secrets
key: DB_PASSWORDWhat this does: It merges with the base deployment, adding/overriding specific fields. The critical setting is
imagePullPolicy: Never, which tells Kubernetes to only use locally loaded images. The environment variables come from secrets instead of being hardcoded, keeping sensitive data like database passwords secure.Key Points:
imagePullPolicy: Neveris critical for Kind to use locally loaded images- Patches use strategic merge to add/override specific fields without duplicating entire manifests
- Secrets are referenced via
secretKeyRefto avoid hardcoding sensitive values - Resource limits prevent any single service from consuming all cluster resources
Verify Pods are Running
After applying the manifests, Kubernetes starts creating pods (containers running your services). This takes a minute as images are loaded, containers start, and health checks pass. Let's verify everything is working:
kubectl get pods -n nexusYou should see four pods (config, api, tracking, gateway) with
STATUS: Running and READY: 1/1. The 1/1 means 1 out of 1 containers in the pod is ready. If you see 0/1, the pod is still starting.To watch the status update in real-time (useful during deployment):
kubectl get pods -n nexus -wPress
Ctrl+C to stop watching. If a pod stays in Pending, CrashLoopBackOff, or ImagePullBackOff status for more than a minute, something's wrong.To troubleshoot a stuck pod, describe it to see detailed events:
kubectl describe pod <pod-name> -n nexusLook at the
Events section at the bottom. Common issues include missing images (forgot to load into Kind), insufficient resources, or application crashes (check logs with kubectl logs).Access Services Locally
Your services are now running inside the Kind cluster, but you can't access them from your browser yet because they're on an internal network. Kind doesn't support LoadBalancer services (which would expose them automatically). Instead, use
port-forward to create a tunnel from your localhost to a pod:# Config Server (port 8071)
kubectl port-forward -n nexus deployment/config 8071:8071
# API Service (port 8080)
kubectl port-forward -n nexus deployment/api 8080:8080
# Tracking Service (port 8090)
kubectl port-forward -n nexus deployment/tracking 8090:8090
# Gateway (port 8072)
kubectl port-forward -n nexus deployment/gateway 8072:8072Each command runs in the foreground, so open a separate terminal tab for each one. The format is
localPort:podPort. For example, 8072:8072 makes the gateway available at localhost:8072.Now test the gateway in your browser or with curl:
http://localhost:8072/config/api/dev
http://localhost:8072/api/<endpoint>The gateway routes requests to other services internally. For example,
/api/* routes to the API service,/tracking/* to the tracking service. This is how microservices communicate in Kubernetes.Service Overview
| Service | Spring App Name | Port | K8s Deployment | Image |
|---|---|---|---|---|
| Config Server | config | 8071 | config | nexus-config:v0.0.4 |
| API | api | 8080 | api | nexus-api:v0.0.4 |
| Tracking | tracking | 8090 | tracking | nexus-tracking:v0.0.4 |
| Gateway | gateway | 8072 | gateway | nexus-gateway:v0.0.4 |
Redeploying After Code Changes
During development, you'll frequently modify code and need to see changes running in Kubernetes. Because Kind uses locally loaded images, you must rebuild, reload, and restart. Here's the complete workflow for updating one service (e.g., the gateway after fixing a bug):
# 1. Rebuild the jar
mvn clean package -pl freightflow-gateway
# 2. Rebuild the Docker image
docker build -t nexus-gateway:v0.0.4 ./freightflow-gateway
# 3. Remove the old image from Kind's cache
docker exec nexus-control-plane crictl rmi nexus-gateway:v0.0.4
# 4. Load the new image
kind load docker-image nexus-gateway:v0.0.4 --name nexus
# 5. Restart the deployment to pick up the new image
kubectl rollout restart deployment/gateway -n nexusTo redeploy all four services at once, use this loop:
mvn clean package \
-pl freightflow-config,freightflow-api,freightflow-tracking,freightflow-gateway
for svc in config api tracking gateway; do
docker build -t nexus-${svc}:v0.0.4 ./freightflow-${svc}
docker exec nexus-control-plane crictl rmi nexus-${svc}:v0.0.4
kind load docker-image nexus-${svc}:v0.0.4 --name nexus
kubectl rollout restart deployment/${svc} -n nexus
doneUseful Commands
Pod & Deployment Management
# Check pod status
kubectl get pods -n nexus
# Describe a pod (for debugging startup issues)
kubectl describe pod <pod-name> -n nexus
# Reapply config and restart
kubectl apply -k k8s/overlays/local/
kubectl rollout restart deployment/api -n nexus
# Delete and recreate all resources
kubectl delete -k k8s/overlays/local/
kubectl apply -k k8s/overlays/local/
# Delete the entire cluster
kind delete cluster --name nexusLog Tailing
# Follow logs for a deployment
kubectl logs -f -n nexus deployment/config
kubectl logs -f -n nexus deployment/api
kubectl logs -f -n nexus deployment/gateway
# View last 100 lines
kubectl logs --tail=100 -n nexus deployment/api
# Follow logs for all pods of a service
kubectl logs -f -l app=api -n nexusCongratulations! You've successfully deployed a complete microservices application to a local Kubernetes cluster.
- A working Kubernetes environment, mirrors production architecture without needing cloud resources or expensive infrastructure
- Four microservices running in pods with proper service discovery, networking, and config management through Spring Cloud Config
- Experience with Kustomize for managing environment-specific configurations, a critical skill for production Kubernetes deployments
- A repeatable deployment workflow you can use for any Spring Boot microservices project, not just this example