Containerization, Docker, and Production Deployment: A Complete Beginner-to-CI/CD Guide
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
YOU (Terminal)
-> Docker Client (docker CLI)
-> Docker Daemon (dockerd)
-> containerd
-> runc
-> Container ProcessWhen 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)
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io
docker --version
docker run hello-worldLinux 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.
# PowerShell (Admin)
wsl --install
wsl --install -d Ubuntu
# Then verify in WSL
docker run hello-world4. 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.
services:
web:
image: nginx
ports:
- "8080:80"
environment:
- APP_ENV=production5. 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.
docker pull python:3.11-slim
docker images
docker rmi python:3.11-slimContainer
A container is a running instance of an image. By default it is ephemeral, so data inside it disappears when removed unless persisted externally.
docker run -d --name my-app python:3.11-slim sleep 3600
docker ps
docker ps -a
docker stop my-app
docker rm my-appDockerfile
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.
docker tag my-app:latest myusername/my-app:v1.0
docker push myusername/my-app:v1.0
docker pull myusername/my-app:v1.0Logs, Metrics, Interactive Shell
docker stats
docker logs my-app
docker logs -f my-app
docker exec -it my-app /bin/bashPorts
docker run -d -p 8080:80 nginx
# localhost:8080 -> container:80Volumes
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 pruneUse named volumes for durable application data, bind mounts for local development, and tmpfs for memory-backed temporary data.
Networking
docker network create my-network
docker run -d --name api --network my-network my-api
docker run -d --name db --network my-network postgres:15Custom bridge networks provide built-in DNS resolution between containers by name, which simplifies service-to-service communication.
Restarting and Rebuilding
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:v26. Hands-On Project: Python Task Manager API
To tie concepts together, build a small Flask API and containerize it end-to-end.
task-manager/
server.py
requirements.txt
Dockerfile
docker-compose.yml
.env
.dockerignoreimport 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')))flask==3.0.0
gunicorn==21.2.0FROM 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"]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/health7. 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.
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:docker compose up -d --build
docker compose ps
docker compose logs -f
docker compose down8. 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.
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)
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 --buildOption B: ECS Fargate (Managed Containers)
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{
"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)
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: 800010. 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.
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@v211. 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.
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.