Dockerfile production: 15 cách giảm image, tăng bảo mật
Hướng dẫn viết Dockerfile production với multi-stage build, cache, .dockerignore, non-root user, pin version, healthcheck và secret an toàn.
Mục lục bài viết
VPS & Hosting cho dự án của bạn
Khám phá các gói máy chủ và lưu trữ phù hợp từ website cá nhân đến ứng dụng production.
Dockerfile chạy được chưa chắc đã phù hợp production. Một Dockerfile kém có thể tạo image rất lớn, chứa source/secret không cần thiết, chạy bằng root và build chậm mỗi lần source thay đổi.
Docker khuyến nghị dùng multi-stage build để tách build environment khỏi runtime và chỉ đưa file cần thiết vào image cuối.
Dockerfile mẫu chưa tối ưu
FROM node:22
WORKDIR /app
COPY . .
RUN npm install
CMD ["npm", "start"]
Vấn đề:
- Copy toàn bộ context sớm.
- Cache dependency dễ bị mất.
- Có thể copy
.env,.git, log. - Cài dev dependency vào runtime.
- Chạy bằng root.
- Base image lớn.
- Không build trước.
- Không pin lock file bằng
npm ci.
1. Dùng base image chính thức và phù hợp
FROM node:22-bookworm-slim
Hoặc:
FROM node:22-alpine
Không có image nhỏ nhất phù hợp mọi app.
Alpine
Ưu điểm:
- Nhỏ.
- Ít package mặc định.
Hạn chế:
- Dùng musl thay glibc.
- Một số native dependency cần xử lý khác.
- Debug khó hơn nếu thiếu tool.
Debian slim
Ưu điểm:
- Tương thích glibc rộng.
- Dễ cài native package.
Hạn chế:
- Lớn hơn Alpine.
Chọn dựa trên dependency và test, không chỉ kích thước.
2. Pin major hoặc version có kiểm soát
FROM node:22.XX.X-bookworm-slim
Pin quá lỏng có thể nhận thay đổi bất ngờ. Pin quá chặt mà không cập nhật sẽ bỏ lỡ security fixes.
Cần quy trình cập nhật base image định kỳ.
Với khả năng tái lập cao, có thể pin digest:
FROM node:22-bookworm-slim@sha256:...
Digest phải được cập nhật tự động hoặc định kỳ.
3. Dùng multi-stage build
Ví dụ NestJS:
FROM node:22-bookworm-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:22-bookworm-slim AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:22-bookworm-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/main.js"]
Image cuối không cần TypeScript compiler, source test và build cache.
4. Tối ưu layer cache
Không nên:
COPY . .
RUN npm ci
Mỗi thay đổi source làm mất cache dependency.
Nên:
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
Dependency chỉ cài lại khi manifest/lock file đổi.
5. Dùng npm ci
RUN npm ci
npm ci:
- Yêu cầu lock file.
- Xóa
node_moduleshiện có. - Cài đúng lock.
- Phù hợp CI/build.
Runtime:
RUN npm ci --omit=dev
Cần kiểm tra dependency runtime có bị đặt nhầm trong devDependencies không.
6. Tạo .dockerignore
node_modules
dist
.next
coverage
.git
.github
.env
.env.*
*.log
Dockerfile*
compose*.yaml
README.md
Lợi ích:
- Build context nhỏ.
- Giảm nguy cơ copy secret.
- Build nhanh.
- Tránh
node_moduleskhác OS.
Không ignore file thật sự cần build.
7. Không copy secret vào image
Sai:
COPY .env.production .env
Image có thể được export hoặc inspect. Secret phải được inject lúc chạy:
environment:
DATABASE_URL: ${DATABASE_URL}
Hoặc secret manager.
Build secret dùng BuildKit secret mount, không dùng ARG nếu giá trị không nên tồn tại trong history/cache.
8. Không dùng ARG cho secret
Sai:
ARG NPM_TOKEN
RUN npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN
Secret có thể lộ qua layer/history/cache.
Với BuildKit:
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci
Build:
docker build \
--secret id=npmrc,src="$HOME/.npmrc" \
.
Kiểm tra hỗ trợ BuildKit và CI.
9. Chạy bằng non-root user
Official Node image có user node.
USER node
Trước đó file phải thuộc user phù hợp:
COPY --chown=node:node --from=builder /app/dist ./dist
Ví dụ:
FROM node:22-bookworm-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --chown=node:node package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --chown=node:node --from=builder /app/dist ./dist
USER node
CMD ["node", "dist/main.js"]
Lưu ý RUN npm ci trước USER node có thể tạo file root-owned. Sau đó app chỉ cần đọc, thường vẫn hoạt động; đánh giá thư mục cần ghi.
10. Chỉ cho app ghi nơi cần
Nếu app cần upload:
RUN mkdir -p /app/uploads \
&& chown -R node:node /app/uploads
USER node
Production nên mount volume vào /app/uploads.
Không cho app sửa source runtime.
11. Dùng exec form cho CMD
Nên:
CMD ["node", "dist/main.js"]
Không nên:
CMD node dist/main.js
Exec form giúp signal được gửi trực tiếp tới process, hỗ trợ graceful shutdown tốt hơn.
Nếu cần init process để xử lý zombie/signal, cân nhắc --init hoặc init phù hợp.
12. Một process chính mỗi container
Container thường nên có một process chính rõ ràng.
Không chạy:
Nginx + Node.js + PostgreSQL
trong cùng container chỉ để “đơn giản”.
Compose quản lý nhiều service:
services:
nginx:
api:
database:
Một process không phải luật tuyệt đối, nhưng giúp lifecycle, log và scale dễ hơn.
13. Healthcheck phù hợp
Dockerfile:
HEALTHCHECK \
--interval=30s \
--timeout=5s \
--start-period=20s \
--retries=3 \
CMD node -e \
"fetch('http://127.0.0.1:3000/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"
Hoặc đặt healthcheck trong Compose để tùy môi trường.
Healthcheck phải:
- Nhẹ.
- Có timeout.
- Không thay đổi dữ liệu.
- Phản ánh readiness phù hợp.
14. EXPOSE không public port
EXPOSE 3000
Chỉ mô tả port dự kiến.
Để publish:
docker run -p 3000:3000 image
Compose production:
ports:
- "127.0.0.1:3000:3000"
15. Gom apt install và dọn cache
Debian:
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
Không tách:
RUN apt-get update
RUN apt-get install -y package
vì cache có thể làm package index cũ.
Chỉ cài package cần runtime.
16. Không dùng latest thiếu kiểm soát
Sai:
FROM node:latest
Compose:
image: postgres:latest
latest không bảo đảm version mới nhất theo nghĩa mong muốn và khó rollback.
Dùng tag rõ:
image: postgres:18
Với app:
deployeasy-api:git-a1b2c3d
17. Gắn metadata
LABEL org.opencontainers.image.title="DeployEasy API"
LABEL org.opencontainers.image.source="https://github.com/example/deployeasy"
LABEL org.opencontainers.image.revision=$GIT_COMMIT
Nếu dùng ARG:
ARG GIT_COMMIT
LABEL org.opencontainers.image.revision=$GIT_COMMIT
Build:
docker build \
--build-arg GIT_COMMIT="$(git rev-parse HEAD)" \
-t deployeasy-api:"$(git rev-parse --short HEAD)" \
.
Không đưa secret vào label.
18. Tách build dependency và runtime dependency
Native module có thể cần build tool:
python
make
g++
Chỉ cài ở builder stage, không mang vào runtime.
Ví dụ:
FROM node:22-bookworm-slim AS builder
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
python3 \
make \
g++ \
&& rm -rf /var/lib/apt/lists/*
Runtime stage chỉ copy artifact và production node_modules.
19. Next.js standalone
Trong next.config.ts:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;
Build tạo .next/standalone chứa file cần thiết để self-host.
Runtime cần copy thêm static/public phù hợp:
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder --chown=node:node \
/app/.next/standalone ./
Chi tiết nằm trong bài deploy Next.js riêng.
20. Scan image
Dùng công cụ quét của registry hoặc hệ thống CI.
Cần phân biệt:
- Vulnerability trong OS package.
- Dependency ứng dụng.
- False positive.
- Package không dùng runtime.
- Severity và exploitability.
Không chỉ chạy scan rồi bỏ qua kết quả.
21. Build đa kiến trúc
Máy dev Apple Silicon có thể build arm64, trong khi VPS là amd64.
Kiểm tra:
uname -m
docker image inspect image \
--format '{{.Architecture}}'
Buildx:
docker buildx build \
--platform linux/amd64 \
-t image:tag \
--load \
.
CI registry có thể build multi-platform.
22. Reproducible build
Để tăng khả năng tái lập:
- Lock dependency.
- Pin base image/tag.
- Không tải artifact không version.
- Ghi commit SHA.
- Build trong CI.
- Không sửa container đang chạy.
- Lưu SBOM/provenance khi cần.
Dockerfile NestJS hoàn chỉnh
FROM node:22-bookworm-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:22-bookworm-slim AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:22-bookworm-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
COPY package.json package-lock.json ./
RUN npm ci --omit=dev \
&& npm cache clean --force
COPY --chown=node:node --from=builder \
/app/dist \
./dist
USER node
EXPOSE 3000
CMD ["node", "dist/main.js"]
Build và test
docker build \
-t deployeasy-api:test \
.
Chạy:
docker run \
--rm \
--name deployeasy-api-test \
-p 127.0.0.1:3000:3000 \
--env-file .env.production \
deployeasy-api:test
Test:
curl -f http://127.0.0.1:3000/health
Kiểm tra image
docker image ls deployeasy-api:test
docker history deployeasy-api:test
docker inspect deployeasy-api:test
Không đăng docker inspect nếu env chứa secret.
Lỗi thường gặp
permission denied khi app ghi file
Container chạy non-root nhưng directory thuộc root.
Sửa trong Dockerfile hoặc volume permission.
Native module lỗi Alpine
Module có thể cần glibc hoặc build tool. Dùng Debian slim hoặc cấu hình dependency phù hợp.
Image rất lớn
Kiểm tra:
- Copy thừa.
- Dev dependency.
- Build cache.
- Source/test.
- Base image.
- Multi-stage.
.dockerignore.
Signal không tắt app
Dùng exec-form CMD và graceful shutdown.
Secret xuất hiện trong image history
Không dùng ARG/ENV/COPY cho secret build/runtime.
Checklist
[ ] Base image phù hợp
[ ] Version/tag có kiểm soát
[ ] Multi-stage build
[ ] package lock + npm ci
[ ] Cache dependency tối ưu
[ ] .dockerignore
[ ] Không copy .env
[ ] Không ARG secret
[ ] Runtime non-root
[ ] Chỉ copy artifact cần thiết
[ ] CMD exec form
[ ] Healthcheck
[ ] Port chỉ publish khi cần
[ ] Log stdout/stderr
[ ] Image tag bằng version/SHA
[ ] Scan image
[ ] Test đúng architecture
Câu hỏi thường gặp
Image nhỏ hơn luôn tốt hơn?
Không tuyệt đối. Cần cân bằng kích thước, compatibility, khả năng debug và security updates.
Có nên dùng Alpine?
Có nếu dependency hoạt động và đã test. Không dùng chỉ vì image nhỏ.
USER node có đủ bảo mật không?
Đây là một lớp quan trọng, nhưng vẫn cần giới hạn capability, secret, network, volume và host security.
Healthcheck nên nằm Dockerfile hay Compose?
Dockerfile tạo mặc định dùng chung; Compose dễ tùy theo môi trường. Chọn theo cách quản lý của đội.
Kết luận
Dockerfile production tốt tạo image nhỏ vừa đủ, tái lập, không chứa secret, chạy non-root và xử lý signal đúng.
Nguồn tham khảo
Đọc tiếp
Bài viết liên quan
Docker là gì? Image, container, volume và Docker Compose
Hướng dẫn Docker từ cơ bản: container hoạt động thế nào, image và container khác gì, Dockerfile, volume, network và Docker Compose.
Đọc bài →Docker Compose production: App, PostgreSQL, healthcheck và backup
Hướng dẫn thiết kế Docker Compose production với app, PostgreSQL, network, volume, healthcheck, secret, restart và deploy an toàn.
Đọc bài →Build Docker trên local rồi deploy lên VPS
Quy trình từng bước tạo Dockerfile, chạy thử monorepo REMAP trên local và dùng cùng cấu hình Docker Compose để triển khai lên VPS.
Đọc bài →