Bạn không phải người đầu tiên đứng trước câu hỏi: nên bắt đầu với Dockerfile hay Docker Compose? Nhiều đội ngũ phát triển đổi qua lại giữa hai công cụ này mà chưa có tiêu chí rõ ràng, dẫn đến cấu hình rối rắm, môi trường chạy kém ổn định, hoặc chi phí vận hành bị đội lên. Sự thật là: Dockerfile và Docker Compose không cạnh tranh, mà bổ trợ lẫn nhau. Vấn đề không phải "cái nào tốt hơn", mà là "dùng cái gì cho đúng lúc, đúng chỗ".
Bài viết này giúp bạn nhìn bức tranh tổng thể, so sánh sâu theo tiêu chí ra quyết định, kèm ví dụ thực tế, mẹo tối ưu và những sai lầm thường gặp. Cuối bài, bạn sẽ có một checklist ngắn gọn để chọn công cụ phù hợp cho từng tình huống.
docker-compose.yml (hoặc compose.yaml) định nghĩa các service (mỗi service là một container), mạng (networks), volume dữ liệu, biến môi trường, phụ thuộc giữa các service, chính sách restart, giới hạn tài nguyên, profiles môi trường, v.v.Điểm mấu chốt:
Chúng không loại trừ nhau. Trên thực tế:
docker run) nếu ứng dụng đơn lẻ.
Chọn Dockerfile và bỏ qua Compose khi:
Một Dockerfile tốt nên hướng tới: tái lập, nhỏ gọn, an toàn. Ví dụ: multi-stage build cho một API Go tối ưu kích thước, không cần Compose nếu nó dùng SQLite hoặc một dịch vụ bên ngoài đã có sẵn:
# syntax=docker/dockerfile:1.6
FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o app ./cmd/api
FROM gcr.io/distroless/base-debian12
WORKDIR /app
COPY --from=build /src/app /app/app
USER nonroot
EXPOSE 8080
ENTRYPOINT ./app
Ưu điểm khi chỉ dùng Dockerfile:
Nhược điểm:
Chọn Compose khi bạn cần một môi trường gồm nhiều thành phần phối hợp:
Compose mạnh ở các điểm:
docker compose up -d để khởi chạy, docker compose down -v để dọn sạch cả network và volume nếu muốn.Ví dụ một file Compose đơn giản cho web + PostgreSQL:
services:
web:
build: .
ports:
- 8080:8080
environment:
- DATABASE_URL=postgres://app:app@db:5432/app?sslmode=disable
depends_on:
- db
db:
image: postgres:16
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=app
- POSTGRES_DB=app
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Lưu ý: depends_on chỉ đảm bảo thứ tự khởi động, không đảm bảo "ứng dụng đã sẵn sàng". Dùng healthcheck và/hoặc script chờ (wait-for) nếu cần độ tin cậy cao.
Giả sử bạn có API Go cần Postgres. Chúng ta sẽ:
api và db.Dockerfile (multi-stage, non-root, cache tốt):
# syntax=docker/dockerfile:1.6
FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags='-s -w' -o app ./cmd/api
FROM gcr.io/distroless/static-debian12
WORKDIR /app
COPY --from=build /src/app /app/app
USER nonroot
EXPOSE 8080
ENTRYPOINT ./app
File compose.yaml:
services:
api:
build:
context: .
environment:
- DATABASE_URL=postgres://app:app@db:5432/app?sslmode=disable
ports:
- 8080:8080
depends_on:
- db
healthcheck:
test: ['CMD-SHELL', 'wget -qO- http://localhost:8080/health || exit 1']
interval: 5s
timeout: 2s
retries: 10
start_period: 10s
db:
image: postgres:16
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=app
- POSTGRES_DB=app
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U app']
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
volumes:
pgdata:
Nếu API cần chờ DB sẵn sàng, thay vì trông cậy hoàn toàn vào depends_on, bạn có thể thêm một tiny init/wait:
#!/usr/bin/env sh
set -eu
until nc -z db 5432; do
echo 'Waiting for Postgres...'
sleep 1
done
exec ./app
Đóng gói script này vào image và ENTRYPOINT ./wait-and-run.sh để đảm bảo API chỉ chạy khi DB lắng nghe cổng 5432.
Chạy:
docker compose up -d --build để build và khởi động.docker compose logs -f để theo dõi.docker compose down -v để dọn sạch cả volume (nếu muốn reset dữ liệu).
docker compose build --parallel tận dụng BuildKit.--scale cho service, nhưng không phải orchestrator phân tán; production ở quy mô lớn nên dùng K8s/ECS/Swarm.kompose để chuyển đổi sơ khởi.RUN --mount=type=secret), chạy non-root, minimize surface.env_file, gắn secrets (tích hợp Swarm) hoặc BuildKit secrets trong phần build; cô lập network.Kết luận theo tiêu chí: nếu bạn chỉ cần một image tốt, Dockerfile là trung tâm. Nếu bạn cần một "môi trường chạy" hoàn chỉnh với nhiều dịch vụ, Compose là công cụ bạn muốn.
Phát triển local:
services:
web:
build: .
volumes:
- .:/app
command: npm run dev
ports:
- 3000:3000
profiles: ['dev'] cho những service chỉ cần ở môi trường dev (adminer, mailhog, jaeger...).docker compose up --profile dev khi cần.Kiểm thử tích hợp (CI):
docker compose -f compose.yaml -f compose.test.yaml up -d --build
npm test || { code=$?; docker compose down -v; exit $code; }
docker compose down -v
healthcheck và --wait (nếu dùng plugin) hoặc script chờ để đảm bảo service đã sẵn sàng trước khi test.Sản xuất quy mô nhỏ-vừa (VM/bare metal):
latest).restart: unless-stopped và giới hạn tài nguyên deploy.resources.limits (trong Compose spec) hoặc mem_limit.Monorepo nhiều service:
x- extension fields để tái sử dụng cấu hình.
.dockerignore: loại bỏ thư mục build, cache, node_modules, .git để không phá cache và giảm context.distroless, alpine (cân nhắc glibc/musl), hoặc images chuyên biệt.RUN adduser -D appuser
USER appuser
HEALTHCHECK --interval=10s --timeout=2s --retries=5 CMD wget -qO- http://localhost:8080/health || exit 1
# syntax=docker/dockerfile:1.6
RUN --mount=type=secret,id=npm_token sh -c 'npm config set //registry.npmjs.org/:_authToken=$(cat /run/secrets/npm_token) && npm ci'
latest.
services:
jaeger:
image: jaegertracing/all-in-one:1.57
profiles: ['dev', 'obs']
compose.yaml + compose.override.yaml (tự động áp dụng) + các file chuyên biệt (compose.prod.yaml). Kết hợp bằng -f theo nhu cầu.depends_on cho readiness. Xây chiến lược chờ chủ động ở service phụ thuộc.up/down.node_modules). Có thể dùng "delegated" hoặc tách mount có chọn lọc.env_file: .env để tách cấu hình nhạy cảm khỏi repo (nhưng .env không phải secret vault!)..env, biến riêng service đặt tại environment.services:
api:
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
Lưu ý: một số tuỳ chọn deploy có hiệu lực khác nhau trên Docker Engine so với Swarm; kiểm tra phiên bản Compose/Docker bạn dùng.services:
web:
build:
context: .
secrets:
- npm_token
secrets:
npm_token:
file: .secrets/npm_token
Kết hợp với RUN --mount=type=secret,id=npm_token trong Dockerfile.
depends_on bảo đảm readiness: thực ra nó chỉ là thứ tự start. Cách khắc phục: healthcheck và/wait-for.latest ở mọi nơi: dẫn tới build/run không tái lập. Khắc phục: pin phiên bản base image và service phụ trợ (postgres:16.2)..dockerignore: làm context to đùng, cache vỡ, build chậm. Khắc phục: thêm các thư mục nặng và không cần thiết vào .dockerignore.USER rõ ràng./app trong container Node.js có thể che mất node_modules trong image. Khắc phục: chỉ mount mã nguồn, hoặc sử dụng kỹ thuật two-volume; hoặc cài node_modules trên host qua volumes chuyên biệt.
docker run hoặc tích hợp vào hạ tầng có sẵn.compose.yaml vào repo, thêm hướng dẫn 1 lệnh chạy toàn bộ.Một lưu ý quan trọng: nếu câu trả lời cho Bước 1 là "Có", thường không có lý do chính đáng để né Compose ở dev/test. Compose giảm lỗi thiết lập máy local và rút ngắn thời gian onboard.
Dockerfile có thể thay Docker Compose không?
Có thể dùng cả hai cùng lúc?
Compose có phù hợp production không?
Làm sao đảm bảo DB sẵn sàng trước khi app kết nối?
depends_on.Compose có làm build chậm hơn không?
docker compose build --parallel. Tốc độ phụ thuộc chủ yếu vào Dockerfile và cache.Khi nào nên tách nhiều file Compose?
-f để merge.Có thể chuyển Compose sang Kubernetes?
kompose để chuyển đổi sơ khởi; sau đó tinh chỉnh manifest cho phù hợp thông số K8s.Chìa khóa để chọn đúng không nằm ở việc tuyên bố một "người thắng cuộc", mà là hiểu đúng vai trò của từng công cụ. Dockerfile giúp bạn đóng gói ứng dụng thành một image chuẩn, tối ưu và an toàn. Docker Compose giúp bạn lắp ghép nhiều container thành một hệ thống vận hành trơn tru, có thể bật/tắt trong một lệnh, và chia sẻ được cho cả đội. Khi kết hợp đúng lúc đúng chỗ, bạn sẽ có vòng đời phát triển mượt mà: build nhanh, môi trường nhất quán, và triển khai đủ linh hoạt cho mọi giai đoạn từ local đến production.