Có bao giờ bạn tự hỏi mình đã dùng Docker hiệu quả như bản thân nó có thể?
Hiện nay, trong phát triển phần mềm, hầu hết các ứng dụng, dịch vụ đều sử dụng Docker. Docker giúp các nhà phát triển xây dựng, chia sẻ, chạy và xác minh ứng dụng ở mọi nơi mà không cần cấu hình hoặc quản lý môi trường phức tạp.
Trong bài viết hôm nay mình sẽ chia sẻ một số "tip" để sử dụng Docker hiệu quả hơn khi nó phát triển để phù hợp, nhanh, nhẹ hơn cho quá trình phát triển phần mềm liền mạch.
1. Dùng .dockerignore
Sử dụng tệp .dockerignore
để loại trừ các tệp hoặc thư mục khỏi ngữ cảnh build image.
Khi bạn chạy lệnh build, build clients sẽ tìm kiếm tệp có tên .dockerignore
trong thư mục gốc của ngữ cảnh. Nếu tệp này tồn tại, các tệp và thư mục khớp với các mẫu trong tệp sẽ bị xóa khỏi ngữ cảnh build trước khi được gửi đến trình build.
Nếu bạn sử dụng nhiều Dockerfile, bạn có thể sử dụng các ignore-file khác nhau cho mỗi Dockerfile. File ignore dành riêng cho Dockerfile tương ứng sẽ được ưu tiên hơn tệp .dockerignore
ở gốc của ngữ cảnh build nếu cả hai đều tồn tại. Xem ví dụ sau:
.
├── index.ts
├── src/
├── docker
│ ├── build.Dockerfile
│ ├── build.Dockerfile.dockerignore
│ ├── lint.Dockerfile
│ ├── lint.Dockerfile.dockerignore
│ ├── test.Dockerfile
│ └── test.Dockerfile.dockerignore
├── package.json
└── package-lock.json
Thật rủi ro và vô lý khi đem những thứ như .git
, .env
, và node_modules
lên production. Sử dụng .dockerignore
, thu gọn build context của bạn, và giữ các secrets cùng những thứ không cần thiết ra khỏi image.
2. Loại bỏ version
trong docker compose
Thuộc tính cấp cao nhất version
được định nghĩa bởi Compose Specification để tương thích ngược. Thuộc tính này chỉ mang tính thông tin và bạn sẽ nhận được thông báo cảnh báo rằng thuộc tính này đã lỗi thời nếu sử dụng.
Thay vào đó, hiện tại thuộc tính cấp cao nhất là name
, được định nghĩa bởi Compose Specification là tên dự án sẽ được sử dụng nếu bạn không đặt tên rõ ràng. Compose cung cấp cho bạn một cách để ghi đè tên này và đặt tên dự án mặc định sẽ được sử dụng nếu phần tử cấp cao nhất name
không được đặt.
Ví dụ một docker-compose.yml hiện đại:
name: myapp services: foo: image: busybox command: echo "I'm running ${COMPOSE_PROJECT_NAME}"
3. Multi-stage builds
Với multi-stage builds, bạn sử dụng nhiều câu lệnh FROM
trong Dockerfile
của mình. Mỗi lệnh FROM
có thể sử dụng một base image khác nhau và mỗi lệnh bắt đầu một giai đoạn mới của bản dựng. Bạn có thể sao chép có chọn lọc các thành phần từ giai đoạn này sang giai đoạn khác, bỏ lại mọi thứ bạn không muốn trong image cuối cùng. Điều này giúp image nhẹ hơn nhiều khi bạn loại bỏ các thứ như build tools, compilers, và test dependencies...
Ví dụ build image với multi-stage:
FROM golang:1.24 AS build
WORKDIR /src
COPY <<EOF /src/main.go
package main import "fmt" func main() { fmt.Println("hello, world")
}
EOF
RUN go build -o /bin/hello ./main.go FROM scratch
COPY /bin/hello /bin/hello
CMD ["/bin/hello"]
4. Chạy với user không phải root
Việc chạy container với quyền root trong production có thể là một rủi ro. Một số vấn lỗ hỏng bảo mật liên quan đến linux có thể gây hại cho container của bạn: CVE-2022-0847, CVE-2019-5736...
Hãy tạo một user không phải root
trong Dockerfile và chuyển sang nó:
RUN useradd -m appuser
USER appuser
Chú ý cần đảm bảo rằng các volume mount thuộc về user không phải root mà bạn đã thêm để mọi thứ hoạt động.
5. Thêm Healthchecks
Thuộc tính healthcheck
khai báo một kiểm tra được chạy để xác định xem các container dịch vụ có "khỏe mạnh" hay không. Nó hoạt động theo cùng một cách và có cùng các giá trị mặc định như lệnh HEALTHCHECK trong Dockerfile. File Compose có thể ghi đè các giá trị được đặt trong Dockerfile.
healthcheck: test: ["CMD", "curl", "-f", "http://localhost"] interval: 1m30s timeout: 10s retries: 3 start_period: 40s start_interval: 5s
Một trường hợp sử dụng phổ biến là để phát hiện trạng thái sẵn sàng của dịch vụ với thuộc tính condition
theo tùy chọn service_healthy
. Điều này chỉ rõ rằng một sự phụ thuộc được mong đợi là healthy, được định nghĩa bằng healthcheck, trước khi bắt đầu một dịch vụ phụ thuộc.
Trong ví dụ dưới Compose chờ kiểm tra tình trạng để chuyển các phụ thuộc được đánh dấu bằng service_healthy
. db
được mong đợi là "ổn định" (chỉ ra bởi healthcheck
) trước khi service web
được tạo.
services: web: build: . depends_on: db: condition: service_healthy restart: true redis: condition: service_started redis: image: redis db: image: postgres healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 10s retries: 5 start_period: 30s timeout: 10s
6. Sử dụng Docker BuildKit
Docker BuildKit là một công cụ build mới của Docker, giúp:
- Tăng tốc độ build image.
- Tăng khả năng tái sử dụng cache (layer caching).
- Hỗ trợ parallel builds.
- Cải thiện bảo mật và logs rõ ràng hơn.
Một trong những điểm mạnh của BuildKit là khả năng cache thư viện cục bộ, đặc biệt hữu ích khi làm việc với package manager như npm, pip, apt, go mod, composer,...
Giả sử bạn đang dùng Node.js, và bạn muốn cache thư viện npm để tránh phải npm install lại mỗi lần. Hãy xem ví dụ sử dụng BuildKit với Dockerfile bên dưới, các điểm chính:
--mount=type=cache,target=/root/.npm
: Chỉ định một cache mount cho thư mục npm cache.- Docker sẽ giữ lại các gói đã cài, và lần build sau sẽ dùng lại cache đó nếu package.json không thay đổi.
- Giúp tiết kiệm thời gian rất nhiều khi thường xuyên rebuild.
# syntax=docker/dockerfile:1.4
FROM node:18 WORKDIR /app # Copy package files trước để tận dụng cache
COPY package.json package-lock.json ./ # Dùng cache để lưu thư mục node_modules
RUN \ npm ci # Copy source code vào sau
COPY . . CMD ["node", "index.js"]
Lưu ý: Dòng đầu tiên # syntax=docker/dockerfile:1.4 là để Docker biết sử dụng cú pháp BuildKit nâng cao.
Cách build tương ứng cho ví dụ trên sẽ là:
DOCKER_BUILDKIT=1 docker build -t my-node-app .
7. Tối ưu cho môi trường development
Nếu bạn đang chờ 2 phút cho mỗi lần build hoặc xem logs từ docker logs -f
, bạn có thể làm tốt hơn.
- Dùng
docker compose watch
để tự động rebuild khi có thay đổi file, tính năng này yêu cầu Docker Compose v2.17+ - Hãy bind-mounted volumes để mount code từ máy host vào container bằng cách sử dụng volumes: trong compose
- Thực hiện hot reload giúp app tự restart khi code thay đổi, tùy theo ngôn ngữ bạn sử dụng trong code mà có thể dùng nodemon, flask auto-reload,..vân vân, mây mây...
Mình sẽ thực hiện một ví dụ minh họa với serivce viết bằng NodeJS như sau:
- Cấu trúc project:
myapp/
├── Dockerfile
├── docker-compose.yml
├── .dockerignore
├── package.json
└── src/ └── index.js
- Nội dung Dockerfile:
# syntax=docker/dockerfile:1.4
FROM node:18 WORKDIR /app COPY package*.json ./ # Cache npm install
RUN \ npm install # Copy source sau
COPY . . CMD ["npx", "nodemon", "src/index.js"]
- docker-compose.yml
name: "myapp" services: app: build: context: . volumes: - .:/app # Mount code - /app/node_modules # Tránh ghi đè node_modules ports: - "3000:3000" command: ["npx", "nodemon", "src/index.js"] develop: watch: - path: . action: rebuild # Khi file thay đổi, Docker rebuild lại container
- Trong code sẽ dùng
nodemon
để hot reload - Cách build:
DOCKER_BUILDKIT=1 docker build -t my-node-app .
- Xem logs realtime như kiểu
npm run dev
:
docker compose logs -f app
Bạn có thói quen Docker lỗi thời nào mà phải bỏ không? Có những kiến thức mới và hay ho để bổ sung cho bài viết không? Hãy chia sẻ trong comments nhé.
Hy vọng với những kiến thức mình chia sẻ có thể giúp ích và cải thiện cách dùng Docker trong môi trường phát triển phần mềm hiện đại.
Tạm biệt và hẹn gặp lại các bạn!