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