Tách biến môi trường dev/prod và quản lý secrets
Điểm xuất phát: app đã có domain và HTTPS
Bài trước bạn đã trỏ domain về hosting và bật HTTPS xanh. Nghĩa là từ giờ, mọi lỗi cấu hình đều có người thấy, không còn là chuyện riêng của máy bạn. Hôm tôi làm xong bước đó, tôi deploy lần hai và app prod lặng lẽ ghi vào database dev vì connection string vẫn hardcode trong code. Bài này là bài chống lại đúng kiểu sự cố đó.
Sau 30 phút, bạn sẽ làm được:
- Phân biệt config và secret, biết cái nào được nằm trong repo
- Tách cấu hình dev và prod bằng biến môi trường, cùng một bộ code
- Đưa secrets lên hosting mà không commit chúng
- Biết thứ tự xử lý khi phát hiện key đã lộ
Nguyên tắc duy nhất cần nhớ cả bài: config tách khỏi code, secret tách khỏi repo.
Vì sao hardcode cấu hình làm sập app lúc 2 giờ sáng
Khi mọi thứ nằm trong code, bạn chỉ có một bản chạy được ở một nơi. Muốn đổi database, đổi API key, bạn phải sửa code rồi deploy lại. Và tệ hơn: key nằm trong code thì key nằm trong git, nằm trong lịch sử commit, nằm trên GitHub.
Cách làm chuẩn (12-factor) chỉ có một: build một lần, cấu hình ở ngoài. Code đọc cùng một tên biến, giá trị do môi trường cấp.
Biến môi trường: đọc cấu hình mà không sửa code
Biến môi trường là cặp KEY=VALUE sống ở tầng process, không nằm trong file source. Mỗi ngôn ngữ đọc một kiểu:
| Nơi đọc | Cú pháp |
|---|---|
| Node.js (server) | process.env.DATABASE_URL |
| Python | os.environ["DATABASE_URL"] |
| Vite (client) | import.meta.env.VITE_API_URL |
| Next.js (client) | process.env.NEXT_PUBLIC_API_URL |
Khi chạy local, giá trị đến từ file .env. Trên hosting, giá trị đến từ dashboard Environment Variables. Code không cần biết mình đang ở đâu.
Và phải fail fast: thiếu biến bắt buộc thì dừng ngay lúc khởi động, đừng chạy nửa vời rồi lỗi khó hiểu vài tiếng sau.
const required = ["DATABASE_URL", "SESSION_SECRET", "API_BASE_URL"];
const missing = required.filter((key) => !process.env[key]);
if (missing.length > 0) {
console.error("Thiếu biến môi trường:", missing.join(", "));
process.exit(1);
}
Tip: App chết lúc 2 giờ sáng vì thiếu biến rõ ràng dễ sửa hơn app chạy được nhưng âm thầm trả về dữ liệu sai.
Bài tập 1. Mở app của bạn, tìm mọi chỗ có URL, port, key hoặc mật khẩu viết trực tiếp trong code. Liệt kê 5 biến cần tách và đánh dấu P (public) hay S (secret).
Gợi ý đáp án
Ví dụ một app nhỏ: DATABASE_URL (S), SESSION_SECRET (S), SMTP_PASSWORD (S), API_BASE_URL (P), APP_NAME (P). Mẹo: nếu giá trị lộ ra mà người khác không làm gì được, đó là public; còn dùng được thay bạn, đó là secret.
Tách dev và prod mà không rối
Quy tắc vàng: cùng một tên biến ở mọi môi trường, chỉ khác giá trị. Nhờ vậy code không phải viết if (env === "prod") rải rác khắp nơi.
| Dev (máy bạn) | Prod (hosting) | |
|---|---|---|
| Nơi lưu giá trị | file .env |
dashboard Environment Variables |
| Ai đọc được | chỉ bạn | app, và người có quyền admin |
| Key bên thứ ba | test mode | live mode |
Ba file cần phân biệt rõ:
.env: giá trị thật khi chạy local. Không commit..env.example: chỉ danh sách tên biến kèm giá trị giả. Commit để người khác biết cần set gì..gitignore: phải có dòng.envtrước lần commit đầu tiên.
# .env.example
DATABASE_URL=postgres://user:password@localhost:5432/app_dev
SESSION_SECRET=change-me
API_BASE_URL=http://localhost:3000
Một thói quen cứu tôi nhiều lần: log tên host của database một lần lúc khởi động (không log mật khẩu) để liếc mắt là biết prod đang trỏ đâu.
Bài tập 2. Tạo .env.example cho app của bạn, thêm .env vào .gitignore, rồi chạy git status để chắc chắn .env không xuất hiện. Tiếp theo chạy git ls-files | grep env xem có file env nào đã lỡ vào repo chưa.
Gợi ý đáp án
Nếu git ls-files in ra .env, file đó đã được track từ trước và .gitignore không còn tác dụng. Chạy git rm --cached .env, sau đó coi giá trị trong file như đã bị lộ và chuyển sang phần xử lý bên dưới.
Secrets: thứ không bao giờ được vào git
Secret là bất cứ giá trị nào mà lộ ra là người khác dùng được: database password, API key, JWT signing secret, SMTP password, khoá thanh toán.
Ba nơi lưu chấp nhận được:
- Dashboard Environment Variables của hosting (đơn giản nhất, dùng ngay)
- Secret manager của cloud khi app lớn hơn
- Password manager của nhóm, để chia sẻ giữa người với người, không gửi qua chat
Và một cái bẫy riêng của frontend: biến có prefix VITE_ hoặc NEXT_PUBLIC_ bị nhúng thẳng vào bundle client. Ai mở DevTools cũng đọc được. Chỉ dùng chúng cho config công khai, tuyệt đối không đặt secret ở đó.
Warning: Một dòng
console.log(process.env)khi debug có thể đẩy toàn bộ secret vào log. Log được lưu, được chia sẻ, được gửi cho dịch vụ bên thứ ba. Đừng làm vậy.
Bài tập 3. Rà lại repo xem có secret nào đã lọt vào lịch sử không: git log -p -S "SECRET" -- . và git log -p | grep -i "api_key". Ghi lại kết quả, kể cả khi bạn nghĩ là không có.
Gợi ý đáp án
Có kết quả thì đừng hoảng, cũng đừng xoá lịch sử trước. Sang phần dưới: rotate trước, dọn sau.
Lỡ lộ key rồi: thứ tự xử lý
Thứ tự này quan trọng hơn mọi thứ khác trong bài. Làm sai thứ tự là mất thêm thời gian mà key vẫn đang mở.
- Rotate hoặc revoke key ngay trên dashboard của dịch vụ đó. Đây là bước đầu tiên, không phải dọn code.
- Tạo key mới, cập nhật vào Environment Variables trên hosting, restart hoặc redeploy.
- Xoá key khỏi code, thêm file chứa nó vào .gitignore.
- Dọn lịch sử git nếu cần (git filter-repo hoặc BFG), nhưng hiểu rằng xoá lịch sử không thu hồi được key đã lộ.
- Kiểm tra log truy cập của dịch vụ đó xem có hoạt động lạ không.
Lý do phải rotate trước: bot quét key trên GitHub trong vòng vài chục giây. Dọn lịch sử mất hàng giờ và không làm key cũ an toàn trở lại.
Checklist cấu hình trước khi deploy
-
git ls-fileskhông có file.envnào -
.env.exampleliệt kê đủ tên biến, giá trị là giả - App fail fast khi thiếu biến bắt buộc
- Prod env vars đã set trên hosting, đúng live mode của dịch vụ bên thứ ba
- Không có
console.login ra config hay secret - Không secret nào nằm trong biến
VITE_hoặcNEXT_PUBLIC_
- File _____ chứa giá trị thật khi chạy local và phải nằm trong _____.
- File _____ được commit để chia sẻ danh sách tên biến, không chứa giá trị thật.
- Biến có prefix _____ hoặc NEXT_PUBLIC_ bị nhúng vào bundle client nên không được chứa secret.
- Khi phát hiện key bị lộ, việc đầu tiên phải làm là _____.
- Code nên _____ khi thiếu biến môi trường bắt buộc.
Ba cái bẫy làm lộ secret mà tôi đã từng mắc
- Commit
.envở lần commit đầu tiên. Xoá file sau đó không giúp gì, nó vẫn nằm trong lịch sử. Thêm.gitignoretrước khigit addbất cứ thứ gì. - Dùng chung một key cho dev và prod. Key test bị lộ thì bạn mất thời gian, key live bị lộ thì bạn mất tiền. Tách ngay từ đầu, đừng đợi.
- Nghĩ rằng repo private là đủ an toàn. Người rời nhóm vẫn còn bản clone. Secret chỉ nên sống ở nơi cấp phát được và thu hồi được.
Rule of thumb: nếu bạn phải dán một giá trị vào chat để đồng nghiệp chạy được app, giá trị đó đang được quản lý sai chỗ.
Front: Biến môi trường là gì? Back: Cặp KEY=VALUE sống ở tầng process, giúp tách cấu hình ra khỏi code.
Front: Node.js đọc biến môi trường bằng cú pháp nào? Back: process.env.TEN_BIEN
Front: File nào chứa giá trị biến môi trường khi chạy local? Back: File .env, và file này không được commit.
Front: Vì sao không commit file .env? Back: Vì nó chứa giá trị thật, thường là secret; commit là lộ key cho mọi bản clone.
Front: .env.example dùng để làm gì? Back: Liệt kê tên biến với giá trị giả để người khác biết cần set gì; file này được commit.
Front: Đặt biến môi trường cho prod ở đâu? Back: Dashboard Environment Variables của hosting, hoặc secret manager của dịch vụ.
Front: Vì sao giữ cùng tên biến ở mọi môi trường? Back: Để code không phải rẽ nhánh theo môi trường, chỉ giá trị là thay đổi.
Front: Prefix VITE_ và NEXT_PUBLIC_ có rủi ro gì? Back: Giá trị bị nhúng vào bundle client, ai xem source cũng thấy, nên chỉ dùng cho config công khai.
Front: Fail fast khi thiếu biến môi trường nghĩa là gì? Back: Kiểm tra biến bắt buộc lúc khởi động, thiếu thì dừng app ngay thay vì chạy sai âm thầm.
Front: Những gì được coi là secret? Back: Database password, API key, JWT signing secret, SMTP password: bất cứ gì lộ ra là người khác dùng được.
Front: Phát hiện key đã bị commit, bước đầu tiên là gì? Back: Rotate hoặc revoke key trên dashboard dịch vụ ngay, rồi mới dọn code và lịch sử git.
Front: Vì sao không console.log(process.env)? Back: Nó ghi toàn bộ secret vào log, mà log thường được lưu và chia sẻ rộng hơn bạn tưởng.
Front: Làm sao kiểm tra prod đang trỏ đúng database? Back: Log tên host của database một lần lúc khởi động (không log mật khẩu) và đối chiếu.
Những gì cần nhớ trước khi sang bài log
- Cùng một bộ code, cấu hình đến từ môi trường:
.envở local, Environment Variables ở hosting. .envkhông bao giờ vào git;.env.examplethì luôn được commit.- Secret bị lộ thì rotate trước, dọn dẹp sau.
- Biến có prefix public là biến công khai, đừng đặt secret ở đó.
Bài tiếp theo: app đã có cấu hình sạch, giờ mình bật log cơ bản và uptime monitoring để biết app chết lúc nào mà không phải chờ người dùng báo.