Skip to main content

Command Palette

Search for a command to run...

Containerizing your App (Blood Pressure Tracker) with Docker & Kubernetes

Published
10 min read
Containerizing your App (Blood Pressure Tracker) with Docker & Kubernetes
O

Detail-oriented and dedicated Cloud/DevOps Engineer with experience in designing, deploying, and managing cloud infrastructure across Azure, AWS and GCP environments. Strong expertise in cybersecurity, system administration, and incident management. Proven history of success in IT support roles, with proficiency in Linux and Windows server administration, virtualisation, identity management, and Active Directory. Committed to enhancing security, optimising system performance, and ensuring the reliability of IT infrastructure.

As a developer who built a blood pressure tracking application, I aimed to simplify and enhance the deployment process for greater reliability. In this comprehensive guide, I'll walk you through containerizing your application with Docker and orchestrating it with Kubernetes.

What We'll Cover

  1. Docker Basics: Containerizing your application

  2. Multi-container Setup: Docker Compose with PostgreSQL

  3. Kubernetes Fundamentals: Orchestration for production

  4. Step-by-step Deployment: From local to cluster

  5. Troubleshooting Common Issues

Prerequisites

  • Basic command line knowledge

  • My blood pressure tracker application code

  • Docker Desktop installed

  • Kubernetes cluster (Minikube, Docker Desktop, or cloud provider)

Part 1: Understanding Containerization

What is Docker?

Think of Docker as a standardized shipping container for software. Just like shipping containers revolutionized cargo transport, Docker containers revolutionize software deployment by packaging your application and all its dependencies into a single, portable unit.

Why Containerize? Consistency: "Works on my machine" becomes "Works everywhere"

  1. Isolation: Applications don't interfere with each other

  2. Portability: Move between development, testing, and production easily

  3. Scalability: Run multiple instances effortlessly

Part 2: Containerizing with Docker

Step 1: Install Docker

Download Docker Desktop from docker.com and install it. Verify installation:

docker --version

Step 2: Create Your Dockerfile

Create Dockerfile in your project root with the correct configuration:

# Use Node.js 18 as base image
FROM node:18-alpine

# Set working directory
WORKDIR /app

# Copy package files first (better caching)
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy application files
COPY . .

# Set default environment variables
ENV DB_USER=postgres
ENV DB_HOST=localhost
ENV DB_NAME=blood_pressure_db
ENV DB_PASSWORD=postgres
ENV DB_PORT=5432
ENV JWT_SECRET=development_secret_key_change_in_production
ENV JWT_EXPIRE=7d
ENV PORT=3000
ENV NODE_ENV=production

# Expose application port
EXPOSE 3000

# Security: Run as non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
RUN chown -R nextjs:nodejs /app
USER nextjs

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000', (res) => { if (res.statusCode !== 200) process.exit(1) }) || process.exit(1)"

# Start application
CMD ["npm", "start"]

Step 3: Create .dockerignore

Create .dockerignore to exclude unnecessary files:

node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.env.local
.env.production
.nyc_output
coverage
.nyc_output
.coverage
.vscode
*.log
Dockerfile
.dockerignore
.docker

Step 4: Build Your Docker Image

# Build the image with a tag
docker build -t blood-pressure-app .

# Verify the image was created
docker images | grep blood-pressure-app

Step 5: Run Your Container

# Run the container with port mapping
docker run -p 3000:3000 blood-pressure-app

# Visit http://localhost:3000 to see your app!

Part 3: Multi-Container Setup with Docker Compose

Your app needs both Node.js and PostgreSQL. Docker Compose manages multiple containers.

Create docker-compose.yml

Create docker-compose.yml with the correct configuration:

version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DB_HOST=postgres
      - DB_USER=bp_user
      - DB_PASSWORD=bp_password
      - DB_NAME=bp_database
      - DB_PORT=5432
      - JWT_SECRET=your_jwt_secret_here_change_this
      - JWT_EXPIRE=7d
      - PORT=3000
      - NODE_ENV=production
    depends_on:
      - postgres
    restart: unless-stopped
    networks:
      - bp-network

  postgres:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=bp_user
      - POSTGRES_PASSWORD=bp_password
      - POSTGRES_DB=bp_database
    volumes:
      - postgres_/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"
    restart: unless-stopped
    networks:
      - bp-network

