Deploy a Docker App to Kubernetes: End-to-End Guide
Deploy a complete Docker application to Kubernetes from scratch. Covers pushing images to a registry, writing all YAML manifests, deploying with kubectl, and verifying the running stack.

This is the final lesson of the Docker & Kubernetes Mastery course. Everything you've learned comes together here: building and pushing a Docker image, writing a full set of Kubernetes manifests, deploying a multi-service application, and verifying it's running correctly.
By the end, you'll have a Node.js API and PostgreSQL database running in Kubernetes — fully configured with ConfigMaps, Secrets, Services, and health checks.
The Application We're Deploying
We're deploying a simple Node.js REST API (myapp) that connects to a PostgreSQL database. The stack has three types of Kubernetes resources: PostgreSQL (StatefulSet + Service + PersistentVolumeClaim), the API (Deployment + Service), and Configuration (ConfigMap + Secret).
Final directory structure:
k8s/
configmap.yaml
secret.yaml
postgres-pvc.yaml
postgres-statefulset.yaml
postgres-service.yaml
api-deployment.yaml
api-service.yamlStep 1: Build and Push the Docker Image
Build the Image
docker build -t myapp:1.0 .Tag for a Registry
To deploy to Kubernetes, the image must be in a registry the cluster can pull from. Using Docker Hub:
docker tag myapp:1.0 yourusername/myapp:1.0
docker login
docker push yourusername/myapp:1.0For local development with minikube, skip the registry push and load directly:
minikube image load myapp:1.0For kind:
kind load docker-image myapp:1.0Step 2: Create the Namespace
kubectl create namespace myappKeeping all resources in a dedicated namespace makes management and cleanup easy.
Step 3: Write the Configuration Resources
k8s/configmap.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: myapp-config
namespace: myapp
data:
NODE_ENV: "production"
PORT: "3000"
DATABASE_HOST: "postgres-service"
DATABASE_PORT: "5432"
DATABASE_NAME: "myapp"k8s/secret.yaml:
apiVersion: v1
kind: Secret
metadata:
name: myapp-secrets
namespace: myapp
type: Opaque
stringData:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: changeme-in-production
DATABASE_URL: "postgresql://myuser:changeme-in-production@postgres-service:5432/myapp"Apply them first — Pods that reference them won't start without them:
kubectl apply -f k8s/configmap.yaml
kubectl apply -f k8s/secret.yamlStep 4: Deploy PostgreSQL
Databases need stable storage and stable network identity. Use a StatefulSet rather than a Deployment.
k8s/postgres-pvc.yaml:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
namespace: myapp
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gik8s/postgres-statefulset.yaml:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: myapp
spec:
serviceName: postgres-service
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine
ports:
- containerPort: 5432
envFrom:
- secretRef:
name: myapp-secrets
env:
- name: POSTGRES_DB
valueFrom:
configMapKeyRef:
name: myapp-config
key: DATABASE_NAME
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
exec:
command: ["pg_isready", "-U", "myuser", "-d", "myapp"]
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command: ["pg_isready", "-U", "myuser", "-d", "myapp"]
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: postgres-storage
persistentVolumeClaim:
claimName: postgres-pvck8s/postgres-service.yaml:
apiVersion: v1
kind: Service
metadata:
name: postgres-service
namespace: myapp
spec:
selector:
app: postgres
ports:
- protocol: TCP
port: 5432
targetPort: 5432
type: ClusterIPApply:
kubectl apply -f k8s/postgres-pvc.yaml
kubectl apply -f k8s/postgres-statefulset.yaml
kubectl apply -f k8s/postgres-service.yaml
kubectl rollout status statefulset/postgres -n myappStep 5: Deploy the API
k8s/api-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-api
namespace: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp-api
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
template:
metadata:
labels:
app: myapp-api
spec:
containers:
- name: api
image: yourusername/myapp:1.0
ports:
- containerPort: 3000
envFrom:
- configMapRef:
name: myapp-config
- secretRef:
name: myapp-secrets
resources:
requests:
memory: "128Mi"
cpu: "200m"
limits:
memory: "256Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 15
periodSeconds: 30
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 10k8s/api-service.yaml:
apiVersion: v1
kind: Service
metadata:
name: myapp-api-service
namespace: myapp
spec:
selector:
app: myapp-api
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: LoadBalancer # use NodePort for local developmentApply:
kubectl apply -f k8s/api-deployment.yaml
kubectl apply -f k8s/api-service.yaml
kubectl rollout status deployment/myapp-api -n myappStep 6: Verify the Deployment
# Check all resources in the namespace
kubectl get all -n myapp
# Test with port-forward (works on any local cluster)
kubectl port-forward service/myapp-api-service 8080:80 -n myapp
curl http://localhost:8080/health
# {"status":"ok","database":"connected"}Step 7: Rolling Update
# Build and push new version
docker build -t yourusername/myapp:2.0 .
docker push yourusername/myapp:2.0
# Update the Deployment
kubectl set image deployment/myapp-api api=yourusername/myapp:2.0 -n myapp
kubectl rollout status deployment/myapp-api -n myapp
# Watch Pods roll over
kubectl get pods -n myapp -wStep 8: Rollback if Needed
kubectl rollout undo deployment/myapp-api -n myapp
kubectl rollout status deployment/myapp-api -n myappComplete Apply Order (From Scratch)
kubectl create namespace myapp
kubectl apply -f k8s/configmap.yaml
kubectl apply -f k8s/secret.yaml
kubectl apply -f k8s/postgres-pvc.yaml
kubectl apply -f k8s/postgres-statefulset.yaml
kubectl apply -f k8s/postgres-service.yaml
# Wait for postgres to be ready
kubectl rollout status statefulset/postgres -n myapp
kubectl apply -f k8s/api-deployment.yaml
kubectl apply -f k8s/api-service.yaml
# Verify
kubectl get all -n myappWhat to Learn Next
You've completed the Docker & Kubernetes Mastery course. Natural next steps:
- Ingress and TLS — route external HTTP traffic by hostname and path, with automatic HTTPS via cert-manager
- Helm — the Kubernetes package manager for templatising and sharing manifests
- Horizontal Pod Autoscaler (HPA) — automatically scale replicas based on CPU or custom metrics
- CI/CD integration — automate image builds and deployments with GitHub Actions, ArgoCD, or Flux
- Monitoring and observability — Prometheus, Grafana, and structured logging
Previous: Lesson 9 — ConfigMaps & Secrets | Back to course overview
Part of the Docker & Kubernetes Mastery course.
External references:
