learning

Phase 7: DevOps & Containers (Docker & Kubernetes)

1412 words8 min read
Phase 7: DevOps & Containers (Docker & Kubernetes)
Authors

Welcome to DevOps, the bridge between writing code and keeping it running reliably in the real world.

For decades, the software industry suffered from the "It works on my machine" problem. A developer would write a brilliant Node.js app on their Mac, push it to production on an Ubuntu server, and it would immediately crash because the server had a different version of Node, missing environment variables, or different file permissions.

DevOps, and specifically Containerization, completely eradicated this problem. Let's dive deep into Docker and its big brother, Kubernetes.


1. The Container Revolution: Docker

Before Docker, we used Virtual Machines (VMs). A VM emulates an entire computer, including the guest Operating System (Windows/Linux). VMs are heavy, take minutes to boot, and waste gigabytes of RAM.

Docker Containers are a breakthrough. Instead of virtualizing the hardware to run a whole new OS, containers virtualize the existing OS. A container bundles your code, your dependencies, and your runtime (like Node.js) into a lightweight, isolated box. It takes milliseconds to start and uses almost zero overhead.

If a Docker container works on your Mac, it is mathematically guaranteed to work identically on an AWS Linux server.

The Docker Workflow

1. The Dockerfile (The Blueprint) You write a set of instructions telling Docker how to build your environment.

# Step 1: Start from a lightweight Linux environment with Node.js pre-installed
FROM node:18-alpine

# Step 2: Create a directory inside the container
WORKDIR /app

# Step 3: Copy package.json and install dependencies
COPY package.json ./
RUN npm install

# Step 4: Copy the rest of your source code
COPY . .

# Step 5: Expose the port the app runs on
EXPOSE 3000

# Step 6: Define the command to start the app
CMD ["node", "server.js"]

2. The Image (The Class) You run docker build -t my-app .. Docker executes the blueprint and generates a read-only Image. Think of an Image like a CD-ROM containing your complete application state.

3. The Container (The Object) You run docker run -p 3000:3000 my-app. Docker takes the Image and runs it. This running instance is the Container. You can spin up 100 identical containers from that single image.


2. Docker Compose: Orchestrating Locally

Real apps aren't just one container. A standard app needs a Node.js API container, a React Frontend container, and a PostgreSQL Database container. Starting them all manually with networking commands is tedious.

Docker Compose lets you define your entire multi-container stack in a single YAML file.

# docker-compose.yml
version: '3.8'
services:
  # Our backend API
  api:
    build: ./backend
    ports:
      - "5000:5000"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/mydb
    depends_on:
      - db

  # Our PostgreSQL database
  db:
    image: postgres:14
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb
    volumes:
      - db-data:/var/lib/postgresql/data

# Volumes persist data even if the container is destroyed
volumes:
  db-data:

By simply typing docker-compose up, your entire architecture boots up locally, fully networked together.


3. Enter Kubernetes (K8s): The Conductor

Docker is fantastic for running a few containers on a single machine. But what if you are Netflix, and you need to run 10,000 containers across 500 different servers?

If a server catches fire, who moves the containers to a healthy server? Who routes internet traffic evenly across all 10,000 containers? Who handles zero-downtime updates?

This is where Kubernetes (K8s) comes in. Kubernetes is an open-source container orchestration platform. It is the operating system of the cloud.

The K8s Architecture Analogy

Imagine a massive cruise ship.

  • The Master Node (The Captain): Makes global decisions. It schedules containers and monitors health.
  • Worker Nodes (The Crew): The actual physical/virtual servers that run your containers.
  • Kubelet (The Radio): A tiny agent on every Worker Node that talks to the Captain.

4. Extremely Detailed Step-by-Step Kubernetes Implementation

Kubernetes uses declarative configuration. You don't tell K8s how to do things; you give it a YAML file describing your desired state, and K8s works tirelessly to make reality match that state.

Let's deploy a Node.js web app to Kubernetes step-by-step.

Step 1: The Pod

A Pod is the smallest deployable unit in Kubernetes. You rarely deploy raw containers; you deploy Pods. A Pod usually wraps a single container, giving it a K8s identity and IP address.

