# 🔑 Tách biến môi trường dev/prod và quản lý secrets

By Better Wise

## Đ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.

```js
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 `.env` trước lần commit đầu tiên.

```bash
# .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:

1. Dashboard Environment Variables của hosting (đơn giản nhất, dùng ngay)
2. Secret manager của cloud khi app lớn hơn
3. 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ở.

1. 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.
2. Tạo key mới, cập nhật vào Environment Variables trên hosting, restart hoặc redeploy.
3. Xoá key khỏi code, thêm file chứa nó vào .gitignore.
4. 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ộ.
5. 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-files` không có file `.env` nào
- [ ] `.env.example` liệ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.log` in ra config hay secret
- [ ] Không secret nào nằm trong biến `VITE_` hoặc `NEXT_PUBLIC_`

1. File _____ chứa giá trị thật khi chạy local và phải nằm trong _____.
2. File _____ được commit để chia sẻ danh sách tên biến, không chứa giá trị thật.
3. Biến có prefix _____ hoặc NEXT_PUBLIC_ bị nhúng vào bundle client nên không được chứa secret.
4. Khi phát hiện key bị lộ, việc đầu tiên phải làm là _____.
5. 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

1. **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 `.gitignore` trước khi `git add` bất cứ thứ gì.
2. **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.
3. **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.
- `.env` không bao giờ vào git; `.env.example` thì 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.

---

## Contents

Part of [🚀 Đưa app vibe-code lên production: domain, HTTPS và deploy an toàn](https://everwise.study/courses/dua-app-vibe-code-len-production-domain-https-va-deploy-an-toan.md)

- **🚀 Đưa app từ local lên internet**
  - [⚙️ Chọn hosting và deploy bản đầu tiên](https://everwise.study/courses/dua-app-vibe-code-len-production-domain-https-va-deploy-an-toan/chon-hosting-va-deploy-ban-dau-tien.md)
  - [🌐 Trỏ domain về hosting](https://everwise.study/courses/dua-app-vibe-code-len-production-domain-https-va-deploy-an-toan/tro-domain-ve-hosting.md) (sign-in required)
  - [🔒 Bật HTTPS và kiểm tra chứng chỉ](https://everwise.study/courses/dua-app-vibe-code-len-production-domain-https-va-deploy-an-toan/bat-https-va-kiem-tra-chung-chi.md)
  - [✅ Checkpoint: Hosting, domain và HTTPS](https://everwise.study/courses/dua-app-vibe-code-len-production-domain-https-va-deploy-an-toan/checkpoint-hosting-domain-https.md)
- **🛡️ Cấu hình an toàn và giám sát**
  - [🔑 Tách biến môi trường dev/prod và quản lý secrets](https://everwise.study/courses/dua-app-vibe-code-len-production-domain-https-va-deploy-an-toan/tach-bien-moi-truong-dev-prod.md) — this lesson
  - [📊 Log cơ bản và uptime monitoring](https://everwise.study/courses/dua-app-vibe-code-len-production-domain-https-va-deploy-an-toan/log-co-ban-va-uptime-monitoring.md)
  - [♻️ Backup dữ liệu và rollback khi lỗi](https://everwise.study/courses/dua-app-vibe-code-len-production-domain-https-va-deploy-an-toan/backup-du-lieu-va-rollback-khi-loi.md)
  - [✅ Checkpoint: Cấu hình, giám sát và khôi phục](https://everwise.study/courses/dua-app-vibe-code-len-production-domain-https-va-deploy-an-toan/checkpoint-cau-hinh-giam-sat-khoi-phuc.md)
- **🏁 Go-live: checklist và chạy thử**
  - [📋 Chạy checklist go-live trên app thật](https://everwise.study/courses/dua-app-vibe-code-len-production-domain-https-va-deploy-an-toan/chay-checklist-go-live-tren-app-that.md) (premium content)
  - [✅ Checkpoint: Sẵn sàng go-live](https://everwise.study/courses/dua-app-vibe-code-len-production-domain-https-va-deploy-an-toan/checkpoint-san-sang-go-live.md) (premium content)
- [Course cheatsheet](https://everwise.study/courses/dua-app-vibe-code-len-production-domain-https-va-deploy-an-toan/cheatsheet.md) (premium content)
