Tối ưu Docker Image với Multi-stage Builds: Từ 1GB xuống vài chục MB
Hướng dẫn tối ưu Docker Image bằng Multi-stage Builds với Node.js, Next.js và React; phân tích layer, cache, native modules, bảo mật và cách đo dung lượng thực tế.
ShanverseUpdated 7 min read— viewsLoading views
Docker
Performance
Vì sao Docker Image phình lên gần 1GB?
Một Dockerfile đơn giản thường giữ lại mọi thứ:
Image cuối có thể chứa:
Base Image đầy đủ.
Compiler và build tools.
devDependencies như TypeScript, ESLint, bundler.
Source code không cần ở runtime.
npm cache và file tạm.
Test, coverage, documentation.
Artifact build lẫn dependency build-time.
Đáng chú ý: xóa file ở một layer sau không luôn lấy lại dung lượng ở layer trước.
Cache đã được ghi vào layer đầu. Layer sau chỉ che/xóa nó khỏi filesystem cuối; bytes cũ có thể vẫn nằm trong lịch sử Image.
Comments
Multi-stage Build hoạt động thế nào?
Mermaid
Loading diagram…
Mỗi FROM mở một stage mới. Stage production dùng COPY --from=<stage> để lấy đúng output cần thiết.
Stage builder có thể rất lớn nhưng không trở thành Image phát hành. Chỉ filesystem của stage cuối được dùng làm kết quả mặc định.
Ví dụ Node.js + TypeScript production-ready
Các stage có trách nhiệm rõ ràng:
deps: có devDependencies để build.
builder: compile TypeScript thành dist/.
prod-deps: chỉ cài dependency runtime.
runner: chỉ chứa Node.js, production dependencies và output.
Nếu bundler tạo một file self-contained không cần node_modules, bỏ hẳn stage prod-deps để Image nhỏ hơn nữa.
So sánh dung lượng đúng cách
Ví dụ minh họa:
Đây không phải cam kết cố định. Chromium, Sharp, TensorFlow, native libraries hoặc large assets có thể khiến Image vẫn lớn.
React SPA: nơi dễ đạt vài chục MB
React/Vite chỉ cần Node.js lúc build. Production có thể dùng web server nhỏ:
Node.js, npm, source và node_modules không đi vào stage cuối. Đây là trường hợp điển hình có thể giảm Image từ hàng trăm MB hoặc hơn xuống vài chục MB.
Next.js với standalone output
Trong next.config.js:
Sau npm run build, chỉ copy output cần thiết:
Standalone output loại bỏ phần lớn dependency không cần cho server runtime. Monorepo có thể cần cấu hình output tracing root.
Tối ưu cache mà không làm Image lớn
BuildKit cache mount tăng tốc download nhưng không được lưu thành filesystem layer của Image cuối.
Nếu COPY . . đứng trước npm ci, mọi thay đổi source có thể khiến dependency layer phải build lại.
.dockerignore vẫn rất quan trọng
Multi-stage Build không ngăn secret được gửi vào builder. .dockerignore giảm build context và nguy cơ copy nhầm, nhưng secret thực sự nên dùng BuildKit secret mount hoặc secret manager.
Không dùng ARG hoặc ENV cho token nhạy cảm vì metadata/layer/cache có thể lưu lại giá trị.
Native modules: lỗi thường gặp khi copy giữa stages
Không nên build native dependency trên Alpine rồi copy sang Debian, hoặc ngược lại:
Binary có thể không chạy. Nguyên tắc:
Giữ cùng OS family/libc giữa build và runtime.
Giữ architecture tương thích.
Cài runtime shared libraries cần thiết ở stage cuối.
Test trên đúng target platform.
Alpine nhỏ hơn nhưng không luôn tốt hơn. Debian Slim thường tương thích native packages dễ hơn.
Chỉ copy những gì thực sự cần
Không nên:
Nên:
Kiểm kê runtime contract: executable, production dependencies, migrations, templates, CA certificates, static assets và config mặc định.
Dùng target stage cho development và CI
Build một stage cụ thể:
Một Dockerfile có thể phục vụ test và production mà không đưa test tools vào Image cuối.
Distroless có phải lựa chọn tốt nhất?
Distroless có thể giảm shell/package manager và attack surface, nhưng debug khó hơn. Chỉ dùng khi:
Application không cần shell.
Runtime libraries đã rõ.
Team có quy trình debug bằng ephemeral/debug container.
Vulnerability scanning và incident workflow hỗ trợ.
Image nhỏ không đồng nghĩa an toàn tuyệt đối; dependency và quyền runtime vẫn quan trọng.
Các lỗi phổ biến
Cài devDependencies trong stage cuối
Image production vẫn lớn. Tách deps và prod-deps.
Xóa file ở layer sau
Bytes có thể còn ở layer cũ. Tránh đưa chúng vào final stage ngay từ đầu.
Copy toàn bộ builder
Vô tình mang source, cache, test và compiler sang runtime.
Chọn base nhỏ nhưng thiếu thư viện
Image build thành công nhưng crash ở runtime. Kiểm tra native dependencies.
Chỉ tối ưu con số MB
Một Image nhỏ nhưng build chậm, khó debug hoặc không cập nhật security patches chưa chắc tốt hơn.
Checklist production
Dockerfile có các stage được đặt tên rõ ràng.
Dependency install dựa trên lockfile.
Source được copy sau dependency manifests để tận dụng cache.
Final stage chỉ chứa runtime artifact.
Build tools và devDependencies không vào production.
Builder/runner tương thích OS và architecture.
Process chạy non-root.
Không có secret trong context, layer hoặc build args.
Image được đo bằng history và system df.
Startup, health check và graceful shutdown đã được test.
Image được scan và cập nhật base định kỳ.
Tổng kết
Multi-stage Builds tối ưu Image bằng cách không đưa rác build vào stage cuối, thay vì cố xóa sau đó. Công thức cốt lõi:
Hướng dẫn Docker Hardening thực tế với non-root user, multi-stage builds, minimal image, read-only filesystem, Linux capabilities, seccomp, scanning và supply-chain security.
Docker Compose khi nào không còn đủ? So sánh Kubernetes và Docker Swarm về multi-host, self-healing, scaling, rolling update, networking, secrets và lộ trình migration production.
Hướng dẫn tự động deploy ứng dụng Docker lên VPS Ubuntu và cloud providers bằng GitHub Actions: AWS ECS/App Runner/EC2, Vercel, Render, secrets, healthcheck, rollback và zero-downtime strategy.