Back to blog

Docker Compose & Ứng Dụng Multi-Container

dockerdocker-composedevopscontainerstutorial
Docker Compose & Ứng Dụng Multi-Container

Chào mừng đến Giai đoạn 2 của Lộ Trình Học Docker & Kubernetes! Giờ bạn đã hiểu Docker cơ bản — images, containers, volumes và networking — đã đến lúc tự build images với Dockerfile và điều phối ứng dụng multi-container bằng Docker Compose.

Đây là lúc Docker thực sự mạnh mẽ. Thay vì pull images có sẵn, bạn sẽ đóng gói ứng dụng của riêng mình. Thay vì chạy từng container một, bạn sẽ define toàn bộ stack trong một file duy nhất.

Bạn Sẽ Học Được

✅ Viết Dockerfile từ đầu với tất cả instructions cần thiết
✅ Chọn base image phù hợp (alpine, slim, distroless)
✅ Dùng multi-stage builds để giảm image size 70-90%
✅ Hiểu layer caching và build optimization
✅ Thành thạo Docker Compose cho multi-container orchestration
✅ Cấu hình networks, volumes và environment variables trong Compose
✅ Thiết lập development và production configurations

Thời gian: 5–7 ngày, 1–2 giờ mỗi ngày
Yêu cầu: Giai đoạn 1: Docker Cơ Bản


Phần 1: Dockerfile Cơ Bản

Dockerfile là file text chứa các instructions để build Docker image. Hãy nghĩ nó như một công thức nấu ăn — mỗi instruction thêm một layer vào image.

Dockerfile Đầu Tiên

Bắt đầu với một ứng dụng Node.js đơn giản:

// app.js
const http = require('http');
 
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ message: 'Hello from Docker!', time: new Date() }));
});
 
server.listen(3000, () => {
  console.log('Server running on port 3000');
});

Dockerfile tương ứng:

# Dùng Node.js làm base image
FROM node:20-alpine
 
# Set working directory trong container
WORKDIR /app
 
# Copy package files trước (để cache tốt hơn)
COPY package*.json ./
 
# Install dependencies
RUN npm install --production
 
# Copy application code
COPY . .
 
# Expose port 3000
EXPOSE 3000
 
# Khởi động ứng dụng
CMD ["node", "app.js"]

Build và chạy:

# Build image
docker build -t my-node-app .
 
# Chạy container
docker run -d -p 3000:3000 --name my-app my-node-app
 
# Test
curl http://localhost:3000
# {"message":"Hello from Docker!","time":"2026-03-19T..."}

Các Dockerfile Instructions Cần Biết

FROM — Base Image

Mọi Dockerfile bắt đầu bằng FROM. Nó define base image mà ứng dụng build trên đó.

# Official images
FROM node:20-alpine
FROM python:3.12-slim
FROM golang:1.22-alpine
FROM openjdk:21-slim
 
# Minimal base images
FROM alpine:3.19
FROM ubuntu:24.04
FROM debian:bookworm-slim
 
# Empty base (cho static binaries)
FROM scratch

RUN — Thực Thi Lệnh

RUN thực thi commands trong quá trình build. Mỗi RUN tạo một layer mới.

