IT Tutorials

Docker and Docker Compose: Microservices Containerization Guide

Before Docker: Dependency hell.

App works on your laptop. Doesn’t work on production. Different OS versions. Different libraries. Different configurations.

After Docker: “It works on my machine” = It works everywhere.

This guide teaches Docker and Docker Compose practically:

  • Container fundamentals
  • Real Dockerfiles
  • Docker Compose for multi-container apps
  • Microservices architecture
  • Real production examples
  • Best practices

Not theory. Just hands-on containerization you can use today.


Part 1: Understanding Docker Fundamentals

What Is Docker?

Docker is containerization platform.

Container = Your application + all dependencies in one box.

Traditional approach:
Server 1: Windows + Python 3.8 + Node.js 12 + PostgreSQL 10
Server 2: Linux + Python 3.9 + Node.js 14 + PostgreSQL 12
Different = Problems

Docker approach:
Container 1: App + Python 3.8 + dependencies (exact)
Container 2: App + Python 3.9 + dependencies (exact)
Same = Works everywhere

Why Docker matters:

Before Docker:
- 30 minutes: Install dependencies
- 2 hours: Fix compatibility issues
- Different behavior in dev/test/production

After Docker:
- 5 minutes: Pull Docker image
- Same behavior everywhere
- Development = Production environment

Reference: What Is Docker


Docker Architecture

Docker Client (docker command)
    ↓
Docker Daemon (runs on your computer)
    ↓
Containers (isolated applications)
    ↓
Images (blueprints for containers)

Key concepts:

Image: Blueprint (like class in programming) Container: Running instance of image (like object) Registry: Storage for images (like Docker Hub)


Part 2: Installing and Setting Up Docker

Step 1: Install Docker

Windows/Mac:

  1. Download Docker Desktop
  2. Install
  3. Start Docker Desktop
  4. Open terminal/PowerShell
  5. Type: docker --version

Linux:

bash
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
docker --version

Time: 15-20 minutes

Reference: Install Docker


Step 2: Pull Your First Docker Image

bash
# Pull official Node.js image
docker pull node:18

# Run container
docker run -it node:18

# You're now inside container with Node.js
node --version  # v18.x.x

Time: 5 minutes


Part 3: Creating Your First Dockerfile

Understanding Dockerfile

Dockerfile = Recipe for Docker image.

Each line creates a layer in the image.

Simple example:

dockerfile
# Start from official Node.js image
FROM node:18-alpine

# Set working directory inside container
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm install

# Copy application code
COPY . .

# Expose port
EXPOSE 3000

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

What each command does:

FROM: Base image (start point)
WORKDIR: Where commands run
COPY: Copy files from host to container
RUN: Execute commands (npm install, etc)
EXPOSE: Document which port app uses
CMD: Default command when container starts

Step 4: Build Your Image

Create Dockerfile and package.json in directory:

package.json:

json
{
  "name": "my-app",
  "version": "1.0.0",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.18.0"
  }
}

server.js:

javascript
const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send('Hello from Docker!');
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

Build image:

bash
docker build -t my-app:1.0 .

# Run container
docker run -p 3000:3000 my-app:1.0

# Visit http://localhost:3000

Time: 10 minutes

Reference: Dockerfile Reference


Part 4: Docker Compose Fundamentals

What Is Docker Compose?

Docker Compose = Tool for multi-container applications.

Instead of running docker run 5 times for 5 different containers:

yaml
# One docker-compose.yml file defines everything
version: '3.8'

services:
  web:
    build: .
  database:
    image: postgres:15
  cache:
    image: redis:7

Run everything:

bash
docker-compose up

All 3 containers start together. Connected automatically.


Step 5: Create docker-compose.yml

Simple example:

yaml
version: '3.8'

services:
  # Web application
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://user:password@db:5432/myapp
    depends_on:
      - db
  
  # PostgreSQL database
  db:
    image: postgres:15
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=myapp
    volumes:
      - db-data:/var/lib/postgresql/data

  # Redis cache
  cache:
    image: redis:7
    ports:
      - "6379:6379"

volumes:
  db-data:

What this does:

web service:
- Builds image from Dockerfile in current directory
- Maps port 3000 to host
- Sets environment variables
- Waits for db to start before starting

db service:
- Uses official PostgreSQL image
- Sets database credentials
- Stores data in persistent volume

cache service:
- Uses official Redis image
- Exposes port for web app to access

Step 6: Run Docker Compose

bash
# Start all services
docker-compose up

# In another terminal, check services
docker-compose ps

# View logs
docker-compose logs -f web