Crucial rule: Pods are mortal. They are born, they die, and they are never resurrected. If a Pod crashes, K8s creates a brand new replacement Pod with a brand new IP address.

Step 2: The Deployment

Because Pods die constantly, we never create them manually. We use a Deployment. A Deployment acts as a manager. You tell it: "I want 3 replicas of my web-app running at all times." If a Node crashes and takes down a Pod, the Deployment notices the count dropped to 2, and instantly spins up a new one on a healthy Node.

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-webapp-deployment
spec:
  replicas: 3 # We want 3 identical copies running!
  selector:
    matchLabels:
      app: my-webapp # This is how the deployment tracks its pods
  template:
    metadata:
      labels:
        app: my-webapp # The label stamped on every pod created
    spec:
      containers:
      - name: node-container
        image: my-docker-repo/my-node-app:v1
        ports:
        - containerPort: 3000

Run: kubectl apply -f deployment.yaml -> K8s magically creates 3 Pods.

Step 3: The Service

We have 3 Pods running our app, but they all have random, changing IP addresses. How does the frontend talk to them?

We create a Service. A Service provides a single, static, permanent IP address. It sits in front of the Pods and acts as an internal Load Balancer. When traffic hits the Service, it routes it to one of the 3 healthy Pods using "labels".

service.yaml

apiVersion: v1
kind: Service
metadata:
  name: my-webapp-service
spec:
  selector:
    app: my-webapp # This matches the labels on our Pods!
  ports:
    - protocol: TCP
      port: 80        # The port the Service listens on
      targetPort: 3000 # The port our Node container is running on
  type: ClusterIP # Means this is only reachable from inside the cluster

Run: kubectl apply -f service.yaml

Step 4: The Ingress

Our app is running and networked internally, but it's completely isolated from the internet. To let users in, we need an Ingress.

An Ingress acts as the grand entrance (a reverse proxy) to your cluster. It reads the domain name (e.g., api.mywebsite.com) and routes the external HTTP traffic to the correct internal Service.

ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-webapp-ingress
spec:
  rules:
  - host: "api.mywebsite.com" # When traffic hits this domain...
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: my-webapp-service # ...route it to our Service!
            port:
              number: 80

Run: kubectl apply -f ingress.yaml

The Complete Flow: User visits api.mywebsite.com -> Hits Ingress -> Routed to Service -> Load balanced to one of 3 Pods managed by the Deployment.


5. CI/CD (Continuous Integration / Continuous Deployment)

Now that we have K8s, how do we get our code there automatically? We use CI/CD pipelines (like GitHub Actions).

Continuous Integration (CI): When you push code to GitHub, a pipeline automatically boots up, installs dependencies, and runs your automated tests. If tests fail, the code is blocked from merging.

Continuous Deployment (CD): If tests pass, the pipeline continues:

  1. It logs into DockerHub.
  2. It runs docker build to create a new Image with your new code.
  3. It runs docker push to upload the Image.
  4. It connects to your Kubernetes cluster and updates the Deployment to use the new image version.

Kubernetes then performs a Rolling Update: It starts 1 new Pod, waits for it to be healthy, kills 1 old Pod, and repeats until all 3 Pods are updated. Zero downtime deployments achieved.

# A simplified GitHub Actions CI/CD Pipeline
name: Build and Deploy to K8s
on:
  push:
    branches: [ main ]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    
    - name: Run Unit Tests
      run: npm install && npm test
      
    - name: Build and Push Docker Image
      run: |
        docker build -t myrepo/myapp:${{ github.sha }} .
        docker login -u user -p pass
        docker push myrepo/myapp:${{ github.sha }}
        
    - name: Deploy to Kubernetes
      run: |
        kubectl set image deployment/my-webapp-deployment node-container=myrepo/myapp:${{ github.sha }}

Conclusion

Docker standardized how we package applications, and Kubernetes standardized how we orchestrate them at a massive scale. Combined with CI/CD, these tools form the backbone of modern, robust, and automated software engineering. You are no longer just writing code; you are engineering highly resilient systems.

Tags

#devops#docker#kubernetes#ci-cd