Skip to content
advanced Phase 8 · Docker Project

Project Architecture

Design the architecture for a multi-container Docker application.

1h
0 problems
Topic Progress 0%

Multi-Container Application Design

Multi-Container Application Design

Application Architecture

We'll build a Task Management App with:

  • Frontend: React SPA served by Nginx
  • API: Node.js/Express REST API
  • Database: PostgreSQL
  • Cache: Redis
┌─────────────┐     ┌──────────────┐     ┌──────────────┐
│   Nginx     │────▶│   Node.js    │────▶│  PostgreSQL  │
│  (React)    │     │   (API)      │     │  (Database)  │
└─────────────┘     └──────┬───────┘     └──────────────┘
                           │
                    ┌──────▼───────┐
                    │    Redis     │
                    │   (Cache)    │
                    └──────────────┘

Directory Structure

task-manager/
├── frontend/
│   ├── Dockerfile
│   ├── Dockerfile.dev
│   ├── package.json
│   ├── src/
│   └── public/
├── api/
│   ├── Dockerfile
│   ├── Dockerfile.dev
│   ├── package.json
│   ├── src/
│   └── config/
├── nginx/
│   └── nginx.conf
├── scripts/
│   └── init.sql
├── docker-compose.yml
├── docker-compose.dev.yml
├── docker-compose.prod.yml
├── .env
├── .env.example
├── .gitignore
└── README.md

Network Design

services:
  nginx:
    networks:
      - frontend

  api:
    networks:
      - frontend    # Receives traffic from nginx
      - backend     # Communicates with db and redis

  db:
    networks:
      - backend     # Only accessible from api

  redis:
    networks:
      - backend     # Only accessible from api

Data Flow

1. User → Browser → Nginx:80 → Static files + /api/* proxy
2. Nginx → API:4000
3. API → DB:5432 (queries, transactions)
4. API → Redis:6379 (caching, sessions)
5. API → Nginx → Browser (JSON responses)

Volume Strategy

Service Volume Type Purpose
db Named volume (pgdata) Persistent database data
redis Named volume (redis_data) Persistent cache
nginx Bind mount Config file only
api Bind mount (dev) Source code for hot reload

Writing Dockerfiles for Each Service

Writing Dockerfiles for Each Service

Frontend Dockerfile (Production)

# frontend/Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:1.25-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY ../nginx/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Frontend Dockerfile (Development)

# frontend/Dockerfile.dev
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host"]

API Dockerfile (Production)

# api/Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .

FROM node:20-alpine
RUN addgroup -g 1001 -S appgroup && \
    adduser -S appuser -u 1001 -G appgroup
WORKDIR /app
COPY --from=builder --chown=appuser:appgroup /app ./
USER appuser
EXPOSE 4000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:4000/health || exit 1
CMD ["node", "src/server.js"]

API Dockerfile (Development)

# api/Dockerfile.dev
FROM node:20-alpine
RUN npm install -g nodemon
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 4000
CMD ["nodemon", "--watch", "src", "src/server.js"]

Nginx Configuration

# nginx/nginx.conf
server {
    listen 80;
    server_name localhost;

    root /usr/share/nginx/html;
    index index.html;

    # API proxy
    location /api/ {
        proxy_pass http://api:4000/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_cache_bypass $http_upgrade;
    }

    # SPA routing
    location / {
        try_files $uri $uri/ /index.html;
    }

    # Cache static assets
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

Database Init Script

-- scripts/init.sql
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

CREATE TABLE tasks (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    title VARCHAR(255) NOT NULL,
    description TEXT,
    status VARCHAR(50) DEFAULT 'pending',
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_tasks_status ON tasks(status);
CREATE INDEX idx_tasks_created_at ON tasks(created_at);

Docker Compose Configuration

Docker Compose Configuration

Development Compose

# docker-compose.dev.yml
services:
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile.dev
    volumes:
      - ./frontend/src:/app/src
      - /app/node_modules
    ports:
      - "5173:5173"
    environment:
      - VITE_API_URL=http://localhost:4000
    networks:
      - frontend

  api:
    build:
      context: ./api
      dockerfile: Dockerfile.dev
    volumes:
      - ./api/src:/app/src
      - /app/node_modules
    ports:
      - "4000:4000"
      - "9229:9229"  # Node.js debugger
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://dev:devpass@db:5432/taskmanager
      - REDIS_URL=redis://redis:6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - frontend
      - backend

  db:
    image: postgres:16
    environment:
      POSTGRES_DB: taskmanager
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: devpass
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U dev"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - backend

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      - backend

volumes:
  pgdata:
  redis_data:

networks:
  frontend:
  backend:

Production Compose

# docker-compose.prod.yml
services:
  frontend:
    build:
      context: ./frontend
    restart: unless-stopped
    networks:
      - frontend

  api:
    build:
      context: ./api
    restart: unless-stopped
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=${REDIS_URL}
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 1G
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - frontend
      - backend

  db:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 4G
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
    networks:
      - backend

volumes:
  pgdata:
  redis_data:

networks:
  frontend:
  backend:
    internal: true