How to Use Docker: Containerization with Compose, Kubernetes & CI/CD
Key Takeaways
- Docker containers are lightweight virtual environments that share the host OS kernel, reducing overhead compared to VMs.
- Docker Compose simplifies multi-container setups with a single YAML file.
- Kubernetes orchestrates containers across clusters, handling scaling and recovery automatically.
- CI/CD pipelines with Docker can cut deployment time from hours to minutes when done right.
---
Getting Started with Docker: What You Actually Need
Docker lets you package software into standardized units called containers. Unlike virtual machines that each carry their own OS, containers share the host OS kernel. This means a container might run in 50 milliseconds while a VM takes 30 seconds to boot. I’ve seen teams run 10 containers on a single laptop with 8GB RAM that would choke on 3 VMs.
First, install Docker Desktop from [docker.com](https://www.docker.com/products/docker-desktop/). It’s free for personal use and includes the CLI, Docker Compose, and a local Kubernetes cluster. On Linux, you can just install the engine with `sudo apt install docker.io` on Ubuntu.
Your First Container: The Hello World of Docker
Open a terminal and run:
```bash
docker run hello-world
```
This downloads a tiny image (about 13KB compressed) and runs it. You’ll see a message explaining Docker’s workflow. If you get a permission error on Linux, add your user to the docker group: `sudo usermod -aG docker $USER` then log out and back in.
Now let’s run an actual web server:
```bash
docker run -d -p 8080:80 nginx:alpine
```
- `-d` runs in background
- `-p 8080:80` maps your host’s port 8080 to the container’s port 80
- `nginx:alpine` is a lightweight Nginx image (about 22MB vs 180MB for full Nginx)
Open http://localhost:8080 and you’ll see the Nginx welcome page.
Docker Compose: Managing Multi-Container Apps
When your app needs a database, cache, and backend, running containers individually gets messy. Docker Compose lets you define everything in a single YAML file.
Create a `docker-compose.yml` file:
```yaml
version: '3.8'
services:
web:
image: nginx:alpine
ports:
- "8080:80"
db:
image: postgres:13-alpine
environment:
POSTGRES_PASSWORD: example
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
```
Run `docker-compose up -d`. This pulls both images, starts them, and creates a named volume for PostgreSQL data. To stop everything: `docker-compose down`. I use this pattern daily for local development — it’s saved me hours of manual setup.
Kubernetes Basics: When One Server Isn’t Enough
Kubernetes (K8s) manages containers across multiple machines. Docker Desktop includes a single-node Kubernetes cluster you can enable in settings (under Kubernetes tab).
First, create a deployment. Save this as `deployment.yaml`:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
```
Apply it: `kubectl apply -f deployment.yaml`. Then expose it as a service:
```bash
kubectl expose deployment nginx-deployment --type=NodePort --port=80
```
Now run `kubectl get services` – you’ll see a port like 31234. Access it at http://localhost:31234. Kubernetes keeps 3 containers running – if one dies, it spins up a replacement automatically.
Comparison: Docker Compose vs Kubernetes
| Feature | Docker Compose | Kubernetes |
| --------- | ---------------- | ------------ |
| Setup complexity | Low – single file | Higher – many YAML resources |
| Scaling | Manual via `--scale` | Automatic via replicas and HPA |
| Networking | Simple DNS per service | Complex with Ingress, Services |
| Use case | Local dev, small deployments | Production, multi-node clusters |
| Learning curve | 1-2 hours | 2-4 weeks for basics |
CI/CD Pipelines with Docker
Continuous integration and deployment (CI/CD) with Docker means building an image, testing it, then pushing to a registry. GitHub Actions makes this straightforward.
Here’s a minimal workflow (`.github/workflows/docker.yml`):
```yaml
name: Build and Push Docker Image
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Push to Docker Hub
run: |
echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
docker push myapp:${{ github.sha }}
```
Set your Docker Hub credentials as repository secrets (Settings > Secrets). Every push to main builds and pushes an image tagged with the commit hash. I’ve used this pattern in production – it cuts manual deployment errors to near zero.
Common Pitfalls I’ve Seen Beginners Make
1. Ignoring .dockerignore – You don’t want node_modules or .git in your image. Add a file with those patterns.
2. Running containers as root – Use `USER` in your Dockerfile to switch to non-root. Security matters.
3. Not cleaning up – `docker system prune -a` once a month reclaims gigabytes.
4. Using latest tag – Pin to specific versions like `nginx:1.25-alpine`. `latest` breaks when updates happen.
FAQ
Q: What’s the difference between a Docker image and a container?
A: An image is a read-only template (like a class in programming). A container is a running instance of that image (like an object). You can have many containers from the same image.
Q: Do I need Kubernetes for a small project?
A: Probably not. Docker Compose handles most single-server needs. Kubernetes adds complexity that pays off only when you have multiple servers or need auto-scaling. I’d recommend it for teams of 5+ or production services handling more than 10K requests per second.
Q: How do I persist data in Docker containers?
A: Use volumes. In Docker run: `-v myvolume:/data`. In Compose: add a `volumes:` section (like the PostgreSQL example above). Volumes survive container restarts and removals. Avoid bind mounts for production – they’re useful for development but cause permission issues.