All posts
DevOps & Containers

Containerization, Docker, and Production Deployment: A Complete Beginner-to-CI/CD Guide

15 min readby imnb
DockerContainersDevOpsCI/CDAWSKubernetes
Share

A complete beginner-friendly deep dive into containerization, Docker internals, local development, Compose, production deployment on AWS, and zero-downtime CI/CD workflows.

You have probably experienced the classic problem: an app runs perfectly on your machine but fails somewhere else. Containerization solves this by packaging application code with its runtime, dependencies, system tools, and configuration into a portable unit called a container.

1. What Is Containerization?

Containerization is a way to run applications in isolated environments while sharing the host operating system kernel. This gives you consistency across development, testing, and production without the overhead of a full virtual machine for each app.

  • Virtual Machines virtualize hardware and include a full guest OS for each workload.
  • Containers virtualize the OS layer and package only app + dependencies.
  • Containers are usually lighter (MBs) and start much faster than VMs.
  • VMs provide stronger isolation boundaries, while containers focus on efficient process-level isolation.

Under the hood, Linux namespaces isolate what processes can see (PID, network, mount points, users), and cgroups limit what processes can consume (CPU, memory, I/O).

2. What Is Docker?

Docker made container workflows mainstream by standardizing image builds, distribution, and runtime management with a developer-friendly CLI and ecosystem.

Docker Brand vs Docker Technology

  • Docker, Inc. is the company.
  • Docker Engine is the open-source runtime that builds and runs containers.
  • Docker Desktop bundles Engine, CLI, Compose, and local tooling for macOS/Windows/Linux.
  • Docker Hub is a registry for storing and sharing images.
  • Docker CLI is the docker command-line client.

Docker Engine Architecture

text
YOU (Terminal)
  -> Docker Client (docker CLI)
    -> Docker Daemon (dockerd)
      -> containerd
        -> runc
          -> Container Process

When you run docker run, the CLI talks to dockerd via API, dockerd delegates lifecycle operations to containerd, and runc creates the isolated process according to OCI runtime specs.

3. Where Can You Run Docker?

Linux (Native)

bash
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io
docker --version
docker run hello-world

Linux is the most direct environment because containers share the host Linux kernel with no extra virtualization layer.

macOS and Windows

macOS and Windows require a Linux VM layer because containers are Linux-native. On Windows, Docker Desktop with WSL 2 backend is typically the best balance of performance and developer experience.

bash
# PowerShell (Admin)
wsl --install
wsl --install -d Ubuntu
# Then verify in WSL
docker run hello-world

4. Background Knowledge Before Docker

  • Linux basics: navigation, permissions, process management, package tools.
  • Networking: IPs, ports, DNS, localhost, TCP/UDP basics.
  • Client-server model: requests, responses, listening services.
  • YAML syntax: indentation, maps, arrays, quoting.
yaml
services:
  web:
    image: nginx
    ports:
      - "8080:80"
    environment:
      - APP_ENV=production

5. Docker Terminology You Must Know

Image

An image is a read-only template used to create containers. Images are layered and cached, which makes rebuilds faster when unchanged layers are reused.

bash
docker pull python:3.11-slim
docker images
docker rmi python:3.11-slim

Container

A container is a running instance of an image. By default it is ephemeral, so data inside it disappears when removed unless persisted externally.

bash
docker run -d --name my-app python:3.11-slim sleep 3600
docker ps
docker ps -a
docker stop my-app
docker rm my-app

Dockerfile

dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "server.py"]

Daemon, Socket, and CLI

dockerd is the background service. The CLI talks to it through the Docker socket (commonly /var/run/docker.sock on Linux). Be careful exposing or mounting this socket; it effectively grants host-level Docker control.

Registry

A registry stores and distributes images. Docker Hub is default; alternatives include ECR and GHCR.

bash
docker tag my-app:latest myusername/my-app:v1.0
docker push myusername/my-app:v1.0
docker pull myusername/my-app:v1.0

Logs, Metrics, Interactive Shell

bash
docker stats
docker logs my-app
docker logs -f my-app
docker exec -it my-app /bin/bash

Ports

bash
docker run -d -p 8080:80 nginx
# localhost:8080 -> container:80

Volumes

bash
docker volume create my-data
docker run -d -v my-data:/app/data my-app
docker volume ls
docker volume inspect my-data
docker volume prune

Use named volumes for durable application data, bind mounts for local development, and tmpfs for memory-backed temporary data.

Networking

bash
docker network create my-network
docker run -d --name api --network my-network my-api
docker run -d --name db --network my-network postgres:15

Custom bridge networks provide built-in DNS resolution between containers by name, which simplifies service-to-service communication.

Restarting and Rebuilding

bash
docker build -t my-app:v2 .
docker stop my-app
docker rm my-app
docker run -d --name my-app -p 8080:8000 my-app:v2

6. Hands-On Project: Python Task Manager API

To tie concepts together, build a small Flask API and containerize it end-to-end.

text
task-manager/
  server.py
  requirements.txt
  Dockerfile
  docker-compose.yml
  .env
  .dockerignore
python
import os
from datetime import datetime
from flask import Flask, request, jsonify

app = Flask(__name__)
tasks = []
task_id_counter = 1

