Unit 3 - Notes

INT332 6 min read

Unit 3: Microservices with Docker Compose

1. Microservices Architecture

1.1 The Need for Microservices

In modern software development, applications must be highly scalable, available, and capable of rapid iteration. Traditional application architectures often struggle to meet these demands as the codebase grows. The need for microservices arises from:

  • Complexity Management: Large applications become too complex for a single developer or team to understand fully.
  • Release Bottlenecks: A minor change in one module requires rebuilding and deploying the entire application.
  • Technology Lock-in: It is difficult to adopt new technologies or frameworks in a traditional architecture without rewriting the whole system.
  • Resource Utilization: Scaling a traditional application means scaling the entire system, even if only one specific function requires more resources.

1.2 Monolithic vs. Microservices Architecture

Feature Monolithic Architecture Microservices Architecture
Structure A single, unified, indivisible unit. All logic is in one codebase. A collection of small, independent, loosely coupled services.
Coupling Tightly coupled. Components depend heavily on one another. Loosely coupled. Services communicate via APIs (e.g., REST, gRPC).
Scaling Scales by duplicating the entire application across multiple servers (vertical/horizontal). Scales independently. Only services with high load are scaled.
Deployment Entire application must be deployed at once. Slower CI/CD pipelines. Independent deployments. A single service can be updated seamlessly.
Tech Stack Constrained to a single technology stack/language. Polyglot. Each service can use the best language/DB for its specific task.
Failure Impact A bug in one module (e.g., memory leak) can crash the whole system. Failures are isolated. If one service fails, the rest of the application remains functional.

1.3 Advantages of Microservices

  • Scalability: Services can be scaled independently on demand. For an e-commerce platform, the "Checkout" service can be scaled heavily during a sale without scaling the "User Profile" service.
  • Isolation (Fault Tolerance): Process isolation ensures that a failure in one service does not cascade into a complete system failure. Security boundaries are also tighter.
  • Agility (Rapid Iteration): Smaller, focused teams can own a service from end to end. They can develop, test, and deploy independently, greatly reducing the time-to-market for new features.

1.4 API Gateway

An API Gateway acts as a reverse proxy to accept all application programming interface (API) calls, aggregate the various services required to fulfill them, and return the appropriate result.

  • Role in Microservices: Clients do not need to know the IP address or routing logic of dozens of microservices. They talk to the API Gateway.
  • Key Functions:
    • Request Routing: Directs incoming requests to the appropriate backend microservice.
    • Authentication & Authorization: Validates user credentials centrally before requests hit backend services.
    • Rate Limiting & Throttling: Prevents abuse and protects backend services from being overwhelmed.
    • Load Balancing: Distributes requests evenly across multiple instances of a service.
    • Response Aggregation: Combines responses from multiple microservices into a single payload for the client.

2. Docker Compose Fundamentals

Docker Compose is a tool for defining and running multi-container Docker applications. It uses a YAML file to configure application services, networks, and volumes, allowing developers to spin up complex architectures with a single command (docker-compose up).

2.1 YAML Structure and Writing docker-compose.yml

YAML (YAML Ain't Markup Language) is a human-readable data serialization standard. In Docker Compose, YAML relies on indentation (using spaces, not tabs) to denote structure.

2.2 Core Components

  • version: Historically used to specify the Compose file format version (e.g., version: '3.8'). Note: In newer Docker Compose V2 specifications, the version tag is deprecated and often omitted, but still widely found in legacy files.
  • services: The core of the file. It defines the individual containers that make up your application (e.g., web, database, cache).
  • volumes: Used for persistent data storage. Defines named volumes that can be shared across containers or persist when containers are destroyed.
  • networks: Defines custom networks to control communication between containers. Containers in the same network can communicate using their service names as hostnames.

2.3 Environment Variables, Secrets, and Configs

  • Environment Variables: Passed to containers using the environment key or via an external .env file using env_file. Useful for passing dynamic configuration like database URLs.
  • Secrets: Sensitive data (passwords, API keys) should not be hardcoded. Compose allows defining secrets that are securely mounted into the container at /run/secrets/<secret_name>.
  • Configs: Similar to secrets but for non-sensitive configuration files (e.g., Nginx config files). Mounted directly into the container filesystem.

2.4 Build vs. Image Fields

  • image: Tells Compose to pull a pre-built image from a container registry (like Docker Hub). Example: image: mysql:8.0.
  • build: Tells Compose to build an image from a local Dockerfile. You specify the context (directory) and optionally the Dockerfile name.
    YAML
        build:
          context: ./backend
          dockerfile: Dockerfile.dev
        
  • Note: You can use both together. If you specify both, Compose will build the image from the Dockerfile and tag it with the name specified in image.

2.5 Service Dependency Ordering (depends_on)

By default, Docker Compose starts all services simultaneously. depends_on controls the startup and shutdown order.

  • Standard usage: Starts service B only after service A starts.
  • Condition based: (e.g., condition: service_healthy) Ensures service B waits until service A is fully operational (requires a healthcheck defined in Service A).

3. Use Case Deployments

3.1 Generalized Multi-Container App (Database + Backend + Frontend)

A typical modern web application requires a frontend (React/Angular), a backend (Node/Python/Java), and a database (MySQL/Postgres/MongoDB). Docker Compose creates an isolated network where the Backend can securely talk to the DB, and the Frontend interacts with the Backend, often exposed to the host machine.

3.2 WordPress + MySQL Deployment

This deployment uses pre-built official images from Docker Hub.

YAML
services:
  db:
    image: mysql:8.0
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wpuser
      MYSQL_PASSWORD: wppassword
    volumes:
      - db_data:/var/lib/mysql

  wordpress:
    depends_on:
      - db
    image: wordpress:latest
    restart: always
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db:3306 # Resolves to the 'db' service above
      WORDPRESS_DB_USER: wpuser
      WORDPRESS_DB_PASSWORD: wppassword
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - wp_data:/var/www/html

volumes:
  db_data:
  wp_data:

3.3 Node.js + MongoDB Deployment

This example builds a custom Node.js backend while utilizing an official MongoDB image.

YAML
services:
  backend:
    build: ./api-server # Path containing Node.js Dockerfile
    ports:
      - "3000:3000"
    environment:
      - MONGO_URI=mongodb://database:27017/myapp
      - NODE_ENV=development
    depends_on:
      - database
    networks:
      - mern-network

  database:
    image: mongo:6.0
    volumes:
      - mongo-data:/data/db
    networks:
      - mern-network

volumes:
  mongo-data:

networks:
  mern-network:
    driver: bridge

3.4 Java Spring Boot + PostgreSQL Deployment

This deployment demonstrates depends_on with health checks, ensuring the Spring Boot application does not attempt to connect before PostgreSQL is fully initialized.

YAML
services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: adminpassword
      POSTGRES_DB: springdb
    ports:
      - "5432:5432"
    volumes:
      - pg_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U admin -d springdb"]
      interval: 10s
      timeout: 5s
      retries: 5

  springboot-app:
    build: 
      context: ./spring-app
    ports:
      - "8080:8080"
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/springdb
      SPRING_DATASOURCE_USERNAME: admin
      SPRING_DATASOURCE_PASSWORD: adminpassword
    depends_on:
      postgres:
        condition: service_healthy # Waits for DB healthcheck to pass

volumes:
  pg_data: