"Trên máy em chạy được mà!" là câu nói quen thuộc đến mức trở thành meme trong giới lập trình. Nguyên nhân thường nằm ở sự khác biệt môi trường: phiên bản Node.js, thư viện hệ thống, biến môi trường hay cấu hình cơ sở dữ liệu giữa máy cá nhân và server không giống nhau. Docker ra đời để giải quyết đúng vấn đề đó bằng cách đóng gói ứng dụng cùng mọi thứ nó cần vào một đơn vị chuẩn hoá. Bài viết này sẽ đưa bạn đi từ những khái niệm cơ bản nhất đến việc viết Dockerfile và chạy nhiều dịch vụ cùng lúc với Docker Compose.

Docker là gì và vì sao nên dùng?

Docker là nền tảng cho phép bạn đóng gói ứng dụng vào container – một môi trường chạy cô lập, nhẹ, dùng chung nhân hệ điều hành với máy chủ. Khác với máy ảo (VM) phải giả lập cả một hệ điều hành đầy đủ, container khởi động nhanh trong vài giây và tiêu tốn ít tài nguyên hơn nhiều.

Lợi ích thực tế khi dùng Docker:

  • Môi trường nhất quán: máy dev, máy test và production chạy cùng một image.
  • Onboarding nhanh: thành viên mới chỉ cần cài Docker rồi chạy một lệnh là có đủ ứng dụng, database, cache.
  • Dễ triển khai và mở rộng: image có thể đẩy lên registry và chạy ở bất cứ đâu hỗ trợ container.
  • Cô lập phụ thuộc: dự án A dùng PostgreSQL 15, dự án B dùng PostgreSQL 17 cũng không xung đột.

Ba khái niệm cốt lõi: Image, Container, Registry

Trước khi gõ lệnh, bạn cần phân biệt rõ ba khái niệm:

  1. Image là bản mẫu chỉ đọc, chứa hệ điều hành tối giản, runtime, thư viện và mã nguồn. Image được xây dựng theo từng lớp (layer).
  2. Container là một thực thể đang chạy được tạo ra từ image. Bạn có thể chạy nhiều container từ cùng một image.
  3. Registry là nơi lưu trữ và chia sẻ image, phổ biến nhất là Docker Hub, ngoài ra còn GitHub Container Registry hay các registry của nhà cung cấp cloud.

Một cách ví von dễ nhớ: image giống như class, còn container giống như object được khởi tạo từ class đó.

Viết Dockerfile đầu tiên

Dockerfile là tệp văn bản mô tả từng bước để tạo image. Dưới đây là ví dụ cho một ứng dụng Node.js đơn giản:

FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
ENV NODE_ENV=production
EXPOSE 3000
USER node
CMD ["node", "server.js"]

Ý nghĩa từng chỉ thị:

  • FROM chọn image nền. Bản alpine có dung lượng nhỏ, phù hợp cho production.
  • WORKDIR đặt thư mục làm việc bên trong container.
  • COPY package*.json rồi RUN npm ci trước khi copy toàn bộ mã nguồn. Đây là mẹo tận dụng cache: khi bạn chỉ sửa code mà không đổi dependency, Docker sẽ dùng lại layer đã cài package, giúp build nhanh hơn rất nhiều.
  • USER node chạy ứng dụng bằng người dùng không có quyền root, tăng tính an toàn.
  • CMD là lệnh mặc định khi container khởi động.

Đừng quên tạo tệp .dockerignore để loại bỏ node_modules, .git, .env khỏi build context. Việc này vừa giảm kích thước image vừa tránh vô tình đưa thông tin bí mật vào image.

Build và chạy:

docker build -t my-app:1.0 .
docker run -d -p 3000:3000 --name my-app my-app:1.0
docker logs -f my-app

Tham số -p 3000:3000 ánh xạ cổng 3000 của máy bạn vào cổng 3000 trong container.

Multi-stage build: image gọn hơn

Với ứng dụng cần bước build (TypeScript, React, Go...), bạn nên dùng multi-stage build. Giai đoạn đầu chứa đầy đủ công cụ biên dịch, giai đoạn sau chỉ copy kết quả cần thiết sang một image nền sạch:

FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
 
FROM node:22-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/package*.json ./
RUN npm ci --omit=dev
CMD ["node", "dist/server.js"]

Image cuối cùng không chứa mã nguồn TypeScript hay devDependencies, nên nhỏ hơn và có bề mặt tấn công hẹp hơn.

Docker Compose: chạy nhiều dịch vụ cùng lúc

Ứng dụng thực tế hiếm khi đứng một mình. Thường bạn cần thêm database, Redis, có khi cả hàng đợi tin nhắn. Gõ từng lệnh docker run với hàng loạt tham số rất dễ sai. Docker Compose cho phép khai báo toàn bộ hệ thống trong một tệp YAML:

services:
  app:
    build: .
    ports: ["3000:3000"]
    environment:
      DATABASE_URL: postgres://postgres:secret@db:5432/postgres
    depends_on: [db]
  db:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: secret
    volumes: ["pgdata:/var/lib/postgresql/data"]
volumes:
  pgdata:

Vài điểm đáng chú ý:

  • Các service trong cùng tệp Compose được đặt chung một mạng, nên app có thể gọi database qua hostname db.
  • Volume pgdata giúp dữ liệu PostgreSQL không bị mất khi container bị xoá và tạo lại.
  • depends_on chỉ đảm bảo thứ tự khởi động, không đảm bảo database đã sẵn sàng nhận kết nối. Muốn chắc chắn hơn, hãy kết hợp healthcheck hoặc cho ứng dụng tự thử kết nối lại.

Các lệnh thường dùng:

docker compose up -d        # khởi động toàn bộ ở chế độ nền
docker compose ps           # xem trạng thái
docker compose logs -f app  # theo dõi log của app
docker compose down         # dừng và xoá container

Những lỗi người mới hay gặp

  • Dùng tag latest cho mọi thứ: khó tái lập và khó biết đang chạy phiên bản nào. Hãy gắn tag rõ ràng.
  • Đưa mật khẩu thẳng vào Dockerfile: thông tin này nằm vĩnh viễn trong các layer. Hãy dùng biến môi trường, tệp .env không commit lên git, hoặc cơ chế secrets.
  • Quên volume cho dữ liệu: chạy database không có volume đồng nghĩa với mất sạch dữ liệu khi xoá container.
  • Image quá nặng: chọn image nền tối giản, dùng multi-stage build và .dockerignore.

Kết luận

Docker không khó như nhiều người nghĩ. Chỉ cần nắm vững image, container, cách viết một Dockerfile tận dụng cache tốt và khai báo hệ thống bằng Docker Compose, bạn đã có thể chuẩn hoá môi trường cho cả đội và rút ngắn đáng kể thời gian thiết lập dự án. Hãy bắt đầu bằng việc "docker hoá" một dự án nhỏ của chính bạn, sau đó dần tìm hiểu thêm về bảo mật image, registry riêng và các công cụ điều phối như Kubernetes.

Nguồn tham khảo