DevOpsKubernetesDocker

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.

TT
Daniel Brooks
5 min read
Deploy a Docker App to Kubernetes: End-to-End Guide

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:

text
k8s/
  configmap.yaml
  secret.yaml
  postgres-pvc.yaml
  postgres-statefulset.yaml
  postgres-service.yaml
  api-deployment.yaml
  api-service.yaml

Step 1: Build and Push the Docker Image

Build the Image

bash
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:

bash
docker tag myapp:1.0 yourusername/myapp:1.0
docker login
docker push yourusername/myapp:1.0

For local development with minikube, skip the registry push and load directly:

bash
minikube image load myapp:1.0

For kind:

bash
kind load docker-image myapp:1.0

Step 2: Create the Namespace

bash
kubectl create namespace myapp

Keeping all resources in a dedicated namespace makes management and cleanup easy.


Step 3: Write the Configuration Resources

k8s/configmap.yaml:

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:

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:

bash
kubectl apply -f k8s/configmap.yaml
kubectl apply -f k8s/secret.yaml

Step 4: Deploy PostgreSQL

Databases need stable storage and stable network identity. Use a StatefulSet rather than a Deployment.

k8s/postgres-pvc.yaml:

yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
  namespace: myapp
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi

k8s/postgres-statefulset.yaml:

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-pvc

k8s/postgres-service.yaml:

yaml
apiVersion: v1
kind: Service
metadata:
  name: postgres-service
  namespace: myapp
spec:
  selector:
    app: postgres
  ports:
    - protocol: TCP
      port: 5432
      targetPort: 5432
  type: ClusterIP

Apply:

bash
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 myapp

Step 5: Deploy the API

k8s/api-deployment.yaml:

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: 10

k8s/api-service.yaml:

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 development

Apply:

bash
kubectl apply -f k8s/api-deployment.yaml
kubectl apply -f k8s/api-service.yaml
kubectl rollout status deployment/myapp-api -n myapp

Step 6: Verify the Deployment

bash
# 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

bash
# 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 -w

Step 8: Rollback if Needed

bash
kubectl rollout undo deployment/myapp-api -n myapp
kubectl rollout status deployment/myapp-api -n myapp

Complete Apply Order (From Scratch)

bash
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 myapp

What 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: