How to Deploy a Node.js App on Kubernetes With Minikube (2026)
Step-by-step guide to deploying a Node.js application on Kubernetes using Minikube in 2026. Covers Dockerfile, Deployment YAML, Service config, and exposing your app.
Get more content like this on Telegram!
Daily AI tips, notes & resources — free
I did my first Kubernetes deployment by copying YAML from StackOverflow and hoping it worked. It didn't. I didn't understand what any of the fields meant and couldn't debug when things went wrong.
This post is what I wish I'd had back then. We'll build a real Node.js app, containerise it, and deploy it to a local Kubernetes cluster with Minikube — touching every piece of the stack and actually understanding what each file does.
By the end, you'll have a running deployment with load balancing and be able to roll updates without downtime. Let's go.
Prerequisites
You'll need:
- Node.js installed locally (for building the app)
- Docker Desktop — install guide
- Minikube — install guide
- kubectl — usually bundled with Docker Desktop or Minikube
Verify everything is working:
node --version # v20.x or later
docker --version # Docker version 26.x
minikube version # minikube version: v1.33.x
kubectl version --client # Client Version: v1.30.x
If you're new to Docker, the Docker tutorial for beginners covers container fundamentals before jumping into Kubernetes.
Step 1: Build the Node.js App
We'll build a simple REST API. Nothing fancy — the focus is the deployment, not the app logic.
Create a project directory:
mkdir node-k8s-demo && cd node-k8s-demo
npm init -y
npm install express
src/index.js:
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
const APP_VERSION = process.env.APP_VERSION || '1.0';
app.use(express.json());
app.get('/', (req, res) => {
res.json({
message: 'Hello from Kubernetes!',
version: APP_VERSION,
hostname: require('os').hostname(), // This will be the pod name
timestamp: new Date().toISOString()
});
});
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT} — version ${APP_VERSION}`);
});
Notice the hostname field — in Kubernetes, os.hostname() returns the Pod name. This lets us see which pod served each request when we load-balance later.
Test it locally:
node src/index.js
curl http://localhost:3000
# {"message":"Hello from Kubernetes!","version":"1.0","hostname":"my-macbook",...}
Step 2: Write the Dockerfile
Dockerfile:
FROM node:20-alpine
WORKDIR /app
# Copy dependency files first for layer caching
COPY package*.json ./
RUN npm ci --only=production
# Copy app source
COPY src/ ./src/
# Don't run as root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 3000
CMD ["node", "src/index.js"]
.dockerignore:
node_modules
.git
*.log
.env
I'm using npm ci instead of npm install here. It's faster, strictly follows package-lock.json, and is better suited for production builds. Small thing, but worth the habit.
Step 3: Start Minikube and Configure Docker
Start Minikube:
minikube start --driver=docker --cpus=2 --memory=4096
This takes a couple of minutes on first run. When it finishes:
kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# minikube Ready control-plane 1m v1.30.0
Now here's a Minikube trick that saves a lot of confusion. Normally you'd push your Docker image to a registry (Docker Hub, ECR, etc.) and pull it from there. For local Minikube development, you can point your Docker CLI at Minikube's internal Docker daemon instead:
eval $(minikube docker-env)
After this, any docker build command builds the image inside Minikube directly. No registry needed. This only applies to your current terminal session.
Build the image:
docker build -t node-k8s-app:1.0 .
Verify it exists inside Minikube:
docker images | grep node-k8s-app
# node-k8s-app 1.0 abc123def456 30 seconds ago 118MB
Step 4: Create the Kubernetes Deployment
Create a k8s/ directory for your Kubernetes manifests:
mkdir k8s
k8s/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: node-app
labels:
app: node-app
spec:
replicas: 3
selector:
matchLabels:
app: node-app
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: node-app
spec:
containers:
- name: node-app
image: node-k8s-app:1.0
imagePullPolicy: Never # Use local image, don't try to pull from registry
ports:
- containerPort: 3000
env:
- name: PORT
value: "3000"
- name: APP_VERSION
value: "1.0"
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "200m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 15
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
Let me explain the fields that matter:
replicas: 3 — Run 3 instances of the pod. Kubernetes distributes them and keeps 3 running at all times.
imagePullPolicy: Never — Critical for Minikube with local images. Without this, Kubernetes tries to pull from Docker Hub and fails because there's no remote image.
Rolling update strategy — maxUnavailable: 0 means Kubernetes never takes a pod down before a new one is ready. Zero downtime. maxSurge: 1 means it creates one extra pod during the update.
Resource requests vs limits — Requests are what the scheduler uses to decide where to place pods. Limits are the hard cap. Setting both is important in production — without limits, one bad pod can consume all node resources.
Liveness vs readiness probes — Liveness tells Kubernetes when to restart a pod (it's unhealthy). Readiness tells Kubernetes when a pod is ready to receive traffic. I've seen teams forget readiness probes and then wonder why their rolling updates cause brief 502 errors.
Apply it:
kubectl apply -f k8s/deployment.yaml
Watch the pods come up:
kubectl get pods -w
# NAME READY STATUS RESTARTS AGE
# node-app-6d8f7b9c4-5kxlm 1/1 Running 0 15s
# node-app-6d8f7b9c4-9fqvn 1/1 Running 0 15s
# node-app-6d8f7b9c4-xr2pt 1/1 Running 0 15s
All three pods running. Let's access them.
Step 5: Create the Service
Pods have ephemeral IP addresses that change when pods restart. A Service provides a stable endpoint that load-balances across all matching pods.
k8s/service.yaml:
apiVersion: v1
kind: Service
metadata:
name: node-app-service
spec:
selector:
app: node-app
ports:
- port: 80
targetPort: 3000
protocol: TCP
type: NodePort
We're using NodePort because it works on Minikube for local access. In production on AWS/GCP/Azure, you'd use LoadBalancer instead, and the cloud provider creates an actual load balancer.
Apply it:
kubectl apply -f k8s/service.yaml
kubectl get service node-app-service
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# node-app-service NodePort 10.96.147.203 <none> 80:31847/TCP 10s
Step 6: Expose and Test
With NodePort, you can get the URL from Minikube directly:
minikube service node-app-service --url
# http://192.168.49.2:31847
Test it:
# Hit it multiple times and watch the hostname change
for i in 1 2 3 4 5; do
curl -s http://192.168.49.2:31847 | python3 -m json.tool
done
You'll see different hostname values in the response — proof that requests are being load-balanced across all three pods.
Step 7: Roll Out an Update
Let's update the app to version 2.0. Change APP_VERSION in src/index.js:
Actually, let's make a real change. Update the response in src/index.js:
app.get('/', (req, res) => {
res.json({
message: 'Hello from Kubernetes — version 2!',
version: APP_VERSION,
hostname: require('os').hostname(),
timestamp: new Date().toISOString(),
newFeature: true // added in v2
});
});
Build the new image:
# Make sure you're still using Minikube's Docker daemon
eval $(minikube docker-env)
docker build -t node-k8s-app:2.0 .
Update the Deployment to use the new image:
kubectl set image deployment/node-app node-app=node-k8s-app:2.0
Watch the rolling update:
kubectl rollout status deployment/node-app
# Waiting for deployment "node-app" rollout to finish: 1 out of 3 new replicas have been updated...
# Waiting for deployment "node-app" rollout to finish: 2 out of 3 new replicas have been updated...
# Waiting for deployment "node-app" rollout to finish: 2 old replicas are pending termination...
# deployment "node-app" successfully rolled out
During this entire process, the service kept serving requests — new requests went to v2 pods, old requests finished on v1 pods. No downtime.
If the new version had a bug:
kubectl rollout undo deployment/node-app
That's it. Instant rollback to the previous version.
Step 8: ConfigMaps for Configuration
Hardcoding config in YAML is fine for demos but bad in practice. Use ConfigMaps:
k8s/configmap.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: node-app-config
data:
APP_VERSION: "2.0"
LOG_LEVEL: "info"
NODE_ENV: "production"
Update the Deployment to reference it:
env:
- name: PORT
value: "3000"
envFrom:
- configMapRef:
name: node-app-config
Apply both:
kubectl apply -f k8s/configmap.yaml
kubectl apply -f k8s/deployment.yaml
For sensitive config (passwords, API keys), use Secrets instead of ConfigMaps. Secrets work identically but the data is base64-encoded and handled more carefully by Kubernetes.
Minikube Dashboard
Minikube includes a web dashboard for visual cluster management:
minikube dashboard
This opens a browser with a full visual view of your deployments, pods, services, and logs. Useful for learning and debugging.
Comparison: Local Dev Options
| Tool | Cluster Nodes | Resources | Best For |
|---|---|---|---|
| Minikube | 1 (multi with profile) | ~2GB RAM | Learning, testing |
| kind (K8s in Docker) | Multiple | Lower | CI/CD, multi-node testing |
| k3s/k3d | Multiple | Very low | Resource-constrained, edge |
| Docker Desktop K8s | 1 | Integrated | Mac/Windows developers |
Going Further
Now that you have a working Kubernetes deployment locally, the next step is automation. You don't want to run kubectl apply manually every time you push code — that's what CI/CD pipeline best practices covers. GitHub Actions can build your Docker image, push it to a registry, and update your Kubernetes deployment automatically on every merge.
For monitoring this deployment, Prometheus and Grafana setup shows you how to add observability to your cluster — essential for production.
If you're comparing back-end languages for what you deploy into Kubernetes, Node.js vs Go vs Python is worth reading. Go and Python apps containerise slightly differently from Node.js.
For the infrastructure layer (VPCs, security groups, the cluster itself), Terraform vs Pulumi vs CloudFormation explains how to manage that as code.
Conclusion
You just went from a blank directory to a running Kubernetes deployment with load balancing, health checks, zero-downtime updates, and rollback capability. That's the full core of what Kubernetes does for applications.
The files we created — Deployment, Service, ConfigMap — are the three you'll use in almost every real Kubernetes application. Everything else (Ingress, StatefulSets, Jobs, CronJobs) follows the same pattern: describe what you want in YAML, apply it, and Kubernetes makes it happen.
Keep experimenting on Minikube. Break things deliberately and use kubectl describe and kubectl logs to figure out why. That's genuinely the fastest way to get comfortable with Kubernetes.
FAQ
What is Minikube and why use it for local Kubernetes? Minikube runs a single-node Kubernetes cluster inside a VM or container on your local machine. It's the official tool for learning and testing Kubernetes locally without cloud costs. You get the full Kubernetes API — Deployments, Services, Ingress, ConfigMaps — all running on your laptop. It supports multiple drivers including Docker, VirtualBox, and Hyper-V.
Can I use the same YAML files from Minikube in production? Yes, with minor adjustments. The Deployment and ConfigMap YAML files are identical. The Service type will change — Minikube uses NodePort for local access while production typically uses LoadBalancer or Ingress. Resource requests and limits might need tuning. The image tag will also change since production pulls from a real registry rather than Minikube's local registry.
How do I update a running deployment in Kubernetes?
Build and tag a new image version, push it to your registry, then update the image in your deployment: kubectl set image deployment/node-app node-app=myapp:2.0. Kubernetes performs a rolling update automatically. You can monitor progress with kubectl rollout status deployment/node-app. If something breaks, roll back instantly with kubectl rollout undo deployment/node-app.
Frequently Asked Questions
AiTechWorlds Team
✓ Verified WriterThe AiTechWorlds team is passionate about AI, technology, and education. We create high-quality, research-backed content to help you learn, grow, and succeed in the modern digital world.
Related Articles
How to Use Docker Compose for Local Dev (Node.js + PostgreSQL)
Set up a full local dev environment with Docker Compose, Node.js, PostgreSQL, and pgAdmin. Includes .env config, named volumes, healthchecks, and common error fixes.
5 GraphQL Resolver Best Practices (DataLoader, Error Handling)
Write efficient GraphQL resolvers that don't hammer your database. DataLoader N+1 fix, error handling patterns, auth in context, and resolver performance comparison.
7 Common API Security Vulnerabilities (and How to Fix Them)
Real API security vulnerabilities from the OWASP API Top 10 — with working code fixes, risk levels, and testing tools so you can protect your APIs today.
How to Run AutoGPT on a VPS for 24/7 Autonomous Operation
Deploy AutoGPT on a VPS for round-the-clock operation. Covers VPS selection, systemd setup, tmux persistence, monitoring, and cost comparison across providers.