DeployEasy
Menu
DockerTrung bình

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.

· 9 phút đọc · 1.818 từ
Mục lục bài viết

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_modules hiệ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_modules khá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