# Stop all services
docker-compose down

# Remove everything including volumes
docker-compose down -v

Time: 5 minutes

Reference: Docker Compose Documentation


Part 5: Containerizing Microservices Applications

Understanding Microservices Architecture

Microservices = Multiple small services working together.

Traditional monolith:
One big application
- User Service
- Product Service
- Order Service
- Payment Service
All in one app = Hard to scale

Microservices:
Each service separate
- User Service (Docker container)
- Product Service (Docker container)
- Order Service (Docker container)
- Payment Service (Docker container)
Each scales independently

Real Microservices Example: E-Commerce Application

Architecture:

User Service (Python Flask)
    ↓
API Gateway (Node.js)
    ↓
├─ Product Service (Python FastAPI)
├─ Order Service (Node.js)
├─ Payment Service (Java Spring Boot)
└─ Notification Service (Python)

Database: PostgreSQL
Cache: Redis
Message Queue: RabbitMQ

Step 7: Create Microservices Dockerfiles

1. User Service (Python):

Create user-service/Dockerfile:

dockerfile
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .

EXPOSE 5000

CMD ["python", "app.py"]

user-service/requirements.txt:

Flask==2.3.0
Flask-SQLAlchemy==3.0.0
python-dotenv==1.0.0

user-service/app.py:

python
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://user:pass@db:5432/users'
db = SQLAlchemy(app)

@app.route('/users', methods=['GET'])
def get_users():
    return {'users': []}

@app.route('/health', methods=['GET'])
def health():
    return {'status': 'healthy'}

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

2. Product Service (Python):

Create product-service/Dockerfile:

dockerfile
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .

EXPOSE 5001

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "5001"]

product-service/main.py:

python
from fastapi import FastAPI
import httpx

app = FastAPI()

@app.get("/products")
async def get_products():
    return {"products": []}

@app.get("/health")
async def health():
    return {"status": "healthy"}

3. Order Service (Node.js):

Create order-service/Dockerfile:

dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

EXPOSE 5002

CMD ["npm", "start"]

order-service/server.js:

javascript
const express = require('express');
const app = express();

app.get('/orders', (req, res) => {
  res.json({ orders: [] });
});

app.get('/health', (req, res) => {
  res.json({ status: 'healthy' });
});

app.listen(5002, () => {
  console.log('Order Service on port 5002');
});

Step 8: Docker Compose for Complete Microservices

Create docker-compose.yml:

yaml
version: '3.8'

services:
  # User Service
  user-service:
    build: ./user-service
    ports:
      - "5000:5000"
    environment:
      - DATABASE_URL=postgresql://user:password@db:5432/users
      - FLASK_ENV=production
    depends_on:
      - db
    networks:
      - microservices-network

  # Product Service
  product-service:
    build: ./product-service
    ports:
      - "5001:5001"
    environment:
      - DATABASE_URL=postgresql://user:password@db:5432/products
    depends_on:
      - db
    networks:
      - microservices-network

  # Order Service
  order-service:
    build: ./order-service
    ports:
      - "5002:5002"
    environment:
      - USER_SERVICE_URL=http://user-service:5000
      - PRODUCT_SERVICE_URL=http://product-service:5001
    depends_on:
      - db
    networks:
      - microservices-network

  # API Gateway
  api-gateway:
    image: nginx:alpine
    ports:
      - "8000:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - user-service
      - product-service
      - order-service
    networks:
      - microservices-network

  # PostgreSQL Database
  db:
    image: postgres:15
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_INITDB_ARGS=--encoding=UTF8
    volumes:
      - postgres-data:/var/lib/postgresql/data
    networks:
      - microservices-network

  # Redis Cache
  cache:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    networks:
      - microservices-network

  # RabbitMQ Message Queue
  rabbitmq:
    image: rabbitmq:3.12-management-alpine
    ports:
      - "5672:5672"
      - "15672:15672"
    environment:
      - RABBITMQ_DEFAULT_USER=user
      - RABBITMQ_DEFAULT_PASS=password
    networks:
      - microservices-network

volumes:
  postgres-data:

networks:
  microservices-network:
    driver: bridge

Deploy everything:

bash
docker-compose up -d

# Check all services running
docker-compose ps

# View logs
docker-compose logs -f

# Stop everything
docker-compose down

Time: 10-15 minutes

Read also:

How to Build a Complete CI/CD Pipeline with GitHub Actions and Docker (2026 Guide)


Part 6: Best Practices for Docker and Microservices

1. Use Multi-Stage Builds (Smaller Images)

