Chapter 02

Docker & Compose Setup

Cài đặt Docker, viết Dockerfile và cấu hình docker-compose.yml cho toàn bộ stack

Cài đặt Docker Engine

Trên Ubuntu server, cài Docker Engine từ repository chính thức của Docker (không dùng bản từ apt mặc định vì thường cũ hơn):

Bash
# 1. Cập nhật packages và cài dependencies
sudo apt update
sudo apt install -y ca-certificates curl gnupg

# 2. Thêm Docker GPG key
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
  sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# 3. Thêm Docker repository
echo \
  "deb [arch=$(dpkg --print-architecture) \
  signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# 4. Cài Docker Engine + Compose plugin
sudo apt update
sudo apt install -y docker-ce docker-ce-cli \
  containerd.io docker-buildx-plugin docker-compose-plugin

# 5. Cho phép user hiện tại chạy docker không cần sudo
sudo usermod -aG docker $USER
newgrp docker

# 6. Kiểm tra cài đặt
docker --version
docker compose version
Docker Compose V2 đi kèm Docker Engine dưới dạng plugin. Lệnh sử dụng là docker compose (không có dấu gạch ngang), thay thế cho docker-compose (V1) đã deprecated.

Cấu trúc thư mục dự án

Tổ chức thư mục rõ ràng giúp quản lý Dockerfile và config cho từng service:

Tree
my-app/
├── docker-compose.yml          # Orchestration file chính
├── docker-compose.override.yml # Override cho development (optional)
├── .env                        # Biến môi trường (KHÔNG commit lên git)
├── .dockerignore               # Files bỏ qua khi build
│
├── frontend/                   # React application
│   ├── Dockerfile
│   ├── nginx.conf              # Nginx config cho React SPA
│   ├── src/
│   ├── public/
│   └── package.json
│
├── backend/                    # ASP.NET API
│   ├── Dockerfile
│   ├── src/
│   │   └── MyApp.Api/
│   │       ├── Program.cs
│   │       ├── appsettings.json
│   │       └── MyApp.Api.csproj
│   └── MyApp.sln
│
└── nginx/                      # Reverse proxy config
    ├── nginx.conf
    └── ssl/                    # SSL certificates

Dockerfile cho React

Sử dụng multi-stage build để tạo image nhẹ nhất có thể — stage 1 build React app, stage 2 chỉ chứa static files và Nginx:

Dockerfile
# ========== Stage 1: Build ==========
FROM node:20-alpine AS build

WORKDIR /app

# Copy package files trước để tận dụng Docker cache
COPY package.json package-lock.json ./
RUN npm ci --silent

# Copy source code và build
COPY . .
RUN npm run build

# ========== Stage 2: Production ==========
FROM nginx:alpine

# Copy build output vào Nginx
COPY --from=build /app/build /usr/share/nginx/html

# Copy custom Nginx config cho SPA routing
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

File nginx.conf cho React SPA (xử lý client-side routing):

Nginx
server {
    listen 80;
    root /usr/share/nginx/html;
    index index.html;

    # SPA: mọi route đều trả về index.html
    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";
    }
}

Dockerfile cho ASP.NET API

Tương tự React, sử dụng multi-stage build — stage 1 restore + publish, stage 2 chỉ chứa runtime:

Dockerfile
# ========== Stage 1: Build ==========
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build

WORKDIR /src

# Copy csproj trước để tận dụng cache layer
COPY src/MyApp.Api/MyApp.Api.csproj src/MyApp.Api/
RUN dotnet restore src/MyApp.Api/MyApp.Api.csproj

# Copy toàn bộ source và publish
COPY . .
RUN dotnet publish src/MyApp.Api/MyApp.Api.csproj \
    -c Release -o /app/publish --no-restore

# ========== Stage 2: Runtime ==========
FROM mcr.microsoft.com/dotnet/aspnet:8.0

WORKDIR /app

# Chạy dưới non-root user cho bảo mật
RUN adduser --disabled-password --gecos "" appuser
USER appuser

COPY --from=build /app/publish .

EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApp.Api.dll"]
Từ .NET 8, Kestrel mặc định listen trên port 8080 (không phải 80) khi chạy dưới non-root user. Đây là thay đổi security quan trọng — không cần chạy container với quyền root.