@app.get('/health')
def health():
    return jsonify({
        'status': 'healthy',
        'timestamp': datetime.utcnow().isoformat(),
        'environment': os.getenv('APP_ENV', 'development')
    })

@app.get('/tasks')
def get_tasks():
    return jsonify({'tasks': tasks, 'count': len(tasks)})

@app.post('/tasks')
def create_task():
    global task_id_counter
    data = request.get_json() or {}
    if 'title' not in data:
        return jsonify({'error': 'Title is required'}), 400
    task = {
        'id': task_id_counter,
        'title': data['title'],
        'description': data.get('description', ''),
        'completed': False,
        'created_at': datetime.utcnow().isoformat()
    }
    tasks.append(task)
    task_id_counter += 1
    return jsonify(task), 201

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=int(os.getenv('PORT', '8000')))
text
flask==3.0.0
gunicorn==21.2.0
dockerfile
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=10s --retries=3 CMD curl -f http://localhost:8000/health || exit 1
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "server:app"]
bash
docker build -t task-manager:v1 .
docker run -d --name task-api -p 8080:8000 -e APP_ENV=development --restart=unless-stopped task-manager:v1
docker logs -f task-api
curl http://localhost:8080/health

7. Docker Compose for Multi-Container Apps

Compose makes multi-service stacks declarative and repeatable. Instead of long docker run commands, define services in one YAML file.

yaml
version: "3.9"

services:
  api:
    build: .
    ports:
      - "8080:8000"
    environment:
      - APP_ENV=development
      - DATABASE_URL=postgresql://admin:secret@db:5432/tasks
    depends_on:
      - db
      - cache
    restart: unless-stopped

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: tasks
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql/data

  cache:
    image: redis:7-alpine

volumes:
  pgdata:
bash
docker compose up -d --build
docker compose ps
docker compose logs -f
docker compose down

8. Local Development Workflow

  • Install Docker Desktop and verify docker --version and docker compose version.
  • Use WSL 2 backend on Windows for near-native Linux behavior.
  • Use .env files or --env-file for environment variables.
  • Avoid baking secrets into Docker images.
bash
docker build --no-cache -t my-app .
docker build --build-arg VERSION=2.0 -t my-app .
docker buildx build --platform linux/amd64,linux/arm64 -t my-app .

9. Production Deployment on AWS

Option A: EC2 (Simple and Manual)

bash
ssh -i my-key.pem ec2-user@<your-ec2-ip>
sudo yum update -y
sudo yum install -y docker
sudo systemctl enable --now docker
git clone https://github.com/youruser/task-manager.git
cd task-manager
docker compose up -d --build

Option B: ECS Fargate (Managed Containers)

bash
aws ecr create-repository --repository-name task-manager
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin <account-id>.dkr.ecr.us-east-1.amazonaws.com
docker tag task-manager:v1 <account-id>.dkr.ecr.us-east-1.amazonaws.com/task-manager:v1
docker push <account-id>.dkr.ecr.us-east-1.amazonaws.com/task-manager:v1
json
{
  "family": "task-manager",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "containerDefinitions": [
    {
      "name": "task-api",
      "image": "<account-id>.dkr.ecr.us-east-1.amazonaws.com/task-manager:v1",
      "portMappings": [{ "containerPort": 8000 }],
      "healthCheck": {
        "command": ["CMD-SHELL", "curl -f http://localhost:8000/health || exit 1"]
      }
    }
  ]
}

Option C: EKS (Kubernetes)

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: task-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: task-api
  template:
    metadata:
      labels:
        app: task-api
    spec:
      containers:
        - name: task-api
          image: <account-id>.dkr.ecr.us-east-1.amazonaws.com/task-manager:v1
          ports:
            - containerPort: 8000
---
apiVersion: v1
kind: Service
metadata:
  name: task-api-service
spec:
  type: LoadBalancer
  selector:
    app: task-api
  ports:
    - port: 80
      targetPort: 8000

10. CI/CD with GitHub Actions

A robust CI/CD pipeline runs tests, builds and pushes images, and deploys through rolling updates so users never hit downtime.

yaml
name: Build and Deploy to ECS
on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - run: pip install -r requirements.txt
      - run: pytest tests/ -v

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
      - uses: aws-actions/amazon-ecr-login@v2
      - run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$GITHUB_SHA .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$GITHUB_SHA

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
      - uses: aws-actions/amazon-ecs-deploy-task-definition@v2

11. Security and Hardening

  • Run containers as non-root users.
  • Scan images for CVEs (for example, Trivy) as part of CI.
  • Do not hardcode secrets in Dockerfiles or source control.
  • Prefer read-only filesystems and drop unnecessary Linux capabilities.
  • Use multi-stage builds to keep production images minimal.
dockerfile
FROM python:3.11-slim
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
USER appuser
CMD ["python", "server.py"]

12. Quick Command Reference

  • Build: docker build -t name .
  • Run: docker run -d --name n image
  • Logs: docker logs -f name
  • Exec shell: docker exec -it name bash
  • Compose up: docker compose up -d --build
  • Compose down: docker compose down
  • Cleanup: docker system prune -a

Conclusion

Containerization gives you reproducible environments, Docker gives you ergonomic tooling, and CI/CD turns deployments into a safe repeatable process. Start with one service, containerize it well, add Compose, then automate your release pipeline. Build, break, iterate, and your deployment confidence will grow fast.

Keep reading