0

[Docker & Docker Compose - Phần 3] Docker Compose, Best Practices, và bức tranh tổng thể

Ở Phần 1 và Phần 2, mình đã đi qua lý do Docker ra đời, kiến trúc bên trong, Image vs Container, layer, build cache, Dockerfile, Volumes và Networking. Phần cuối nói về Docker Compose — công cụ quản lý nhiều container cùng lúc — và tổng hợp best practice quan trọng nhất.

Một ứng dụng thật sự không chỉ có một container

Một web app điển hình cần: frontend, backend, database, có thể thêm cache như Redis. Chạy từng container bằng tay qua các lệnh docker run riêng lẻ, bạn phải tự quản lý network, thứ tự khởi động, biến môi trường, port mapping — nhanh chóng khó kiểm soát khi số container tăng.

Vì sao dùng Docker Compose

Thay vì gọi docker run riêng lẻ cho từng service, một lệnh docker compose up đọc file khai báo và tự dựng toàn bộ hệ thống. Điểm mấu chốt là tính khai báo (declarative): bạn mô tả trạng thái mong muốn — service nào, port nào, volume nào — Compose tự tìm cách đưa hệ thống về đúng trạng thái đó, thay vì bạn viết script từng bước. Compose quản lý giúp bạn: network giữa các service, volume, thứ tự khởi động (depends_on), biến môi trường, port mapping.

Cấu trúc file docker-compose.yml

Hai điểm lưu ý: build vs image — build compile Dockerfile thành image tại local, image chỉ pull image có sẵn. depends_on chỉ kiểm soát thứ tự start, KHÔNG đợi service thực sự "sẵn sàng" nhận request — chỉ đợi tiến trình container đã start xong. Điều này hay gây bất ngờ với database, vốn cần vài giây sau khi start mới chấp nhận kết nối.

Vòng đời của Compose: up và down

compose up:   Build (nếu cần) → Tạo network → Tạo volume → Start services
compose down: Stop services → Xóa container → Giữ lại volume (*)

5 Best Practice quan trọng nhất

Docker có 12 best practice chính thức (docs.docker.com/build/building/best-practices), dưới đây là 5 cái tác động lớn nhất tới công việc hàng ngày.

1. Tận dụng build cache — đặt layer ổn định lên trước, code hay đổi để sau (đã nói kỹ ở Phần 2):

COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

2. Dùng Multi-stage build — build với đầy đủ toolchain, chỉ ship phần kết quả cuối:

# Stage 1: builder — có toàn bộ toolchain
FROM node:22 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Stage 2: runtime — gọn nhẹ, chỉ chứa output
FROM node:22-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]

Stage builder có thể nặng hơn 1GB (toolchain, dev dependencies, source thô), nhưng runtime chỉ lấy đúng những gì COPY --from=builder chỉ định. Kết quả thường nhỏ hơn 10 lần — không compiler, không dev deps, không lộ source thừa hay secret dùng lúc build.

3. Chọn đúng base image — base image quyết định "trần" cho kích thước, lỗ hổng bảo mật, độ tin cậy của mọi thứ build trên nó:

Ưu tiên Docker Official Images / Verified Publisher images. Lưu ý: Alpine dùng musl libc, đôi khi gây lỗi với Node module native — "nhỏ nhất" không đồng nghĩa "tốt nhất", nên test kỹ trước khi chọn Alpine cho production.

4. Pin phiên bản base image — bằng digest, không chỉ bằng tag:

# Trông như đã pin — nhưng chưa chắc
FROM alpine:3.21

# Pin đúng cách — bằng digest
FROM alpine:3.21@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511...

Tag là mutable — publisher có thể rebuild và trỏ lại tag 3.21 sang bộ byte khác bất kỳ lúc nào. Chỉ digest mới đảm bảo build luôn dùng đúng một image, mãi mãi. Đánh đổi: pin bằng digest nghĩa là không còn tự động nhận security patch — cần quy trình chủ động kiểm tra và cập nhật digest (công cụ như Docker Scout hỗ trợ việc này).

5. Tách rời các ứng dụng (decouple) — một tiến trình, một mối quan tâm, mỗi container:

yaml

services:
  web:
    build: .
  db:
    image: postgres:16
  cache:
    image: redis:7

So với nhồi nginx + postgres + redis + node vào chung một container (nếu Postgres crash, cả container sập theo), mỗi service ở đây độc lập restart, scale, update — nâng version Postgres mà không đụng gì tới container app. Đây chính là lý do kiến trúc mà Docker Compose tồn tại.

Bức tranh tổng thể

Cột trái là con đường single-app: Dockerfile là công thức build ra Image, chạy Image cho ra Container. Cột phải là con đường multi-service: Compose điều phối nhiều container để chúng hoạt động như một ứng dụng thống nhất. Gần như mọi nội dung trong series này chỉ là chi tiết nằm dưới một trong hai luồng trên.

Tổng kết

  • Docker giải quyết vấn đề nhất quán môi trường
  • Image là blueprint bất biến (immutable)
  • Container là instance đang chạy của image
  • Image layer giúp cache và tái sử dụng hiệu quả
  • Volume giúp dữ liệu tồn tại bền vững
  • Network giúp container giao tiếp với nhau
  • Docker Compose điều phối nhiều container cùng lúc

Và điều quan trọng nhất: hiểu bản chất khái niệm có giá trị hơn nhiều so với học thuộc câu lệnh. Command có thể tra cứu lại bất cứ lúc nào, nhưng mental model — VM vs Docker, image layer/cache, cách Compose hoạt động — mới là thứ giúp bạn thực sự làm chủ công cụ này lâu dài.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí