Trong quá trình phát triển, thói quen phổ biến nhất của lập trình viên là gom tất cả mọi thứ vào một file .env duy nhất: từ cổng kết nối PORT=3000, đường dẫn PUBLIC_URL cho đến mật khẩu database và Stripe API key.
Thói quen này rất tiện khi chạy ở máy cá nhân (localhost), nhưng khi mang nguyên cách làm đó lên máy chủ production, bạn đang đối mặt với những rủi ro bảo mật cực lớn:
- Vô tình commit file
.envlên GitHub: bot quét tự động sẽ vét sạch API key và mật khẩu chỉ trong vòng vài phút. - Lệnh
COPY . .trong Dockerfile vô tình đóng gói luôn file.envvào bên trong Docker image: bất kỳ ai có quyền pull image về máy đều có thể mở ra và xem sạch mật khẩu. - Truyền mật khẩu database qua
ARGhoặcENVtrong Dockerfile: giá trị mật khẩu sẽ bị ghi vĩnh viễn vào lịch sử các layer của image (docker history), không có cách nào xóa sạch nếu không build lại từ đầu.
Để vận hành hệ thống an toàn, quy tắc đầu tiên là: Phải tách bạch giữa Cấu hình thông thường (Config) và Dữ liệu bí mật (Secret).
Phân biệt rõ giữa Config và Secret
Không phải biến nào cũng cần bảo mật như nhau. Bạn có thể phân loại nhanh theo bảng sau:
- Cấu hình thông thường (Config):
APP_ENV=production,PORT=8080,LOG_LEVEL=info,PUBLIC_API_URL=https://api.example.com. Những biến này chỉ dùng để điều khiển hành vi ứng dụng hoặc mô tả môi trường. Nếu có bị nhìn thấy thì hệ thống cũng không bị xâm nhập. Bạn có thể quản lý qua fileenv_filehoặc khai báo trực tiếp trongcompose.yaml. - Dữ liệu bí mật (Secret runtime): Mật khẩu database, token thanh toán, khóa riêng tư TLS (private key), chuỗi mã hóa phiên (session secret). Đây là những chiếc chìa khóa vạn năng cho phép truy cập trực tiếp vào dữ liệu khách hàng. Chúng tuyệt đối không được xuất hiện trên Git và không nên truyền qua biến môi trường công khai.
- Bí mật lúc đóng gói (Build secret): Personal Access Token để tải thư viện private từ GitHub, NPM token nội bộ. Những token này chỉ cần dùng trong lúc chạy lệnh
docker build, hoàn toàn không cần tồn tại trong container khi chạy thực tế.
Dùng Compose secrets thay vì nhét mật khẩu vào biến môi trường
Khi bạn truyền mật khẩu bằng block environment: trong file Compose, bất kỳ ai gõ lệnh docker inspect <container> trên máy chủ cũng đều nhìn thấy mật khẩu dưới dạng văn bản thuần (plaintext). Hơn nữa, nếu ứng dụng gặp lỗi và dump toàn bộ biến môi trường ra log, mật khẩu cũng sẽ lộ theo.
Docker Compose cung cấp một cơ chế bảo mật tốt hơn nhiều: Compose Secrets.
Thay vì biến môi trường, Docker sẽ mount secret dưới dạng một file chỉ đọc (read-only) nằm trong bộ nhớ tạm /run/secrets/ bên trong container:
services:
web:
image: my-app:1.2.0
secrets:
- db_password
environment:
# Báo cho ứng dụng biết đường dẫn file đọc mật khẩu
DB_PASSWORD_FILE: /run/secrets/db_password
db:
image: postgres:16
secrets:
- db_password
environment:
POSTGRES_DB: app_db
POSTGRES_USER: app_user
# Các image chính thức (Postgres, MySQL) đều hỗ trợ sẵn đuôi _FILE
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txtƯu điểm vượt trội của mô hình này:
- Bảo mật tuyệt đối trong container: Mật khẩu không nằm trong biến môi trường, không bị lộ khi chạy
docker inspect. - Cấp quyền đúng người đúng việc: Service nào cần mật khẩu (như
web,db) mới được khai báo trong mụcsecrets. Các service khác như Nginx proxy hay Redis cache hoàn toàn không nhìn thấy file mật khẩu này. - Thư mục
./secretstrên máy chủ: Được đặt ngoài Git (đã thêm vào.gitignore) và phân quyền nghiêm ngặt trên Linux (chỉ user chạy docker mới có quyền đọc:chmod 600 ./secrets/*).
Build secret: Đừng bao giờ dùng ARG cho token riêng tư
Nhiều lập trình viên có thói quen truyền token tải thư viện bằng lệnh:
# SAI: Token sẽ bị lưu vĩnh viễn vào image layer history!
ARG NPM_TOKEN
RUN npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN && npm installDù bạn có thêm lệnh xóa file token ở dòng sau, bất kỳ ai gõ docker history <image> cũng vẫn đọc được nguyên vẹn giá trị của ARG NPM_TOKEN.
Cách làm chuẩn là sử dụng BuildKit secret mount:
# syntax=docker/dockerfile:1
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
# Đọc token tạm thời trong bộ nhớ lúc chạy lệnh, không lưu vào image layer
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) npm install
COPY . .
RUN npm run buildKhi build, bạn truyền secret vào bằng lệnh:
docker build --secret id=npm_token,src=./secrets/npm_token.txt -t my-app .Token chỉ tồn tại đúng trong vài chục giây chạy lệnh npm install, sau đó biến mất hoàn toàn và không để lại bất kỳ dấu vết nào trong artifact cuối cùng.
Luôn có file .dockerignore chuẩn chỉ
Trước khi Docker tiến hành build image, toàn bộ thư mục dự án (build context) sẽ được đóng gói và gửi sang Docker daemon. Nếu bạn không có file .dockerignore, file .env chứa mật khẩu máy cá nhân của bạn sẽ bị gửi sang và rất dễ bị copy nhầm vào container qua lệnh COPY . ..
Một file .dockerignore tối thiểu cho dự án production luôn phải có:
.git
.env
.env.*
secrets/
*.pem
*.key
backups/
node_modulesPhải làm gì nếu lỡ commit file .env chứa secret lên Git?
Nếu một ngày bạn giật mình phát hiện mình hoặc đồng nghiệp vừa lỡ tay push file .env chứa mật khẩu thật lên repository (kể cả repo private của công ty):
Đừng chỉ đơn giản là xóa file đó rồi commit một lần nữa. Toàn bộ lịch sử Git vẫn lưu lại commit cũ, và ai xem qua git log cũng đều thấy được mật khẩu.
Hành động đúng duy nhất lúc này là:
- Lập tức thu hồi (Revoke) hoặc đổi mật khẩu (Rotate) tại nguồn cấp: Vào database đổi mật khẩu mới, vào cổng thanh toán hoặc AWS thu hồi API token cũ và tạo token mới ngay lập tức. Hãy coi như chìa khóa cũ đã rơi vào tay kẻ xấu.
- Cập nhật chìa khóa mới vào máy chủ: Đưa secret mới vào file bí mật trên VPS và tạo lại container (
docker compose up -d). - Dọn dẹp Git sau cùng: Sau khi chìa khóa cũ đã bị vô hiệu hóa, bạn mới dùng các công cụ như
git-filter-repohoặc BFG Repo-Cleaner để làm sạch lịch sử Git nếu cần thiết.
Tóm lại
Bảo mật biến môi trường không đòi hỏi hạ tầng đắt đỏ, nó bắt đầu từ thói quen tách bạch:
- Config đưa vào
compose.yamlhoặcenv_file. - Secret runtime đưa vào Docker Compose
secrets. - Secret build đưa vào BuildKit
--mount=type=secret. - Khóa chặt thư mục nhạy cảm bằng
.dockerignore.
Khi các lớp này đã chuẩn chỉ, bạn có thể tự tin triển khai hệ thống theo Checklist đưa ứng dụng Docker Compose lên production mà không sợ vô tình để lộ dữ liệu kinh doanh của doanh nghiệp.



