Phân tích
7 phút đọc15 tháng 6, 2026292

Docker Production-Ready: Security & Performance Không Phải Chuyện Đùa

Hardening Docker cho production từ non-root user, secrets management đến resource limits và logging. Những thứ bạn không thể bỏ qua trước khi deploy.

N

Nguyễn Nhật Long

@nguyennhatlong1303

Docker Production-Ready: Security & Performance Không Phải Chuyện Đùa

Nếu bạn đã đi qua 6 bài trước trong series này, giờ bạn đã biết cách build image, viết Dockerfile sạch, compose multi-service stack. Nhưng chạy được trên local và chạy được trên production là hai chuyện hoàn toàn khác nhau. Production nghĩa là data thật, user thật, và khi có sự cố thì không phải chỉ mình bạn chịu.

Bài này mình sẽ đi thẳng vào phần hardening những thứ mà nhiều người bỏ qua vì "chạy được là được", cho đến khi không chạy được nữa.

Đừng Để Container Chạy Với Quyền Root

Đây là lỗi mình thấy cực kỳ phổ biến, kể cả ở các team đã dùng Docker khá lâu. Mặc định, Docker chạy process bên trong container với quyền root. Nghe có vẻ ổn vì container đã isolated rồi, nhưng thực tế không đơn giản vậy.

Nếu có một lỗ hổng nào đó cho phép attacker escape khỏi container sandbox không phải không thể xảy ra thì họ đang có quyền root trên host của bạn. Game over.

Fix cực kỳ đơn giản, thêm vào Dockerfile:

Dockerfile
1RUN addgroup -S appgroup && adduser -S appuser -G appgroup
2
3# ... các bước build khác ...
4
5USER appuser

Instruction USER này phải đặt sau tất cả các lệnh cần quyền root (như apt-get install, npm install vào global, copy file vào /app...). Sau đó verify lại:

Terminal
1docker exec <container_id> whoami
2# Output phải là: appuser
3# Không phải: root

Theo kinh nghiệm của mình, một số base image như node:alpine đã có sẵn user node, bạn chỉ cần USER node là xong, không cần tạo mới.

Read-only Filesystem Thêm Một Lớp Bảo Vệ

Nếu app của bạn không cần ghi file ra filesystem (chỉ đọc config, chỉ serve static), hãy mount container ở chế độ read-only:

Terminal
1docker run --read-only my-app

Nhưng thực tế hầu hết app đều cần ghi gì đó log, temp file, cache. Lúc này bạn kết hợp với volume và tmpfs:

Terminal
1docker run --read-only \
2 --tmpfs /tmp \
3 --mount type=volume,src=app-data,dst=/app/data \
4 my-app

--tmpfs /tmp cho phép ghi vào /tmp nhưng lưu trong memory, không persist. Volume app-data thì persist ra ngoài. Còn lại toàn bộ filesystem là read-only. Cái này đặc biệt hiệu quả để ngăn attacker drop thêm file độc hại vào container nếu chúng exploit được app của bạn.

Secrets Phần Mà Nhiều Người Làm Sai Nhất

Mình nói thẳng: hardcode secret vào Dockerfile hoặc commit .env file lên git là sai. Không có ngoại lệ. Mình đã thấy production database password nằm thẳng trong Dockerfile được push lên public GitHub repo và đó không phải junior làm, là senior với 7 năm kinh nghiệm.

Có mấy cách tiếp cận tùy vào scale của bạn:

Với Docker Swarm, flow khá clean:

ApproachPhù hợp khiTrade-off
`.env` file (gitignored)Small team, dev/stagingVẫn là plaintext trên disk, phải manage manually
Docker Secrets (Swarm)Đang dùng Docker SwarmChỉ available trong Swarm mode
HashiCorp VaultTeam lớn, nhiều serviceSetup phức tạp, nhưng rất mạnh
AWS Secrets ManagerĐang chạy trên AWSTích hợp tốt với IAM, có cost
Kubernetes SecretsĐang dùng K8sBase64 encoded, không phải encrypted mặc định
Terminal
1# Tạo secret
2echo "super_secret_password" | docker secret create db_password -
3
4# Reference trong compose file
5services:
6 app:
7 image: my-app
8 secrets:
9 - db_password
10
11secrets:
12 db_password:
13 external: true

Secret sẽ được mount vào /run/secrets/db_password bên trong container, app đọc từ file thay vì env var. Không expose qua docker inspect, không lộ trong logs.

Nếu chưa dùng Swarm, ít nhất hãy dùng .env file và đảm bảo nó trong .gitignore. Tạo thêm .env.example với placeholder values để team biết cần set những gì.

Scan Image Trước Khi Deploy

Bạn pull một base image về, tưởng sạch, nhưng thực ra bên trong có vài chục CVE đang chờ. Mình hay dùng hai tool:

Docker Scout built-in từ Docker Desktop:

Terminal
1docker scout cves my-app:latest

Trivy open source, mình thích cái này hơn vì chạy được trong CI pipeline dễ hơn:

Terminal
1trivy image my-app:latest