# Install system packages
RUN apt-get update && apt-get install -y \
    curl \
    git \
    && rm -rf /var/lib/apt/lists/*
 
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
 
# Tạo directories
RUN mkdir -p /app/data

Best practice: Chain commands bằng && và cleanup trong cùng một RUN instruction để giảm layer size.

COPY vs ADD

Cả hai đều copy files vào image, nhưng hoạt động khác nhau:

# COPY — Copy file đơn giản (nên dùng)
COPY package.json ./
COPY src/ ./src/
COPY . .
 
# ADD — Tính năng thêm (thường tránh dùng)
ADD https://example.com/file.tar.gz /app/    # Download URLs
ADD archive.tar.gz /app/                      # Tự động giải nén

Best practice: Dùng COPY trừ khi bạn cần download URL hoặc auto-extract từ ADD.

WORKDIR — Thư Mục Làm Việc

Set working directory cho các instructions tiếp theo:

WORKDIR /app
 
# Tất cả commands sau đây chạy trong /app
COPY . .
RUN npm install
CMD ["node", "app.js"]

CMD vs ENTRYPOINT

Cả hai define cái gì chạy khi container start, nhưng mục đích khác nhau:

# CMD — Default command (có thể override)
CMD ["node", "app.js"]
CMD ["python", "main.py"]
 
# ENTRYPOINT — Executable cố định (arguments có thể thêm vào)
ENTRYPOINT ["python", "main.py"]
CMD ["--port", "8000"]  # Default arguments

Khác biệt trong thực tế:

# Với CMD ["node", "app.js"]
docker run my-app                    # Chạy: node app.js
docker run my-app bash               # Chạy: bash (CMD bị override)
 
# Với ENTRYPOINT ["python", "main.py"]
docker run my-app                    # Chạy: python main.py --port 8000
docker run my-app --port 9000        # Chạy: python main.py --port 9000
docker run my-app bash               # Chạy: python main.py bash (lỗi)

Quy tắc:

  • Dùng CMD cho general-purpose images khi user có thể muốn chạy commands khác
  • Dùng ENTRYPOINT + CMD khi container luôn phải chạy một program cụ thể

ENV và ARG — Biến

# ENV — Có trong build time VÀ runtime
ENV NODE_ENV=production
ENV PORT=3000
 
# ARG — CHỈ có trong build time
ARG VERSION=1.0.0
ARG NODE_VERSION=20
 
# Dùng ARG trong FROM
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-alpine
 
# Dùng build args
docker build --build-arg VERSION=2.0.0 -t my-app .

EXPOSE — Document Ports

EXPOSE không thực sự publish ports — nó chỉ là documentation cho biết container lắng nghe port nào:

EXPOSE 3000
EXPOSE 8080
EXPOSE 5432

Bạn vẫn cần -p flag khi chạy: docker run -p 3000:3000 my-app

USER — Chạy Không Phải Root

# Tạo non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
 
# Chuyển sang non-root user
USER appuser
 
# Tất cả commands sau chạy dưới appuser
COPY --chown=appuser:appgroup . .
CMD ["node", "app.js"]

Security: Luôn chạy production containers dưới non-root users.

VOLUME — Khai Báo Mount Points

VOLUME ["/data"]
VOLUME ["/var/log/app"]

LABEL — Metadata

LABEL maintainer="[email protected]"
LABEL version="1.0"
LABEL description="My awesome application"

Build Context và .dockerignore

Khi bạn chạy docker build ., Docker gửi toàn bộ directory (build context) đến Docker daemon. File .dockerignore loại trừ files không cần thiết:

# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
.env
.env.*
Dockerfile
docker-compose.yml
README.md
.DS_Store
coverage
.nyc_output
dist
build
*.md

Tại sao quan trọng:

# Không có .dockerignore — gửi mọi thứ (chậm)
Sending build context to Docker daemon  250MB
 
# Có .dockerignore — chỉ gửi cái cần (nhanh)
Sending build context to Docker daemon  2.5MB

Phần 2: Dockerfile Best Practices

Viết Dockerfile thì dễ. Viết Dockerfile tốt cần hiểu layers, caching và security.

Chọn Base Image Đúng

Base image ảnh hưởng lớn đến image size và security:

Base ImageSizeUse Case
node:20~1GBFull development environment
node:20-slim~200MBProduction (hầu hết dependencies)
node:20-alpine~130MBProduction (lightweight)
python:3.12~1GBFull Python environment
python:3.12-slim~150MBProduction Python
python:3.12-alpine~50MBMinimal Python
golang:1.22~800MBGo build environment
alpine:3.19~7MBMinimal Linux
gcr.io/distroless/static~2MBStatic binaries only

Hướng dẫn:

  • Development: Dùng full images để có debugging tools
  • Production: Dùng variants slim hoặc alpine
  • Go/Rust: Dùng scratch hoặc distroless cho final images (static binaries)
  • Luôn luôn: Pin specific versions (không dùng :latest)

Layer Caching và Thứ Tự Instructions

Docker cache mỗi layer. Nếu layer không thay đổi, Docker dùng lại cache. Thứ tự rất quan trọng:

# ❌ SAI — Reinstall dependencies mỗi khi code thay đổi
FROM node:20-alpine
WORKDIR /app
COPY . .                    # Code thay đổi invalidate layer này
RUN npm install             # Phải install lại mỗi lần
CMD ["node", "app.js"]
 
# ✅ ĐÚNG — Dependencies được cache trừ khi package.json thay đổi
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./       # Chỉ thay đổi khi dependencies đổi
RUN npm install             # Cached trừ khi package.json thay đổi
COPY . .                    # Code changes chỉ ảnh hưởng layer này
CMD ["node", "app.js"]

Nguyên tắc: Copy files ít thay đổi trước.

  • Layers xanh: Hiếm khi thay đổi (cached gần như luôn)
  • Layers vàng: Thay đổi khi dependencies update
  • Layers đỏ: Thay đổi với mỗi code update

Giảm Số Lượng Layers

Mỗi RUN, COPY và ADD tạo layer mới. Gộp các commands liên quan:

# ❌ SAI — 3 layers riêng biệt
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
 
# ✅ ĐÚNG — 1 layer, có cleanup
RUN apt-get update && apt-get install -y \
    curl \
    git \
    && rm -rf /var/lib/apt/lists/*

Security Hardening

FROM node:20-alpine
 
# Tạo non-root user
RUN addgroup -S app && adduser -S app -G app
 
WORKDIR /app
 
# Install dependencies với root
COPY package*.json ./
RUN npm ci --production && npm cache clean --force
 
# Copy app files với ownership đúng
COPY --chown=app:app . .
 
# Chuyển sang non-root user
USER app
 
EXPOSE 3000
CMD ["node", "app.js"]

Phần 3: Multi-Stage Builds

Multi-stage builds là game-changer. Cho phép bạn dùng một image để build và một image khác (nhỏ hơn) để chạy.

Vấn Đề

Không có multi-stage builds:

# Single-stage: gồm TẤT CẢ build tools trong final image
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
# Final image: ~1GB (gồm TypeScript compiler, dev dependencies, v.v.)
CMD ["node", "dist/app.js"]

Giải Pháp

# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
 
# Stage 2: Production
FROM node:20-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY --from=builder /app/dist ./dist
 
USER node
EXPOSE 3000
CMD ["node", "dist/app.js"]

--from=builder chỉ copy built artifacts từ stage đầu. Mọi thứ khác (source code, dev dependencies, build tools) bị loại bỏ.

Multi-Stage Build cho Python (FastAPI)

# Stage 1: Build dependencies
FROM python:3.12-slim AS builder
WORKDIR /app
 
# Install build dependencies
RUN apt-get update && apt-get install -y \
    gcc \
    && rm -rf /var/lib/apt/lists/*
 
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
 
# Stage 2: Production
FROM python:3.12-slim
WORKDIR /app
 
# Copy installed packages từ builder
COPY --from=builder /install /usr/local
 
# Tạo non-root user
RUN useradd --create-home appuser
USER appuser
 
COPY . .
 
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Multi-Stage Build cho Go

Go tỏa sáng với multi-stage builds vì Go compile thành static binary:

# Stage 1: Build
FROM golang:1.22-alpine AS builder
WORKDIR /app
 
COPY go.mod go.sum ./
RUN go mod download
 
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /server ./cmd/server
 
# Stage 2: Production (từ scratch — image nhỏ nhất có thể!)
FROM scratch
COPY --from=builder /server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
 
EXPOSE 8080
ENTRYPOINT ["/server"]

Kết quả: Final image chỉ vài MB thay vì ~800MB.

Multi-Stage Build cho Java (Spring Boot)

# Stage 1: Build với Maven
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:resolve
COPY src ./src
RUN mvn package -DskipTests
 
# Stage 2: Production
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
 
RUN addgroup -S app && adduser -S app -G app
 
COPY --from=builder /app/target/*.jar app.jar
RUN chown app:app app.jar
 
USER app
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

So Sánh Image Size

Impact của multi-stage builds:

Ứng DụngSingle-StageMulti-StageGiảm
Node.js API~1.1GB~180MB84%
Python FastAPI~950MB~150MB84%
Go API~800MB~8MB99%
Java Spring Boot~800MB~280MB65%

Phần 4: Docker Compose Cơ Bản

Chạy docker run với đống flags dài dòng mệt lắm, nhất là khi ứng dụng có nhiều services. Docker Compose giải quyết bằng cách define và quản lý multi-container applications trong một file YAML duy nhất.

Docker Compose Là Gì?

Docker Compose là tool define và chạy multi-container Docker applications. Thay vì viết thế này:

# Tạo network
docker network create myapp
 
# Start database
docker run -d --name postgres \
  --network myapp \
  -e POSTGRES_DB=mydb \
  -e POSTGRES_USER=admin \
  -e POSTGRES_PASSWORD=secret \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16-alpine
 
# Start Redis
docker run -d --name redis \
  --network myapp \
  redis:7-alpine
 
# Start application
docker run -d --name app \
  --network myapp \
  -p 3000:3000 \
  -e DATABASE_URL=postgres://admin:secret@postgres:5432/mydb \
  -e REDIS_URL=redis://redis:6379 \
  my-app

Bạn viết thế này:

# docker-compose.yml
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://admin:secret@postgres:5432/mydb
      REDIS_URL: redis://redis:6379
    depends_on:
      - postgres
      - redis
 
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql/data
 
  redis:
    image: redis:7-alpine
 
volumes:
  pgdata:

Và chạy mọi thứ bằng một lệnh:

docker compose up -d

Cấu Trúc docker-compose.yml

Compose file có các top-level keys:

services:    # Container definitions (bắt buộc)
networks:    # Custom networks (tùy chọn)
volumes:     # Named volumes (tùy chọn)
configs:     # Configuration objects (tùy chọn, Swarm mode)
secrets:     # Secret objects (tùy chọn, Swarm mode)

Service Configuration Chi Tiết

Mỗi service có thể cấu hình nhiều options:

services:
  web:
    # Image source (chọn một)
    image: nginx:alpine                  # Dùng pre-built image
    build: .                             # Build từ Dockerfile
    build:                               # Build với options
      context: ./app
      dockerfile: Dockerfile.prod
      args:
        NODE_ENV: production
 
    # Container settings
    container_name: my-web               # Tên container tùy chỉnh
    hostname: web-server                 # Hostname container
    restart: unless-stopped              # Restart policy
 
    # Networking
    ports:
      - "80:80"                          # host:container
      - "443:443"
      - "127.0.0.1:8080:8080"          # Bind vào interface cụ thể
 
    # Storage
    volumes:
      - ./src:/app/src                   # Bind mount
      - app-data:/app/data               # Named volume
      - /app/node_modules                # Anonymous volume
 
    # Environment
    environment:
      NODE_ENV: production
      API_KEY: ${API_KEY}                # Từ host environment
    env_file:
      - .env                             # Load từ file
 
    # Dependencies
    depends_on:
      - db
      - redis
 
    # Resource limits
    deploy:
      resources:
        limits:
          cpus: "0.5"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 256M
 
    # Health check
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:80"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
 
    # Logging
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Environment Variables

Docker Compose hỗ trợ nhiều cách set environment variables:

Inline trong docker-compose.yml

services:
  app:
    environment:
      NODE_ENV: production
      PORT: "3000"
      DEBUG: "false"

Từ .env File

# .env
POSTGRES_USER=admin
POSTGRES_PASSWORD=supersecret
POSTGRES_DB=myapp
APP_PORT=3000
services:
  app:
    env_file:
      - .env
    ports:
      - "${APP_PORT}:3000"
 
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}

Variable Substitution

services:
  app:
    image: my-app:${TAG:-latest}          # Default 'latest' nếu TAG chưa set
    environment:
      LOG_LEVEL: ${LOG_LEVEL:?error}      # Error nếu LOG_LEVEL chưa set

Depends_on và Health Checks

depends_on kiểm soát thứ tự khởi động, nhưng không đợi service sẵn sàng:

services:
  app:
    build: .
    depends_on:
      postgres:
        condition: service_healthy         # Đợi health check pass
      redis:
        condition: service_started         # Chỉ đợi container start
 
  postgres:
    image: postgres:16-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5
 
  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 5

Restart Policies

services:
  app:
    restart: "no"              # Không restart (mặc định)
    restart: always            # Luôn restart
    restart: on-failure        # Restart chỉ khi exit code khác 0
    restart: unless-stopped    # Restart trừ khi dừng thủ công

Production: Dùng unless-stopped hoặc always cho critical services.


Phần 5: Compose Networking

Docker Compose tự động tạo network cho project. Tất cả services giao tiếp bằng service name làm hostname.

Default Network

# docker-compose.yml
services:
  app:
    build: .
    ports:
      - "3000:3000"
 
  db:
    image: postgres:16-alpine
 
  redis:
    image: redis:7-alpine

Với setup này:

  • Compose tạo network tên <project>_default
  • app kết nối PostgreSQL tại db:5432
  • app kết nối Redis tại redis:6379
  • Tất cả services giao tiếp bằng service name
// Trong app container, kết nối bằng service names:
const dbUrl = 'postgres://admin:secret@db:5432/mydb';   // "db" = service name
const redisUrl = 'redis://redis:6379';                   // "redis" = service name

Custom Networks

Cho setup phức tạp hơn, define custom networks để isolate nhóm services:

services:
  # Frontend — chỉ nói chuyện với API
  frontend:
    build: ./frontend
    ports:
      - "80:80"
    networks:
      - frontend-net
 
  # API — nói chuyện với frontend VÀ backend services
  api:
    build: ./api
    ports:
      - "3000:3000"
    networks:
      - frontend-net
      - backend-net
 
  # Database — chỉ API truy cập được
  db:
    image: postgres:16-alpine
    networks:
      - backend-net
 
  # Redis — chỉ API truy cập được
  redis:
    image: redis:7-alpine
    networks:
      - backend-net
 
networks:
  frontend-net:
    driver: bridge
  backend-net:
    driver: bridge

Container frontend không thể truy cập db hoặc redis trực tiếp — network isolation được enforce.


Phần 6: Compose cho Development

Docker Compose thực sự tỏa sáng trong development workflows. Setup hot reload, override files và development-specific configurations.

Volume Mounts cho Hot Reload

Mount source code như volume để thay đổi reflect ngay lập tức:

services:
  app:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - ./src:/app/src                   # Mount source code
      - /app/node_modules                # Tránh overwrite node_modules
    environment:
      NODE_ENV: development
    command: npm run dev                  # Dùng dev server với hot reload

Override Files

Docker Compose tự động merge docker-compose.yml với docker-compose.override.yml:

docker-compose.yml (base — shared settings):

services:
  app:
    build: .
    ports:
      - "3000:3000"
    depends_on:
      - db
 
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql/data
 
volumes:
  pgdata:

docker-compose.override.yml (development — tự động load):

services:
  app:
    build:
      target: development
    volumes:
      - ./src:/app/src
      - /app/node_modules
    environment:
      NODE_ENV: development
      DEBUG: "true"
    command: npm run dev
 
  db:
    ports:
      - "5432:5432"                      # Expose DB port cho local tools

docker-compose.prod.yml (production — chỉ định rõ):

services:
  app:
    build:
      target: production
    environment:
      NODE_ENV: production
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 1G
 
  db:
    restart: unless-stopped
    # Không expose port — chỉ truy cập được trong Docker network

Cách dùng:

# Development (tự động load override file)
docker compose up
 
# Production (chỉ định prod file)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Compose Profiles

Dùng profiles để include services có điều kiện:

services:
  app:
    build: .
    ports:
      - "3000:3000"
 
  db:
    image: postgres:16-alpine
 
  # Chỉ start khi profile "debug" active
  adminer:
    image: adminer
    ports:
      - "8080:8080"
    profiles:
      - debug
 
  # Chỉ start khi profile "monitoring" active
  prometheus:
    image: prom/prometheus
    ports:
      - "9090:9090"
    profiles:
      - monitoring
 
  grafana:
    image: grafana/grafana
    ports:
      - "3001:3000"
    profiles:
      - monitoring
# Start chỉ app + db
docker compose up -d
 
# Start app + db + adminer
docker compose --profile debug up -d
 
# Start app + db + monitoring stack
docker compose --profile monitoring up -d
 
# Start tất cả
docker compose --profile debug --profile monitoring up -d

Phần 7: Ví Dụ Full-Stack Application

Gộp tất cả lại với một full-stack application hoàn chỉnh: React frontend + Node.js API + PostgreSQL + Redis.

Cấu Trúc Project

my-fullstack-app/
├── frontend/
│   ├── Dockerfile
│   ├── package.json
│   ├── src/
│   └── nginx.conf
├── api/
│   ├── Dockerfile
│   ├── package.json
│   └── src/
├── docker-compose.yml
├── docker-compose.override.yml
├── docker-compose.prod.yml
├── .env
└── .env.example

API Dockerfile (Multi-Stage)

# api/Dockerfile
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
 
# Development stage
FROM base AS development
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
 
# Build stage
FROM base AS builder
RUN npm ci
COPY . .
RUN npm run build
 
# Production stage
FROM node:20-alpine AS production
WORKDIR /app
 
RUN addgroup -S app && adduser -S app -G app
 
COPY package*.json ./
RUN npm ci --production
 
COPY --from=builder /app/dist ./dist
 
USER app
EXPOSE 3000
CMD ["node", "dist/app.js"]

Frontend Dockerfile (Multi-Stage)

# frontend/Dockerfile
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
 
# Development stage
FROM base AS development
RUN npm install
COPY . .
EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host"]
 
# Build stage
FROM base AS builder
RUN npm ci
COPY . .
RUN npm run build
 
# Production stage — serve bằng Nginx
FROM nginx:alpine AS production
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Nginx Configuration cho Frontend

# frontend/nginx.conf
server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html;
    index index.html;
 
    # Handle client-side routing
    location / {
        try_files $uri $uri/ /index.html;
    }
 
    # Proxy API requests tới backend
    location /api/ {
        proxy_pass http://api:3000/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
 
    # Cache static assets
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

Docker Compose Files

docker-compose.yml (base):

services:
  frontend:
    build:
      context: ./frontend
    depends_on:
      - api
 
  api:
    build:
      context: ./api
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
      REDIS_URL: redis://redis:6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
 
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 5s
      timeout: 5s
      retries: 5
 
  redis:
    image: redis:7-alpine
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 5
 
volumes:
  pgdata:
  redis-data:

docker-compose.override.yml (development):

services:
  frontend:
    build:
      target: development
    ports:
      - "5173:5173"
    volumes:
      - ./frontend/src:/app/src
      - /app/node_modules
 
  api:
    build:
      target: development
    ports:
      - "3000:3000"
    volumes:
      - ./api/src:/app/src
      - /app/node_modules
    environment:
      NODE_ENV: development
      DEBUG: "true"
 
  db:
    ports:
      - "5432:5432"
 
  redis:
    ports:
      - "6379:6379"

docker-compose.prod.yml (production):

services:
  frontend:
    build:
      target: production
    ports:
      - "80:80"
    restart: unless-stopped
 
  api:
    build:
      target: production
    environment:
      NODE_ENV: production
    restart: unless-stopped
 
  db:
    restart: unless-stopped
 
  redis:
    restart: unless-stopped

.env file:

POSTGRES_DB=myapp
POSTGRES_USER=admin
POSTGRES_PASSWORD=changeme_in_production

Chạy Full Stack

# Development — hot reload, exposed ports, debug tools
docker compose up
 
# Production build và chạy
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
 
# Xem logs
docker compose logs -f
 
# Xem logs service cụ thể
docker compose logs -f api
 
# Dừng tất cả
docker compose down
 
# Dừng và xóa volumes (clean slate)
docker compose down -v

Phần 8: Docker Compose Commands Cần Biết

Lifecycle Commands

# Start services (foreground)
docker compose up
 
# Start services (background)
docker compose up -d
 
# Start với rebuild
docker compose up -d --build
 
# Dừng services
docker compose stop
 
# Dừng và xóa containers, networks
docker compose down
 
# Dừng, xóa containers VÀ xóa volumes
docker compose down -v
 
# Restart service cụ thể
docker compose restart api

Inspection Commands

# Liệt kê running services
docker compose ps
 
# Xem logs (tất cả services)
docker compose logs
 
# Follow logs cho service cụ thể
docker compose logs -f api
 
# Xem resource usage
docker compose top

Execution Commands

# Chạy command trong running container
docker compose exec api sh
docker compose exec db psql -U admin myapp
 
# Chạy one-off command (tạo container mới)
docker compose run --rm api npm test
docker compose run --rm api npm run migrate

Build Commands

# Build tất cả images
docker compose build
 
# Build service cụ thể
docker compose build api
 
# Build không cache
docker compose build --no-cache
 
# Pull latest images
docker compose pull

Scaling Services

# Scale service lên 3 instances
docker compose up -d --scale api=3
 
# Lưu ý: cần bỏ fixed port mapping
# và dùng load balancer (như Nginx) phía trước

Patterns và Tips Thường Dùng

1. Wait-for-It Pattern

Đôi khi depends_on với health checks chưa đủ. Dùng wait script:

services:
  api:
    build: .
    command: >
      sh -c "
        echo 'Waiting for database...' &&
        while ! nc -z db 5432; do sleep 1; done &&
        echo 'Database is ready!' &&
        npm run migrate &&
        npm start
      "
    depends_on:
      - db

2. Database Initialization

Chạy SQL scripts khi start lần đầu:

services:
  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql   # Tự chạy lần đầu

3. Chia Sẻ Data Giữa Services

services:
  app:
    volumes:
      - shared-uploads:/app/uploads
 
  worker:
    volumes:
      - shared-uploads:/app/uploads
 
volumes:
  shared-uploads:

4. Dùng YAML Anchors Giảm Trùng Lặp

x-common-env: &common-env
  NODE_ENV: production
  LOG_LEVEL: info
  TZ: UTC
 
services:
  api:
    build: ./api
    environment:
      <<: *common-env
      PORT: "3000"
 
  worker:
    build: ./worker
    environment:
      <<: *common-env
      CONCURRENCY: "5"

5. Multi-Compose File cho Các Environments

# docker-compose.yml        — base config
# docker-compose.override.yml — dev (tự động load)
# docker-compose.prod.yml   — production
# docker-compose.test.yml   — testing
 
# Chạy tests
docker compose -f docker-compose.yml -f docker-compose.test.yml run --rm api npm test
 
# Deploy production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Bài Tập Thực Hành

Bài 1: Dockerfile Challenge

Viết multi-stage Dockerfile cho ứng dụng Python Flask:

  1. Build stage: install dependencies từ requirements.txt
  2. Production stage: dùng python:3.12-slim, chạy non-root user
  3. Final image phải dưới 200MB

Bài 2: Compose Stack

Tạo docker-compose.yml cho WordPress stack:

  • WordPress container (port 8080)
  • MySQL container với persistent volume
  • Dùng environment variables cho database credentials
  • Thêm health checks cho cả hai services

Bài 3: Full Development Environment

Setup development environment với:

  • Node.js API có hot reload
  • PostgreSQL với initialization scripts
  • Redis cho caching
  • Adminer để quản lý database (dùng profiles)
  • Tách development và production configurations

Tiếp Theo

Bài tiếp theo sẽ đi sâu vào Kubernetes Fundamentals — học cách orchestrate containers ở quy mô lớn:

  • Kiến trúc Kubernetes (control plane, worker nodes)
  • Pods, Deployments và Services
  • ConfigMaps và Secrets
  • kubectl commands cần thiết
  • Setup local Kubernetes cluster

Tóm Tắt

✅ Dockerfile define cách build images — dùng FROM, COPY, RUN, CMD
✅ Luôn dùng .dockerignore để giữ build context nhỏ
✅ Sắp xếp Dockerfile instructions cho optimal layer caching
✅ Multi-stage builds giảm image size 70-99%
✅ Chạy containers dưới non-root users cho security
✅ Docker Compose define multi-container apps trong một YAML file
✅ Services giao tiếp bằng name trong Compose networks
✅ Dùng override files để tách dev và production configs
✅ Health checks + depends_on đảm bảo thứ tự startup đúng
✅ Profiles include services có điều kiện như debug tools


Series: Lộ Trình Docker & Kubernetes
Trước đó: Giai đoạn 1: Docker Cơ Bản
Tiếp theo: Giai đoạn 3: Kubernetes Cơ Bản


Có câu hỏi về Dockerfile hay Docker Compose? Hãy liên hệ hoặc để lại comment!

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.