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. Through my blog, TechWithAssem, I share practical tutorials, real-world implementations, and step-by-step guides to help engineers grow in Cloud and DevOps.

Related Articles

Leave a Reply

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

Back to top button