.env best practices | Checklist onboard freelancer vào repo khách
.env là tiện ích local, không phải hệ thống ghi nhận secret production. Luôn gitignore file thật, commit .env.example, quote khi giá trị có space/#/=, tách môi trường, validate lúc boot. Freelancer vào repo khách: copy example → DB local → secret qua vault — không xin “gửi giúp cái env production”. Chỉnh format trên .env Formatter Kawa (trình duyệt, không upload).
Freelance Việt nhận repo Nest/Next từ khách Nhật hoặc startup nội địa. Slack: “Anh gửi em file env nhé.” Ba phút sau: DATABASE_URL production nằm trong history chat. Hoặc ngược lại — clone xong app chết vì thiếu 14 key không ai ghi trong README. Bài này là best practices dotenv gắn với checklist onboarding, không phải định nghĩa KEY=value ba dòng.
Ai nên đọc
- Freelancer / outsource vừa join repo khách lần đầu trong tuần
- Lead cần playbook “không gửi .env qua Zalo”
- Fresher từng commit nhầm
.envrồi phải rotate cả cuối tuần
Cái gì được phép nằm trong .env
| Được | Không nên |
|---|---|
| URL DB local, port, feature flag dev | Master key production trên laptop “cho tiện” |
Public key rõ tên NEXT_PUBLIC_* / VITE_* | Dán cả JSON service-account một dòng khi không cần |
| Override cá nhân (đã gitignore) | Secret dùng chung team lẽ ra nằm vault |
Quy tắc ngón tay cái: nếu lộ giá trị = truy cập dữ liệu khách hoặc tiền → ngoài DB local throwaway, ưu tiên secret manager / biến môi trường platform.
# Ví dụ local — giá trị giả
DATABASE_URL=postgresql://app:localdev@localhost:5432/app
APP_ENV=development
LOG_LEVEL=debug
STRIPE_SECRET_KEY=sk_test_replace_me
Git hygiene thật sự chạy được
.env
.env.*
!.env.example
- Commit
.env.example: placeholder + comment một dòng mỗi key - Không commit
.env,.env.local,.env.productioncó secret thật - Kiểm tra Docker:
COPY .dễ nhét file gitignore trên host vào image - Bật quét secret trên CI / pre-commit (
gitleaks, …)
Đã lỡ push: rotate ngay (DB password, API key, JWT secret). Xóa file ở commit sau không xóa khỏi history cũ — coi như đã lộ.
Không gửi nội dung .env production qua Slack, Zalo, email, hay PR description. Freelancer chỉ cần shape key + giá trị local an toàn.
Format: = , quote, comment
# Đúng: SCREAMING_SNAKE, không space quanh =
DATABASE_URL=postgresql://app:secret@localhost:5432/app
APP_ENV=development
# Sai thường gặp
database-url = "postgres://..."
API_KEY=my key with spaces # nhiều loader cắt ở space
CONNECTION=host=localhost;port=5432 # = trong value — nên quote
NOTE=hello # world # # có thể thành comment
| Tình huống | Cách viết | Lý do |
|---|---|---|
| Giá trị có khoảng trắng | API_KEY="my key with spaces" | Tránh cắt sớm |
| Giá trị có dấu = | CONN="host=localhost;port=5432" | Phần sau = đầu tiên là value |
| Có # trong value | Quote cả chuỗi | Nhiều parser coi # là comment |
| Comment | # giải thích key | Dòng bắt đầu # — giữ trong .env.example |
| Đổi tên key đột ngột | Migration window + cập nhật example | Cả team chết overnight |
Tránh export cùng key trong shell profile và .env với giá trị khác — bạn sẽ debug sai lớp.
Khi .env.example lộn xộn (key A/Z lẫn, thiếu comment), sắp xếp trên .env Formatter rồi commit example — không paste secret production vào tool online lạ. Kawa chạy local trên trình duyệt.
🛠️ Kiểm tra định dạng .env ngay tại đây
Tách môi trường (đừng invent loader thứ sáu)
Pattern phổ biến:
.env.example # commit — template
.env # local secrets — gitignore
.env.test # mặc định cho test runner
Framework có overlay riêng (Next: .env.local, Vite modes…). Đọc docs một lần, ghi vào README: “Team dùng file X, thứ tự load Y.” Đừng tự viết loader thứ sáu — ticket onboard sẽ dài vô ích.
Production:
- Ưu tiên env trên Vercel / Cloudflare / Fly / k8s Secrets
- Bắt buộc dùng file trên server → quyền filesystem chặt + rotate tự động
- Không bake
.envproduction vào image public
Xác định APP_ENV / NODE_ENV và file nào được load
Local: chỉ giá trị throwaway trong .env (gitignore)
Staging/prod: inject từ platform hoặc vault — không copy file laptop
Document required vs optional trong .env.example
Validate lúc boot — đừng để undefined im lặng
import { z } from "zod";
const env = z
.object({
DATABASE_URL: z.string().url(),
APP_ENV: z.enum(["development", "test", "production"]),
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
})
.parse(process.env);
export default env;
- Production: fail fast nếu thiếu key nguy hiểm
- Dev: message nêu tên key + “xem
.env.example” - Silent
undefined→ phát hiện thiếu Stripe key ở checkout đầu tiên (đắt)
Checklist onboard freelancer vào repo khách
Dán vào README hoặc Notion kickoff:
Ngày 1 — trước khi chạy app
-
.gitignorecó.env/.env.*và!.env.example - Có
.env.exampleđủ key + comment một dòng - README nói framework + thứ tự load file
- Script
cp .env.example .env(hoặcmake bootstrap) tồn tại
Ngày 1 — nhận secret
- Không nhận file
.envproduction qua chat - Vault / 1Password / invite platform env cho staging (nếu cần)
- DB local riêng — không tunnel thẳng prod trừ khi có quy trình + audit
Ngày 1 — trước commit đầu
-
git statuskhông có.env - App boot qua schema validation (hoặc checklist tay đủ key)
- Biết đâu là
NEXT_PUBLIC_*(lộ ra browser) vs secret server-only
Tuần đầu
- CI có secret scan
- Biết kênh rotate khi nghi leak
- Không screenshot
.envvào ticket Jira/Linear
Case: “Gửi giúp env”
Khách gửi env.zip chứa prod. Freelancer copy vào laptop, commit nhầm vì quên gitignore trên máy mới. Fix quy trình: chỉ example trên Git; prod chỉ trên dashboard hosting; mọi người onboard theo checklist trên. Rotate toàn bộ key trong zip.
Case: “Chạy được máy anh, chết máy em”
Hai người dùng .env.development và .env.local khác nhau theo thói quen; một người export DATABASE_URL trong PowerShell. Fix: README chốt một file local; xóa export shell trùng tên; validate boot nêu key thiếu.
Case: Docker Compose trên bastion chung
env_file: .env trỏ cùng file prod trên máy share. Prefer environment lấy từ host env / secrets — không để một file .env dùng chung mọi compose trên bastion.
Chia sẻ config mà không chia sẻ secret
- Shape + default không mật →
.env.example - Secret thật → 1Password / vault / platform
- Preview app → CI generate rồi inject
- Required vs optional ghi rõ — junior không copy dump cũ từ đồng nghiệp
Khi chỉ cần sắp xếp / làm sạch example trước khi PR: mở /vi/tools/env-formatter/, dán bản đã che, sort key, copy lại.
Checklist PR / review env
- Không có
.envthật trong diff -
.env.examplecập nhật nếu thêm key - Key mới có comment + giá trị giả
- Public prefix đúng (
NEXT_PUBLIC_/VITE_) nếu cố ý lộ client - Boot validation cập nhật schema
- README/onboard note cập nhật nếu đổi file load
Kết
Best practice .env không phải “biết KEY=value” — là ranh giới secret / shared / process nào được đọc. Freelancer Việt vào repo khách: example → local → vault; không bao giờ “xin file env production”. Quote đúng, tách môi trường, validate boot, gitignore thật. Bookmark kèm .env Formatter để giữ .env.example sạch trước mỗi lần onboard người mới.