Bad (large image):

dockerfile
FROM golang:1.20
WORKDIR /app
COPY . .
RUN go build -o app
CMD ["./app"]

Good (small image):

dockerfile
# Build stage
FROM golang:1.20 AS builder
WORKDIR /app
COPY . .
RUN go build -o app

# Runtime stage (smaller)
FROM alpine:latest
COPY --from=builder /app/app .
CMD ["./app"]

Benefit: Final image is 50-70% smaller.

Reference: Multi-Stage Builds


2. Use Minimal Base Images

FROM ubuntu:22.04        # 77 MB
FROM python:3.11         # 1+ GB
FROM python:3.11-slim    # 125 MB  ✓ Use this
FROM python:3.11-alpine  # 50 MB   ✓ Or this

Alpine images are 10-20x smaller.


3. Don’t Run as Root

dockerfile
# Bad: runs as root
FROM node:18
WORKDIR /app
COPY . .
CMD ["npm", "start"]

# Good: runs as non-root user
FROM node:18
RUN useradd -m appuser
USER appuser
WORKDIR /app
COPY . .
CMD ["npm", "start"]

4. Health Checks

yaml
services:
  web:
    build: .
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

Docker automatically removes unhealthy containers.


5. Logging Strategy

dockerfile
# Don't write logs to files
# Write to stdout for Docker to capture

# Wrong:
RUN echo "Application started" >> app.log

# Right:
RUN echo "Application started"  # Goes to stdout

View logs:

bash
docker logs container-name
docker-compose logs service-name

Part 7: Container Registry (Docker Hub)

Publishing Your Images

bash
# Login to Docker Hub
docker login

# Tag image
docker tag my-app:1.0 username/my-app:1.0

# Push to registry
docker push username/my-app:1.0

# Others can now pull
docker pull username/my-app:1.0

Reference: Docker Hub


Part 8: Production Considerations

Networking

yaml
services:
  web:
    networks:
      - public
      - internal
  
  db:
    networks:
      - internal

networks:
  public:
    driver: bridge
  internal:
    driver: bridge

web talks to public and internal networks. db talks only to internal network (not exposed).


Volumes for Persistence

yaml
services:
  db:
    image: postgres:15
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:
    driver: local

Data persists even if container stops.


Environment Secrets

.env file:

DB_PASSWORD=super_secret_password
API_KEY=abc123def456

docker-compose.yml:

yaml
services:
  app:
    environment:
      - DB_PASSWORD=${DB_PASSWORD}
      - API_KEY=${API_KEY}

Never commit .env to git!


Part 9: Troubleshooting Docker and Compose

Issue 1: Container Won’t Start

bash
# Check logs
docker logs container-name

# Run in interactive mode to see errors
docker run -it image-name

# Check image exists
docker images

Issue 2: Port Already in Use

bash
# Find what's using port
netstat -an | grep 3000  # Linux/Mac
netstat -ano | findstr :3000  # Windows

# Change port in docker-compose
ports:
  - "3001:3000"  # Use 3001 instead

Issue 3: Services Can’t Communicate

bash
# Check network
docker network ls

# Inspect network
docker network inspect network-name

# Services must use service name as hostname
# Wrong: http://localhost:5000
# Right: http://user-service:5000

Part 10: Docker vs Docker Compose vs Kubernetes

Docker:
- Single container
- Simple applications
- Development

Docker Compose:
- Multiple containers
- Multi-service applications
- Development and small production

Kubernetes:
- Large-scale orchestration
- Auto-scaling
- High availability
- Enterprise production

Reference: [Kubernetes vs Docker Compose](https://kubernetes.io/docs/concepts/architecture/)

Conclusion: Containerization Is Standard

Docker and Docker Compose aren’t optional anymore.

They’re standard practice for:

  • Development (consistent environment)
  • Testing (same as production)
  • Production (reliable deployment)
  • Microservices (service isolation)

Start with Docker. Learn Docker Compose. Scale to Kubernetes when needed.

Mo Assem

My name is Mohamed Assem, and I am a Cloud & Infrastructure Engineer with over 14 years of experience in IT, working across both Microsoft Azure and AWS. My expertise lies in cloud operations, automation, and building modern, scalable infrastructure. I design and implement CI/CD pipelines and infrastructure as code solutions using tools like Terraform and Docker to streamline operations and improve efficiency. Open to relocation to Europe for senior infrastructure and cloud engineering roles. Through my blog, TechWithAssem, I share practical tutorials, real-world implementations, and step-by-step guides to help engineers grow in Cloud and DevOps.

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button