12 Things That Cut Our Kubernetes Deploys From Minutes to Seconds
Translated from the original Korean post. 한국어 원문 보기 →
When Deploys Start Creeping
Run Kubernetes long enough and you'll hit the same pattern. Deploys that took five minutes now take ten. Then fifteen. The problem isn't that it jumped — it's that it crept. You add one dependency to the image. You tweak a line of probe config. You bump the replica count. Each change is trivial. Then a hotfix needs to go out and your hands are shaking.
Deploy time isn't one problem with one cause. It's the sum of inefficiencies scattered across the whole pipeline: image build, registry pull, scheduling, probe checks, rollout. Fix one stage and you won't feel a thing. Here's what I've pulled apart, stage by stage, across a few projects over the past several months.

1. Shrink the Image, Ruthlessly
The image is always the first thing I touch. Simple reason: big images pull slowly, and every node pays that cost. Spin up new pods across ten nodes and you've downloaded that fat image ten times.
Multi-stage builds plus an Alpine base go a long way here.
# Before: 1GB 크기의 무거운 이미지
# After: 100MB 이하로 압축
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./
EXPOSE 3000
CMD ["npm", "start"]
On one project, switching an Ubuntu base to Alpine cut pull time by nearly 70%. Peel through the layers with something like dive and you'll find more junk than you expected riding along from the build. Build caches, test artifacts, docs — all baked into the final image. It happens constantly.
2. Tune Your Health Probes Properly
Readiness and liveness probes are load-bearing for deploy stability, and yet plenty of teams ship the defaults untouched. Get them wrong and you either kill healthy apps or route traffic to dead ones.
Two things matter. Match the initial delay to how long your app actually takes to warm up, and keep the check interval short.
spec:
containers:
- name: app
image: myapp:v1
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30 # 앱 시작 시간 고려
periodSeconds: 10 # 빠른 감지
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5 # 준비 상태 빠른 체크
periodSeconds: 5
I also keep seeing liveness and readiness pointed at the same endpoint. They ask different questions. Readiness asks "can I take traffic?" Liveness asks "am I alive or dead?" For something with a long warmup like a JVM app, a bad initialDelaySeconds means perfectly healthy pods restart in a loop and the deploy never finishes. I heard about an e-commerce team that halved its deploy failure rate on probe tuning alone. Entirely believable.
3. Set Requests and Limits Based on Reality
Measure actual usage with kubectl top first, then set your resources. Scheduling gets faster. Oversized requests leave the scheduler hunting for a node that fits; undersized ones pack the node until everything crawls.
resources:
requests:
cpu: "250m" # 실제 평균 사용량 기준
memory: "256Mi"
limits:
cpu: "500m" # 버스트 트래픽 고려
memory: "512Mi"
Keep the roles straight: requests drive scheduling, limits cap runtime. Pinning both to the same value is stable but leaves you running tight on resources. Multiple teams report pod placement getting 40% faster once these are dialed in.
4. Let HPA Do the Scaling
The Horizontal Pod Autoscaler replaces the person who used to manually bump replicas when traffic spiked. Target 50–70% CPU utilization as a starting point.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: myapp-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
minReplicas: 2 # 콜드 스타트 방지
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
HPA isn't magic, though. Scaling out costs you the time it takes a new pod to start and pass its probes. By the time it's ready, the traffic already hit. That's why you keep minReplicas at 2 or higher — no cold start. If CPU alone doesn't cut it, custom metrics or KEDA are worth a look.
5. Optimize the Rolling Update Strategy
For zero-downtime deploys the key setting is maxUnavailable: 0. Bring up the new pod before killing the old one. If speed matters more, raise maxSurge to swap more pods at once.
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0 # 무중단 보장
These two trade off against each other. maxUnavailable: 0 is safe, but replacement crawls forward one slot at a time. A bigger maxSurge is fast, but you're holding more resources during the swap. Match it to the service: pick safety for finance workloads where zero downtime is non-negotiable, speed for internal services.
6. Automate Deploys with GitOps
Bring in a GitOps tool like ArgoCD or Flux and merging a PR is the deploy. No human typing kubectl apply.
This isn't just about convenience. With manual commands, who changed what and when lives in someone's head and their terminal history. With GitOps, the cluster's state is declared in Git, so your deploy history is the commit log. Rolling back means reverting to an earlier commit. Plenty of teams report big MTTR drops after adopting GitOps — less because things got faster, more because "what do we revert to" became obvious.
7. Optimize Node Types and Affinity
Moving to ARM Graviton instances can cut cost while giving you 20–40% better performance.
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-type
operator: In
values: ["graviton"]
The catch: going ARM means building multi-arch images. Push an amd64 image and scheduling works fine, but the pod won't start. Chase the cost savings without checking and you're rebuilding your pipeline. And affinity is a scheduling constraint by nature — tighten it too far and pods sit in Pending because there's nowhere to put them.