volumes:
  postgres_

networks:
  bp-network:
    driver: bridge

Create Database Initialization Script

Create init.sql for database setup:

-- Create users table
CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Create bp_readings table
CREATE TABLE IF NOT EXISTS bp_readings (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
    systolic INTEGER NOT NULL,
    diastolic INTEGER NOT NULL,
    reading_date TIMESTAMP NOT NULL,
    notes TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Create indexes for better performance
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_bp_readings_user_id ON bp_readings(user_id);
CREATE INDEX IF NOT EXISTS idx_bp_readings_date ON bp_readings(reading_date);

Run with Docker Compose

# Build and start all services
docker-compose up --build

# Run in background (detached mode)
docker-compose up -d --build

# View logs
docker-compose logs -f

# Stop all services
docker-compose down

Part 4: Introduction to Kubernetes

What is Kubernetes?

Kubernetes (often abbreviated as K8s) is an orchestration platform that automates the deployment, scaling, and management of containerized applications across clusters of hosts.

Think of it as a conductor for an orchestra - it manages multiple containers (musicians) to play in harmony.

Key Kubernetes Concepts

  1. Pod: The smallest deployable unit (like a pod of whales)

  2. Service: Network abstraction for accessing pods

  3. Deployment: Manages pod replicas

  4. ConfigMap: Stores configuration data

  5. Secret: Stores sensitive data securely

Part 5: Kubernetes Setup for Beginners

Install Kubernetes Tools

For Docker Desktop users:

  1. Open Docker Desktop

  2. Go to Settings → Kubernetes

  3. Check "Enable Kubernetes"

  4. Click Apply & Restart

Start Your Local Cluster

# Start Minikube (if not using Docker Desktop)
minikube start

# Check cluster status
kubectl cluster-info

Part 6: Kubernetes Manifests

Create a k8s directory with these files:

Namespace (k8s/namespace.yaml)

apiVersion: v1
kind: Namespace
metadata:
  name: blood-pressure-tracker

ConfigMap (k8s/configmap.yaml)

apiVersion: v1
kind: ConfigMap
metadata:
  name: bp-app-config
  namespace: blood-pressure-tracker

  DB_HOST: "postgres-service"
  DB_PORT: "5432"
  DB_NAME: "bp_database"
  JWT_EXPIRE: "7d"
  PORT: "3000"
  NODE_ENV: "production"

Secret (k8s/secrets.yaml)

apiVersion: v1
kind: Secret
metadata:
  name: bp-app-secrets
  namespace: blood-pressure-tracker
type: Opaque

  # Base64 encoded values
  # echo -n 'bp_user' | base64 = YnBfdXNlcg==
  # echo -n 'bp_password' | base64 = YnBfcGFzc3dvcmQ=
  # echo -n 'your_jwt_secret_here_change_this' | base64 = eW91cl9qd3Rfc2VjcmV0X2hlcmVfY2hhbmdlX3RoaXM=
  DB_USER: YnBfdXNlcg==
  DB_PASSWORD: YnBfcGFzc3dvcmQ=
  JWT_SECRET: eW91cl9qd3Rfc2VjcmV0X2hlcmVfY2hhbmdlX3RoaXM=

PostgreSQL ConfigMap (k8s/postgres-configmap.yaml)

apiVersion: v1
kind: ConfigMap
metadata:
  name: postgres-init-scripts
  namespace: blood-pressure-tracker

  init.sql: |
    CREATE TABLE IF NOT EXISTS users (
        id SERIAL PRIMARY KEY,
        name VARCHAR(100) NOT NULL,
        email VARCHAR(100) UNIQUE NOT NULL,
        password_hash VARCHAR(255) NOT NULL,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );

    CREATE TABLE IF NOT EXISTS bp_readings (
        id SERIAL PRIMARY KEY,
        user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
        systolic INTEGER NOT NULL,
        diastolic INTEGER NOT NULL,
        reading_date TIMESTAMP NOT NULL,
        notes TEXT,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );

    CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
    CREATE INDEX IF NOT EXISTS idx_bp_readings_user_id ON bp_readings(user_id);
    CREATE INDEX IF NOT EXISTS idx_bp_readings_date ON bp_readings(reading_date);

PostgreSQL Deployment (k8s/postgres-deployment.yaml)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres-deployment
  namespace: blood-pressure-tracker
  labels:
    app: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:15-alpine
        ports:
        - containerPort: 5432
        env:
        - name: POSTGRES_USER
          valueFrom:
            secretKeyRef:
              name: bp-app-secrets
              key: DB_USER
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: bp-app-secrets
              key: DB_PASSWORD
        - name: POSTGRES_DB
          value: "bp_database"
        volumeMounts:
        - name: postgres-storage
          mountPath: /var/lib/postgresql/data
        - name: init-scripts
          mountPath: /docker-entrypoint-initdb.d
      volumes:
      - name: postgres-storage
        emptyDir: {}
      - name: init-scripts
        configMap:
          name: postgres-init-scripts
---
apiVersion: v1
kind: Service
metadata:
  name: postgres-service
  namespace: blood-pressure-tracker
spec:
  selector:
    app: postgres
  ports:
    - protocol: TCP
      port: 5432
      targetPort: 5432
  type: ClusterIP

Application Deployment (k8s/app-deployment.yaml)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: bp-app-deployment
  namespace: blood-pressure-tracker
  labels:
    app: bp-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: bp-app
  template:
    metadata:
      labels:
        app: bp-app
    spec:
      containers:
      - name: bp-app
        image: laoluafolami/blood-pressure-app:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 3000
        envFrom:
        - configMapRef:
            name: bp-app-config
        - secretRef:
            name: bp-app-secrets
        readinessProbe:
          httpGet:
            path: /
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 10
        livenessProbe:
          httpGet:
            path: /
            port: 3000
          initialDelaySeconds: 15
          periodSeconds: 20
---
apiVersion: v1
kind: Service
metadata:
  name: bp-app-service
  namespace: blood-pressure-tracker
spec:
  selector:
    app: bp-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000
  type: LoadBalancer

Part 7: Deploying to Kubernetes

Step 1: Push Image to Docker Hub

# Login to Docker Hub
docker login

# Build your image
docker build -t blood-pressure-app .

# Tag with your Docker Hub username
docker tag blood-pressure-app:latest laoluafolami/blood-pressure-app:latest

# Push to Docker Hub
docker push laoluafolami/blood-pressure-app:latest

Step 2: Apply All Manifests

# Apply namespace first
kubectl apply -f k8s/namespace.yaml

# Apply ConfigMaps
kubectl apply -f k8s/configmap.yaml
kubectl apply -f k8s/postgres-configmap.yaml

# Apply Secrets
kubectl apply -f k8s/secrets.yaml

# Apply PostgreSQL
kubectl apply -f k8s/postgres-deployment.yaml

# Apply Application
kubectl apply -f k8s/app-deployment.yaml

Step 3: Monitor Your Deployment

# Check pods
kubectl get pods -n blood-pressure-tracker

# Check services
kubectl get services -n blood-pressure-tracker

# View logs
kubectl logs -n blood-pressure-tracker -l app=bp-app

# Access your app locally
kubectl port-forward -n blood-pressure-tracker service/bp-app-service 3000:80

Part 8: Checking Your Kubernetes Deployment

Complete Health Check Script

Create a simple script to check everything:

#!/bin/bash
echo "=== Checking Blood Pressure Tracker Deployment ==="
echo ""

echo "1. Checking namespace..."
kubectl get namespace blood-pressure-tracker

echo ""
echo "2. Checking pods..."
kubectl get pods -n blood-pressure-tracker

echo ""
echo "3. Checking services..."
kubectl get services -n blood-pressure-tracker

echo ""
echo "4. Checking deployments..."
kubectl get deployments -n blood-pressure-tracker

echo ""
echo "5. Checking app logs (last 10 lines)..."
kubectl logs -n blood-pressure-tracker -l app=bp-app --tail=10

echo ""
echo "6. Checking database logs (last 10 lines)..."
kubectl logs -n blood-pressure-tracker -l app=postgres --tail=10

echo ""
echo "=== Health Check Complete ==="

Access Your Application

# Port forward to access locally
kubectl port-forward -n blood-pressure-tracker service/bp-app-service 3000:80

# Then visit http://localhost:3000

Then visit http://localhost:3000

Part 9: Troubleshooting the issues I encountered

ImagePullBackOff Error

If you see this error:

Error from server (BadRequest): container "bp-app" in pod "bp-app-deployment-xxx" is waiting to start: trying and failing to pull image

Solution : Verify Docker Hub Image

# Make sure your image exists on Docker Hub
docker pull laoluafolami/blood-pressure-app:latest

Then update your deployment:

spec:
  containers:
  - name: bp-app
    image: laoluafolami/blood-pressure-app:latest
  imagePullSecrets:
  - name: docker-config

Database Connection Issues

# Check if postgres service is running
kubectl get service postgres-service -n blood-pressure-tracker

# Test DNS resolution from app pod
kubectl exec -it -n blood-pressure-tracker deployment/bp-app-deployment -- nslookup postgres-service.blood-pressure-tracker.svc.cluster.local

# Check database tables
kubectl exec -it -n blood-pressure-tracker deployment/postgres-deployment -- psql -U bp_user -d bp_database -c "\dt"

Environment Variables Issues

# Check environment variables in pod
kubectl exec -it -n blood-pressure-tracker deployment/bp-app-deployment -- env | grep DB

# Check configmaps
kubectl get configmaps -n blood-pressure-tracker -o yaml

# Check secrets
kubectl get secrets -n blood-pressure-tracker -o yaml

Accessing Minikube Dashboard

minikube dashboard

Benefits of Containerization & Orchestration

Docker Benefits:

  • Consistent environments

  • Easy dependency management

  • Simplified deployment

  • Better resource utilization

Kubernetes Benefits:

  • Automatic scaling

  • Self-healing capabilities

  • Load balancing

  • Rolling updates

  • Multi-cloud deployment

Common Commands Cheat Sheet

Docker

# Build image
docker build -t myapp .

# Run container
docker run -p 3000:3000 myapp

# List containers
docker ps

# Stop container
docker stop <container-id>

Docker Compose

# Start services
docker-compose up

# Start in background
docker-compose up -d

# Stop services
docker-compose down

# View logs
docker-compose logs

Kubernetes

# Apply manifest
kubectl apply -f manifest.yaml

# Get pods
kubectl get pods

# Get services
kubectl get services

# View logs
kubectl logs <pod-name>

# Delete resources
kubectl delete -f manifest.yaml

Conclusion

Containerizing your application with Docker and orchestrating it with Kubernetes transforms how you deploy and manage software. What once required complex server configurations can now be accomplished with simple, portable containers.

The blood pressure tracker application, once a simple local project, can now be deployed anywhere with consistent behavior. Whether you're running it on your local machine, in a data center, or in the cloud, containers ensure your application works the same way everywhere.

Start with Docker for local development, then move to Kubernetes when you're ready for production-grade orchestration. The journey from a simple app to a containerized, orchestrated system is both empowering and essential for modern software development.

You can find the complete source code on my GitHub repository

Additional Resources

  1. Docker Documentation: docs.docker.com

  2. Kubernetes Documentation: kubernetes.io/docs

  3. Docker Hub: hub.docker.com

  4. Minikube: minikube.sigs.k8s.io

Feel free to like and share.

Containerizing your App (Blood Pressure Tracker) with Docker & Kuberne