Deploy to Kubernetes
Deploy containerized applications to Kubernetes with production-ready configurations including health checks, resource management, and automated rollouts.
When to Use
- •Deploying new applications to Kubernetes clusters (EKS, GKE, AKS, self-hosted)
- •Migrating from Docker Compose or traditional VMs to container orchestration
- •Implementing zero-downtime rolling updates and rollbacks
- •Managing application configuration and secrets in Kubernetes
- •Setting up multi-environment deployments (dev, staging, production)
- •Creating reusable Helm charts for application distribution
Inputs
- •Required: Kubernetes cluster access (
kubectl cluster-info) - •Required: Container images pushed to registry (Docker Hub, ECR, GCR, Harbor)
- •Required: Application requirements (ports, environment variables, volumes)
- •Optional: TLS certificates for HTTPS ingress
- •Optional: Persistent storage requirements (StatefulSets, PVCs)
- •Optional: Helm CLI for chart-based deployments
Procedure
Step 1: Create Namespace and Resource Quotas
Organize applications into namespaces with resource limits and RBAC.
# Create namespace
kubectl create namespace myapp-prod
# Apply resource quota
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ResourceQuota
metadata:
name: compute-quota
namespace: myapp-prod
spec:
hard:
requests.cpu: "10"
requests.memory: "20Gi"
limits.cpu: "20"
limits.memory: "40Gi"
persistentvolumeclaims: "5"
services.loadbalancers: "2"
---
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: myapp-prod
spec:
limits:
- default:
cpu: "500m"
memory: "512Mi"
defaultRequest:
cpu: "100m"
memory: "128Mi"
type: Container
EOF
# Create service account
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
name: myapp
namespace: myapp-prod
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: myapp-role
namespace: myapp-prod
rules:
- apiGroups: [""]
resources: ["configmaps", "secrets"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: myapp-rolebinding
namespace: myapp-prod
subjects:
- kind: ServiceAccount
name: myapp
namespace: myapp-prod
roleRef:
kind: Role
name: myapp-role
apiGroup: rbac.authorization.k8s.io
EOF
# Verify namespace setup
kubectl get resourcequota -n myapp-prod
kubectl get limitrange -n myapp-prod
kubectl get sa -n myapp-prod
Expected: Namespace created with resource quotas limiting compute and storage. LimitRange sets default CPU/memory requests and limits. ServiceAccount configured with least-privilege RBAC.
On failure: For quota errors, verify cluster has sufficient resources with kubectl describe nodes. For RBAC errors, check cluster-admin permissions with kubectl auth can-i create role --namespace myapp-prod. Use kubectl describe on rejected resources to see quota/limit violations.
Step 2: Configure Application Secrets and ConfigMaps
Externalize configuration and sensitive data using ConfigMaps and Secrets.
# Create ConfigMap from literal values
kubectl create configmap myapp-config \
--namespace=myapp-prod \
--from-literal=LOG_LEVEL=info \
--from-literal=API_TIMEOUT=30s \
--from-literal=FEATURE_FLAGS='{"newUI":true,"betaAPI":false}'
# Create ConfigMap from file
cat > app.properties <<EOF
database.pool.size=20
cache.ttl=3600
retry.attempts=3
EOF
kubectl create configmap myapp-properties \
--namespace=myapp-prod \
--from-file=app.properties
# Create Secret for database credentials
kubectl create secret generic myapp-db-secret \
--namespace=myapp-prod \
--from-literal=username=appuser \
--from-literal=password='sup3rs3cr3t!' \
--from-literal=connection-string='postgresql://db.example.com:5432/myapp'
# Create TLS secret for ingress
kubectl create secret tls myapp-tls \
--namespace=myapp-prod \
--cert=path/to/tls.crt \
--key=path/to/tls.key
# Verify secrets/configmaps
kubectl get configmap -n myapp-prod
kubectl get secret -n myapp-prod
kubectl describe configmap myapp-config -n myapp-prod
For more complex configurations, use YAML manifests:
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: myapp-config
namespace: myapp-prod
data:
nginx.conf: |
server {
listen 8080;
location / {
proxy_pass http://backend:3000;
proxy_set_header Host $host;
}
}
app-config.json: |
{
"logLevel": "info",
"features": {
"authentication": true,
"metrics": true
}
}
---
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: myapp-secret
namespace: myapp-prod
type: Opaque
stringData: # Automatically base64 encoded
api-key: "sk-1234567890abcdef"
jwt-secret: "my-jwt-signing-key"
Expected: ConfigMaps store non-sensitive configuration, Secrets store credentials/keys. Values accessible to Pods via environment variables or volume mounts. TLS secrets properly formatted for Ingress resources.
On failure: For encoding issues, use stringData instead of data in YAML. For TLS secret errors, verify certificate and key format with openssl x509 -in tls.crt -text -noout. For access issues, check ServiceAccount RBAC permissions. View decoded secret with kubectl get secret myapp-secret -o jsonpath='{.data.api-key}' | base64 -d.
Step 3: Create Deployment with Health Checks and Resource Limits
Deploy application with production-ready configuration including probes and resource management.
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: myapp-prod
labels:
app: myapp
version: v1.0.0
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0 # Zero-downtime updates
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
version: v1.0.0
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/metrics"
spec:
serviceAccountName: myapp
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
containers:
- name: myapp
image: myregistry.io/myapp:v1.0.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
protocol: TCP
env:
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: myapp-config
key: LOG_LEVEL
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: myapp-db-secret
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: myapp-db-secret
key: password
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
startupProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 0
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 30 # 5 minutes for slow startup
volumeMounts:
- name: config
mountPath: /etc/myapp
readOnly: true
- name: cache
mountPath: /var/cache/myapp
volumes:
- name: config
configMap:
name: myapp-properties
- name: cache
emptyDir: {}
imagePullSecrets:
- name: registry-credentials
Apply and monitor deployment:
# Apply deployment kubectl apply -f deployment.yaml # Watch rollout status kubectl rollout status deployment/myapp -n myapp-prod # Check pod status kubectl get pods -n myapp-prod -l app=myapp # View pod logs kubectl logs -n myapp-prod -l app=myapp --tail=50 -f # Describe deployment for events kubectl describe deployment myapp -n myapp-prod # Check resource usage kubectl top pods -n myapp-prod -l app=myapp
Expected: Deployment creates 3 replicas with rolling update strategy. Pods pass readiness probes before receiving traffic. Liveness probes restart unhealthy pods. Resource requests/limits prevent OOM kills. Logs show successful application startup.
On failure: For ImagePullBackOff, verify image exists and imagePullSecret is valid with kubectl get secret registry-credentials -o yaml. For CrashLoopBackOff, check logs with kubectl logs pod-name --previous. For probe failures, test endpoints manually with kubectl port-forward and curl localhost:8080/healthz. For OOMKilled pods, increase memory limits or investigate memory leaks.
Step 4: Expose Application with Services and Load Balancers
Create Service resources to expose applications internally and externally.
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: myapp
namespace: myapp-prod
labels:
app: myapp
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
service.beta.kubernetes.io/aws-load-balancer-backend-protocol: "http"
spec:
type: LoadBalancer
selector:
app: myapp
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800
---
# Internal service for inter-service communication
apiVersion: v1
kind: Service
metadata:
name: myapp-internal
namespace: myapp-prod
labels:
app: myapp
spec:
type: ClusterIP
selector:
app: myapp
ports:
- name: http
port: 8080
targetPort: http
protocol: TCP
- name: metrics
port: 9090
targetPort: metrics
protocol: TCP
---
# Headless service for StatefulSets
apiVersion: v1
kind: Service
metadata:
name: myapp-headless
namespace: myapp-prod
spec:
clusterIP: None
selector:
app: myapp
ports:
- name: http
port: 8080
targetPort: http
Apply and test services:
# Apply services
kubectl apply -f service.yaml
# Get service details
kubectl get svc -n myapp-prod
# Wait for LoadBalancer external IP
kubectl get svc myapp -n myapp-prod -w
# Test internal service
kubectl run -it --rm debug --image=curlimages/curl --restart=Never -n myapp-prod -- \
curl http://myapp-internal.myapp-prod.svc.cluster.local:8080/healthz
# Test external LoadBalancer
EXTERNAL_IP=$(kubectl get svc myapp -n myapp-prod -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
curl http://$EXTERNAL_IP/healthz
# Check endpoints
kubectl get endpoints myapp -n myapp-prod
Expected: LoadBalancer Service provisions external LB with public IP/hostname. ClusterIP Service provides stable internal DNS. Endpoints list shows healthy Pod IPs. Curl requests succeed with expected responses.
On failure: For pending LoadBalancer, check cloud provider integration and quotas. For no endpoints, verify Pod labels match Service selector with kubectl get pods --show-labels. For connection refused, verify targetPort matches container port. Use kubectl port-forward to bypass Service layer for debugging.
Step 5: Configure Horizontal Pod Autoscaling
Implement automatic scaling based on CPU/memory or custom metrics.
# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: myapp-hpa
namespace: myapp-prod
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60
- type: Pods
value: 2
periodSeconds: 60
selectPolicy: Min
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 30
- type: Pods
value: 4
periodSeconds: 30
selectPolicy: Max
Install metrics-server if not available:
# Install metrics-server kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml # Verify metrics-server kubectl get deployment metrics-server -n kube-system kubectl top nodes kubectl top pods -n myapp-prod # Apply HPA kubectl apply -f hpa.yaml # Check HPA status kubectl get hpa -n myapp-prod kubectl describe hpa myapp-hpa -n myapp-prod # Generate load to test autoscaling kubectl run -i --tty load-generator --rm --image=busybox --restart=Never -- /bin/sh -c "while sleep 0.01; do wget -q -O- http://myapp.myapp-prod.svc.cluster.local; done" # Watch scaling in another terminal kubectl get hpa myapp-hpa -n myapp-prod --watch
Expected: HPA monitors CPU/memory metrics. When thresholds exceeded, replicas scale up to maxReplicas. When load decreases, replicas scale down gradually (stabilization window prevents flapping). Metrics visible with kubectl top.
On failure: For "unknown" metrics, verify metrics-server is running and Pods have resource requests defined. For no scaling, check current utilization is actually exceeding targets with kubectl top pods. For flapping, increase stabilizationWindowSeconds. For slow scale-up, reduce periodSeconds in scaleUp policies.
Step 6: Package Application with Helm Chart
Create reusable Helm chart for multi-environment deployments.
# Create Helm chart structure
helm create myapp-chart
cd myapp-chart
# Edit Chart.yaml
cat > Chart.yaml <<EOF
apiVersion: v2
name: myapp
description: A Helm chart for MyApp
type: application
version: 1.0.0
appVersion: "1.0.0"
keywords:
- web
- api
maintainers:
- name: Philipp Thoss
email: ph.thoss@example.com
EOF
# Edit values.yaml
cat > values.yaml <<EOF
replicaCount: 3
image:
repository: myregistry.io/myapp
pullPolicy: IfNotPresent
tag: "v1.0.0"
imagePullSecrets:
- name: registry-credentials
serviceAccount:
create: true
name: myapp
service:
type: LoadBalancer
port: 80
targetPort: 8080
ingress:
enabled: false
className: nginx
hosts:
- host: myapp.example.com
paths:
- path: /
pathType: Prefix
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 70
config:
logLevel: info
apiTimeout: 30s
secrets:
dbUsername: appuser
dbPassword: changeme
EOF
# Edit templates/deployment.yaml to use values
# (Helm will template {{ .Values.replicaCount }}, etc.)
# Validate chart
helm lint .
# Dry-run to see rendered manifests
helm install myapp . --dry-run --debug --namespace myapp-prod
# Install chart
helm install myapp . --namespace myapp-prod --create-namespace
# Upgrade with new values
helm upgrade myapp . --namespace myapp-prod --set replicaCount=5
# Rollback if needed
helm rollback myapp 1 --namespace myapp-prod
# List releases
helm list -n myapp-prod
# Uninstall
helm uninstall myapp -n myapp-prod
Expected: Helm chart packages all Kubernetes resources with templated values. Dry-run shows rendered manifests. Install deploys all resources in correct order. Upgrades perform rolling updates. Rollback reverts to previous revision.
On failure: For template errors, run helm template . to render locally without installing. For dependency issues, run helm dependency update. For value override failures, verify YAML path exists in values.yaml. Use helm get manifest myapp -n myapp-prod to see actual deployed resources.
Validation
- • Pods in Running state with all containers ready
- • Readiness probes pass before Pods added to Service endpoints
- • Liveness probes restart unhealthy containers automatically
- • Resource requests and limits prevent OOM kills and node overcommit
- • Secrets and ConfigMaps mounted correctly with expected values
- • Services resolve via DNS (cluster.local) from other Pods
- • LoadBalancer/Ingress accessible from external networks
- • HPA scales replicas up under load and down when idle
- • Rolling updates complete with zero downtime
- • Logs collected and accessible via kubectl logs or centralized logging
Common Pitfalls
- •
Missing readiness probes: Pods receive traffic before fully started. Always implement readiness probes that verify application dependencies.
- •
Insufficient startup time: Fast liveness probes kill slow-starting apps. Use startupProbe with generous failureThreshold for initialization.
- •
No resource limits: Pods consume unlimited CPU/memory causing node instability. Always set requests and limits.
- •
Hardcoded configuration: Environment-specific values in manifests prevent reuse. Use ConfigMaps, Secrets, and Helm values.
- •
Default service account: Pods have unnecessary cluster permissions. Create dedicated ServiceAccounts with minimal RBAC.
- •
No rolling update strategy: Deployments recreate all Pods simultaneously causing downtime. Use RollingUpdate with maxUnavailable: 0.
- •
Secrets in version control: Sensitive data committed to Git. Use sealed-secrets, external-secrets-operator, or vault.
- •
No pod disruption budget: Cluster maintenance drains nodes and breaks service. Create PodDisruptionBudget to ensure minimum available replicas.
Related Skills
- •
setup-docker-compose- Container orchestration fundamentals before Kubernetes - •
containerize-mcp-server- Creating container images for deployment - •
write-helm-chart- Advanced Helm chart development - •
manage-kubernetes-secrets- SealedSecrets and external-secrets-operator - •
configure-ingress-networking- NGINX Ingress and cert-manager setup - •
implement-gitops-workflow- ArgoCD/Flux for declarative deployments - •
setup-container-registry- Image registry integration