Dockerfile Checklist - Checklist mình sử dụng khi viết Dockerfile

Nếu các bạn thường xuyên làm việc với Kubernetes hay CI/CD pipelines (như mình đã chia sẻ ở các series trước), thì chắc chắn Dockerfile là một thứ không thể thiếu. Viết Dockerfile thì dễ — ai cũng có thể làm được chỉ sau 5 phút Google — nhưng viết sao cho tối ưu, bảo mật và lightweight thì lại là một câu chuyện khác.
Hôm nay, mình sẽ tổng hợp lại một Dockerfile Creation Checklist mà mình thường dùng để review code cho anh em trong team. Bài viết này sẽ đi qua 3 khía cạnh chính: Configuration, Performance, và Security. Với mỗi ý, mình sẽ đưa ra ví dụ Good và Bad để mọi người dễ hình dung nhé.
Let's go! ⏩️
Lưu ý: Các ý mình list dưới đây là do mình tổng hợp từ nhiều nguồn, bạn hãy lựa chọn cái nào phù hợp có thể áp dụng cho dự án của mình nhé. Bạn không cần phải áp dụng toàn bộ các ý phía dưới vào Dockerfile của bạn.
Configuration
Các ý trong phần này tập trung vào việc làm cho image của bạn hoạt động ổn định, dễ cấu hình và tương thích tốt trên nhiều môi trường.
Support multiple machine architecture (amd64, arm64)
Đừng bao giờ hardcode platform nếu bạn muốn image chạy được trên cả Macbook M1/M2 (ARM) và server truyền thống (AMD).
- Bad: Hardcode trong lúc build hoặc pull base image chỉ hỗ trợ 1 platform mà không check.
- Good: Sử dụng biến TARGETARCH hoặc BUILDPLATFORM (của Docker Buildx) để tự động chọn binary phù hợp.
ARG TARGETARCH
COPY bin/app-${TARGETARCH} /app/main
Set explicit CMD / ENTRYPOINT / Run application process as PID 1
Luôn ưu tiên Exec form ["executable", "param1", "param2"] thay vì Shell form command param1. Shell form sẽ bọc command của bạn trong /bin/sh -c, khiến process của bạn không phải là PID 1 và không nhận được Unix signals (như SIGTERM để tắt graceful shutdown).
Container nên có một init process (PID 1) để xử lý việc thu dọn "zombie processes" và chuyển tiếp signals.
- Bad: Chạy trực tiếp ứng dụng java/python mà không có init system xử lý signal.
- Good: Sử dụng cờ --init khi chạy docker run hoặc dùng tini trong Dockerfile.
ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
ENTRYPOINT ["/tini", "--", "/docker-entrypoint.sh"]
Add a .dockerignore file
Đừng gửi cả thư mục .git, node_modules hay các file log local vào Docker daemon context. Nó làm build chậm và tăng kích thước image vô ích.
- Bad: COPY . . (hốt cả thế giới vào image).
- Good: Có file .dockerignore:
.git
node_modules
build
*.log
.env
Define listen port (EXPOSE)
Đây là documentation cho người dùng biết container lắng nghe ở port nào.
- Bad: Không khai báo gì cả, người chạy phải tự đoán.
- Good:
EXPOSE 8080
Define HEALTHCHECK
Giúp Docker (và Kubernetes) biết ứng dụng của bạn còn "sống" hay không để restart.
- Bad: Không có healthcheck (container chạy nhưng app bên trong treo cứng mà orchestrator không biết).
- Good:
HEALTHCHECK \
CMD curl -f http://localhost:8080/health || exit 1
Add labels (org.opencontainers.image.*)
Thêm metadata chuẩn OCI giúp quản lý image dễ hơn.
- Bad: Image "vô danh", không biết ai maintain.
- Good:
LABEL org.opencontainers.image.authors="Hoang Viet"
LABEL org.opencontainers.image.source="https://github.com/viet111/my-app"
Pin package + dependency versions
Tránh việc hôm nay build chạy, mai build lỗi do package tự update version mới.
- Bad:
apt-get install python3 - Good:
apt-get install python3=3.9.1-r0
Reproducible builds: use ARG
Linh hoạt thay đổi version mà không cần sửa cứng trong Dockerfile.
- Bad:
FROM node:14 - Good:
ARG NODE_VERSION=18
FROM node:${NODE_VERSION}-alpine
Performance
Tại phần này mình sẽ nêu ra các ý mà các bạn có thể áp dụng để có một image build nhanh, Image nhỏ, Cache ngon.
Use minimal, official base image
Dùng các bản phân phối nhỏ gọn như Alpine hoặc Distroless.
- Bad:
FROM ubuntu:latest(Nặng, chứa quá nhiều thứ không cần thiết). - Good: FROM node:18-alpine hoặc FROM gcr.io/distroless/static-debian11.
Use multi-stage builds
Đây là kỹ thuật quan trọng nhất để giảm size image cho các ngôn ngữ biên dịch (Go, Java, C++).
- Bad: Build và Run trên cùng 1 stage. Image chứa cả source code, compiler (GCC, Maven...) và artifact.
- Good: Stage 1 build, Stage 2 chỉ copy file binary sang một image rỗng (scratch/alpine).
# Build Stage
FROM golang:1.19 AS builder
WORKDIR /app
COPY . .
RUN go build -o main .
# Run Stage
FROM alpine:latest
COPY /app/main .
CMD ["./main"]
Prefer COPY over ADD
ADD có thể giải nén file tar và tải file từ URL (khó kiểm soát cache). COPY đơn giản và rõ ràng hơn.
- Bad:
ADD . /app - Good:
COPY . /app(Chỉ dùng ADD khi bạn thực sự cần tải file remote hoặc giải nén tar).
Minimize number of layers
Kết hợp các lệnh RUN lại với nhau để giảm số lớp layer trung gian.
- Bad:
RUN apt-get update
RUN apt-get install -y vim
RUN apt-get install -y curl
- Good:
RUN apt-get update && apt-get install -y \
vim \
curl \
&& rm -rf /var/lib/apt/lists/*
Order layers for cache efficiency
Sắp xếp các câu lệnh trong Dockerfile từ ít thay đổi (OS, Dependencies) đến hay thay đổi (Source Code).
- Bad: Copy source code trước khi cài dependencies. Mỗi lần sửa code, Docker phải cài lại thư viện từ đầu.
COPY . .
RUN npm install # Mất cache mỗi khi code đổi
- Good:
COPY package.json .
RUN npm install # Được cache nếu package.json không đổi
COPY . .
Security
Bảo mật container là một phần không thể thiếu trong quy trình DevSecOps.
Use base image with sha256 hash
Tags như :latest hay :18-alpine có thể bị thay đổi nội dung (mutable). Hash sha256 là bất biến. Việc định nghĩa mã SHA256 giúp ta tránh các cuộc tấn công supply chain.
- Bad:
FROM node:lts-alpine - Good:
FROM node@sha256:52a6d123...(Đảm bảo chính xác 100% nội dung image).
Remove shell, package manager, debug tools
Kẻ tấn công xâm nhập được vào container cũng không làm gì được nếu không có shell (sh, bash) hay curl/wget.
- Bad: Cài đủ thứ vim, curl, net-tools để tiện debug.
- Good: Dùng Distroless images (không shell, không package manager).
Run static scans (hadolint, trivy)
Tích hợp vào CI/CD pipeline để chặn build nếu Dockerfile vi phạm quy tắc hay chứa các lỗ hổng CVE có mức ảnh hưởng HIGH, CRITICAL,...
Ví dụ: Chạy trivy image my-app:latest trước khi push lên registry.
Use non-root user
Mặc định Docker chạy với quyền root. Nếu hacker thoát khỏi container (container breakout), họ sẽ có quyền root trên máy host.
- Bad: Không khai báo USER (mặc định là root).
- Good:
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
Don’t put any secrets, credentials in Dockerfile
Tuyệt đối không hardcode API Key, Password. Cho dù bạn có remove secret ở các step sau trong Dockerfile thì secret vẫn sẽ tồn tại ở layer trước đó.
- Bad:
ENV DB_PASSWORD=secret123
COPY id_rsa /root/.ssh/
- Good: Sử dụng Environment Variables lúc chạy (docker run -e) hoặc mount secret (--secret trong Docker Buildkit).
Hy vọng checklist này sẽ giúp anh em tự tin hơn khi viết Dockerfile. Lưu lại và áp dụng ngay cho dự án của mình nhé! Nếu thấy hay, đừng quên upvote và follow mình để đón chờ những bài viết tiếp theo về DevOps và Kubernetes.
Happy Coding! 👨💻
Nếu như bạn đang gặp khó khăn trong vấn đề chuyên môn, cần người hỗ trợ về mặt hệ thống, DevOps tools hay cần định hướng trong công việc thì mình tự tin có thể hỗ trợ được bạn. Liên hệ với mình để trao đổi thêm nhé https://hoangviet.io.vn/, mình rất vui khi được trao đổi và cộng tác với bạn.
All rights reserved