8. Speed Up Image Pulls
Step 1 shrank the image. This one fixes the path it travels. Pre-pull images onto nodes with a DaemonSet or set up regional registry mirroring and pull time drops hard. Setting imagePullPolicy: IfNotPresent so you stop re-downloading images you already have is table stakes.
When the registry lives outside your region, a big chunk of pull time bleeds into network round trips. That's why you hear stories like a globally distributed team cutting pull time from two minutes to twenty seconds with ECR mirroring. Making the image smaller and keeping it close aren't separate wins — they're two sides of the same problem.
9. Isolate with Namespaces and Resource Quotas
Split dev, staging, and prod into namespaces and stop resource contention with ResourceQuota. Deploy performance gets predictable.
apiVersion: v1
kind: ResourceQuota
metadata:
name: compute-quota
spec:
hard:
requests.cpu: "10"
requests.memory: 20Gi
limits.cpu: "20"
limits.memory: 40Gi
Quotas don't make anything faster directly. They're a guardrail so one team going off the rails doesn't drag down everyone else's deploys. Share a cluster across teams and somebody's load test will eventually stall the team next door. I've seen it.
10. Simplify Networking and Storage
Run a high-performance CNI like Calico or Cilium, and strip unnecessary PV mounts off stateless apps. That alone speeds up deploys. Storage attach takes longer than people think, so a volume you didn't need delays pod startup by exactly that much. Worth checking whether you've been reflexively attaching PVCs to stateless workloads.
11. Externalize Config with ConfigMap/Secret
Pull per-environment config into a ConfigMap and you can deploy without rebuilding the image.
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
LOG_LEVEL: "info"
DB_HOST: "postgres.cluster"
Bake config into the image and changing one log level means running the entire build pipeline again. Stack that up over time and your deploy cycle stretches by multiples. The more config-heavy the app, the more you feel the difference.
12. Monitor, Then Keep Improving
If you don't measure it, you won't notice it getting slower. That creeping deploy time I opened with? Tracking the number is the only way to catch it. Watch deploy time in Prometheus and Grafana, alert on anything over two minutes, and regressions surface early.
| 메트릭 | 목표값 | 모니터링 도구 |
|---|---|---|
| 배포 시간 | < 2분 | Grafana |
| 이미지 풀 시간 | < 30초 | kubectl describe |
| 파드 시작 시간 | < 15초 | Prometheus |
| 롤아웃 성공률 | > 95% | ArgoCD/Flux |
Deploy time you cut once will creep right back if you leave it alone. Teams that check in on it regularly are the ones that hold their gains. No surprise there.

Small Changes, Stacked
Working through these one at a time, what struck me is that deploy speed isn't a single heroic optimization. It's clearing out inefficiency stage by stage. Item 1 and item 8 turned out to be the same problem viewed twice, and a lot of these supposedly separate items sit on the same pipeline.
The biggest change wasn't the deploy time number. It was that my hands don't shake anymore when a hotfix has to go out. Maybe that's the real value of fast deploys — not the speed, but the slack it buys you.
Was this post helpful?
One click helps me write the next one