How to migrate to Kubernetes with zero downtime: a step-by-step guide
A practical guide to moving production workloads to Kubernetes incrementally, with no interruption for your users and no surprises at 3am.
Migrating to Kubernetes can feel like a high-risk operation. Most teams treat it as a “big bang”: stop production, move everything at once, pray it works. There is a better way.
The core principle: migrate incrementally
The trick is to treat the migration as a series of reversible steps rather than a single event. At every step Kubernetes runs alongside your existing infrastructure until you are confident everything works.
Phase 1: Prepare the environment without touching production
Before moving a single workload, you need a staging cluster configured exactly like the one you will run in production.
# Create an EKS cluster with eksctl
eksctl create cluster \
--name evoltix-staging \
--region eu-central-1 \
--nodegroup-name workers \
--node-type t3.medium \
--nodes 2
At this point production is still running exactly as before. You have changed nothing.
Phase 2: Containerise the services
Every service needs its own Dockerfile. The golden rule: one process per container.
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "src/index.js"]
Test the container locally before pushing it to any registry:
docker build -t my-service:latest .
docker run -p 3000:3000 my-service:latest
Phase 3: The first deployment to staging
A basic Kubernetes Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-service
spec:
replicas: 2
selector:
matchLabels:
app: my-service
template:
metadata:
labels:
app: my-service
spec:
containers:
- name: my-service
image: my-registry/my-service:latest
ports:
- containerPort: 3000
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
Do not skip
resources. Without limits, a single pod with a memory leak can take down the whole node.
Phase 4: Cutting traffic over with zero downtime
This is the heart of a downtime-free migration. We use a dual load balancer: 5% of traffic goes to Kubernetes first, the rest stays on the old infrastructure.
On AWS this is implemented with ALB weighted target groups:
resource "aws_lb_listener_rule" "canary" {
listener_arn = aws_lb_listener.main.arn
action {
type = "forward"
forward {
target_group {
arn = aws_lb_target_group.old.arn
weight = 95
}
target_group {
arn = aws_lb_target_group.kubernetes.arn
weight = 5
}
}
}
}
You watch the metrics for 24 to 48 hours. If everything holds, you raise the weight gradually: 5% → 20% → 50% → 100%.
If something breaks, rolling back is setting the weight back to 0. In seconds.
What not to do
- Do not migrate the database at the same time. Stateless services move to Kubernetes first; databases come later, and with far more care.
- Do not ignore health checks. Kubernetes needs to know when a pod is actually ready to receive traffic.
- Do not put secrets in the Deployment’s environment variables. Use Kubernetes Secrets or, better, AWS Secrets Manager with the CSI driver.
What you end up with
Once the migration is done, you have:
- Deployments in minutes instead of hours
- Automatic rollback when a health check fails
- Horizontal scaling with a single command
- Reproducible infrastructure, defined as code
Is your team considering a move to Kubernetes? Tell us about your case — we run this migration for companies of every size.