Một số nguyên tắc mình luôn follow:

  • Pin version cụ thể: node:20.11-alpine3.19 thay vì node:latest
  • Dùng Alpine hoặc Distroless khi có thể ít package hơn = ít attack surface hơn
  • Update base image định kỳ, ít nhất mỗi tháng một lần

Distroless image của Google là một option rất hay nếu bạn cần minimal nhất có thể không có shell, không có package manager, chỉ có runtime. Attacker vào được container cũng không làm được gì nhiều.

Resource Limits Đừng Để Một Container Ăn Hết Host

Mình đã từng gặp tình huống một service bị memory leak, không có limit, ăn hết RAM của host, kéo theo tất cả các service khác trên cùng máy chết theo. Một buổi chiều thứ Sáu rất vui.

Set limit từ đầu:

Terminal
1docker run \
2 --memory=512m \
3 --memory-swap=1g \
4 --cpus=1.5 \
5 my-app

Trong Docker Compose:

YAML
1services:
2 app:
3 image: my-app
4 deploy:
5 resources:
6 limits:
7 memory: 512m
8 cpus: '1.5'
9 reservations:
10 memory: 256m
11 cpus: '0.5'

reservations là guaranteed resources, limits là maximum. Container vượt memory limit sẽ bị OOM killed nghe có vẻ brutal nhưng tốt hơn là để nó kéo cả host xuống.

Anh em lưu ý: --memory-swap là tổng memory + swap, không phải chỉ riêng swap. Nếu set --memory=512m --memory-swap=1g thì swap thực tế là 512m.

Health Check Để Docker Tự Biết Container Có Ổn Không

Restart policy (always, unless-stopped) chỉ restart khi container exit. Nhưng nếu container vẫn chạy mà app bên trong bị stuck, deadlock, hoặc không nhận request nữa thì sao? Đây là lúc HEALTHCHECK phát huy tác dụng:

Dockerfile
1HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
2 CMD curl -f http://localhost:3000/health || exit 1
  • --interval: check mỗi 30 giây
  • --timeout: nếu sau 10 giây chưa có response thì coi là failed
  • --start-period: grace period khi container mới start (để app có thời gian khởi động)
  • --retries: fail 3 lần liên tiếp mới đánh dấu là unhealthy

Khi container chuyển sang unhealthy, Docker Swarm hoặc Kubernetes sẽ tự route traffic sang instance khác và restart instance bị lỗi. Kết hợp với restart policy on-failure hoặc always thì khá là solid.

Endpoint /health trong app của bạn nên check những thứ thực sự quan trọng: database connection, cache connection, không phải chỉ return 200 unconditionally.

Logging Đúng Cách

Default logging driver của Docker là json-file. Không có rotation, để lâu là đĩa đầy. Mình hay thấy server hết disk vì Docker logs, đặc biệt với các service verbose.

Fix nhanh:

Terminal
1docker run \
2 --log-opt max-size=10m \
3 --log-opt max-file=3 \
4 my-app

Hoặc set global trong /etc/docker/daemon.json:

JSON
1{
2 "log-driver": "json-file",
3 "log-opts": {
4 "max-size": "10m",
5 "max-file": "3"
6 }
7}

Với production nghiêm túc hơn, bạn cần centralized logging. Hai option phổ biến:

Mình đang dùng Loki ở project hiện tại, thấy khá ổn cho team nhỏ. Nếu bạn đã có Grafana cho metrics rồi thì thêm Loki vào rất tự nhiên.

StackƯu điểmNhược điểm
ELK (Elasticsearch + Logstash + Kibana)Powerful search, mature ecosystemHeavy, tốn RAM, setup phức tạp
Grafana LokiLightweight, tích hợp tốt với GrafanaSearch không mạnh bằng ES

Logging driver fluentd hoặc syslog cho phép forward logs trực tiếp ra aggregator mà không cần lưu local:

Terminal
1docker run \
2 --log-driver=fluentd \
3 --log-opt fluentd-address=localhost:24224 \
4 my-app

Debug nhanh thì vẫn dùng:

Terminal
1docker logs --tail 100 -f <container_id>

Nhìn lại thì những thứ mình vừa cover non-root user, read-only filesystem, secrets management, image scanning, resource limits, health check, logging không cái nào đặc biệt khó. Nhưng cộng lại, chúng tạo ra sự khác biệt rất lớn giữa một container setup ẩu và một setup mà bạn tự tin đưa lên production.

Bài tiếp theo mình sẽ nói về CI/CD pipeline với Docker tự động build, scan, push image và deploy. Phần đó mới là nơi tất cả những thứ này được kết nối lại thành một workflow hoàn chỉnh.

NN

Nguyễn Nhật Long

@nguyennhatlong1303

Nguyễn Nhật Long is a Senior Frontend Engineer and Frontend Team Leader with 7 years of experience building real-time fintech platforms. Specializing in React, Next.js, TypeScript, and React Native, shipping 10+ products across Web, Mobile, Telegram Mini-Apps, and Web3.

Thấy hay? Chia sẻ cho bạn bè!

Docker Production-Ready: Security & Performance Không Phải Chuyện Đùa — Stacklog