.dockerignore

Giảm build context size và tránh copy files không cần thiết vào image:

.dockerignore
# Dependencies
node_modules/
**/bin/
**/obj/

# Build output
build/
dist/
publish/

# Environment & secrets
.env
.env.*
*.pfx
*.pem

# IDE & OS
.vs/
.vscode/
.idea/
*.swp
.DS_Store
Thumbs.db

# Git & Docker
.git/
.gitignore
docker-compose*.yml
Dockerfile*
README.md

docker-compose.yml

File quan trọng nhất — định nghĩa toàn bộ stack, networks, volumes, và dependencies:

YAML
services:
  # ── Nginx Reverse Proxy ──
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      frontend:
        condition: service_started
      backend:
        condition: service_healthy
    networks:
      - frontend-net
      - backend-net
    restart: unless-stopped

  # ── React Frontend ──
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    networks:
      - frontend-net
    restart: unless-stopped

  # ── ASP.NET API ──
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
      - ConnectionStrings__DefaultConnection=${DB_CONNECTION}
      - Redis__ConnectionString=${REDIS_CONNECTION}
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    networks:
      - backend-net
      - data-net
    restart: unless-stopped

  # ── PostgreSQL ──
  postgres:
    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: 10s
      timeout: 5s
      retries: 5
    networks:
      - data-net
    restart: unless-stopped

  # ── Redis ──
  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - data-net
    restart: unless-stopped

# ── Networks ──
networks:
  frontend-net:
    driver: bridge
  backend-net:
    driver: bridge
  data-net:
    driver: bridge
    internal: true  # Không có internet access

# ── Volumes ──
volumes:
  pgdata:
  redisdata:
Không bao giờ hardcode passwords hay secrets trực tiếp trong docker-compose.yml. Luôn sử dụng biến môi trường từ file .env — và đảm bảo .env nằm trong .gitignore.

Vòng đời Docker Compose

Các lệnh chính để quản lý stack:

Build
docker compose build
Start
docker compose up -d
Monitor
docker compose ps
Update
build + up
Stop
docker compose down
Bash
# Build tất cả images
docker compose build

# Khởi chạy toàn bộ stack (background)
docker compose up -d

# Xem trạng thái các containers
docker compose ps

# Xem logs (follow mode)
docker compose logs -f

# Xem logs của một service cụ thể
docker compose logs -f backend

# Restart một service
docker compose restart backend

# Dừng toàn bộ stack
docker compose down

# Dừng và XÓA volumes (CẢNH BÁO: mất dữ liệu!)
docker compose down -v
Lệnh docker compose down -v sẽ xóa toàn bộ volumes bao gồm dữ liệu PostgreSQL. Chỉ dùng khi bạn thực sự muốn reset mọi thứ. Trong production, hầu như không bao giờ nên dùng flag -v.

Triển khai lên server

Quy trình cơ bản để deploy lần đầu lên Ubuntu server:

Bash
# 1. SSH vào server
ssh user@your-server-ip

# 2. Clone source code
git clone https://github.com/your-org/my-app.git
cd my-app

# 3. Tạo file .env từ template
cp .env.example .env
nano .env  # Chỉnh sửa giá trị cho production

# 4. Build và khởi chạy
docker compose build --no-cache
docker compose up -d

# 5. Kiểm tra trạng thái
docker compose ps
docker compose logs -f --tail=50
Với các lần deploy tiếp theo (update code), chỉ cần: git pull && docker compose build && docker compose up -d. Docker Compose sẽ chỉ restart các service có image thay đổi.

Tại sao dùng Multi-stage Build?

So sánh image size giữa single-stage và multi-stage:

🚨

Single-stage

Chứa cả build tools lẫn runtime
  • React image: ~1.2 GB (chứa node_modules)
  • API image: ~900 MB (chứa .NET SDK)
  • Build tools không cần ở production
  • Attack surface lớn hơn

Multi-stage

Chỉ chứa output cần thiết
  • React image: ~40 MB (Nginx + static files)
  • API image: ~220 MB (.NET runtime only)
  • Không có build tools, ít dependencies
  • Attack surface nhỏ, khởi động nhanh