🏗️ Tách môi trường dev, staging, production

By Better Wise

Vì sao phải tách môi trường trước khi viết thêm tính năng

Bạn đã từng đưa một app lên hosting hoặc VPS, nên bạn biết cảm giác sửa code trực tiếp trên server thật. Bài này dựng hàng rào đầu tiên để bạn không bao giờ phải làm vậy nữa: tách app thành ba môi trường dev, staging, production, mỗi nơi có cấu hình và dữ liệu riêng.

Sau bài này, bạn có thể:

  • Giải thích được vai trò của từng môi trường và ai được phép làm gì ở đó.
  • Tách cấu hình của một app vibe-code thành ba bộ riêng, không hardcode giá trị.
  • Tách dữ liệu để thao tác test không bao giờ chạm vào dữ liệu người dùng thật.
  • Mô tả đường đi của một commit từ dev tới production và điểm dừng để rollback.

Important: Câu hỏi tôi sẽ hỏi bạn ở mọi bước trong khoá này: nếu bước này hỏng thì rollback thế nào? Chưa trả lời được thì chưa được bấm deploy.

Ba môi trường, ba mức độ dũng cảm

Môi trường Ai dùng Dữ liệu Hỏng thì sao
dev Chính bạn, chạy local Dữ liệu giả, seed Sửa rồi chạy lại, không ai biết
staging Bạn và người review Dữ liệu giả hoặc đã ẩn danh hoá Không ai bị ảnh hưởng, nhưng release bị chặn
production Người dùng thật Dữ liệu thật Sự cố thật, phải rollback ngay

Điểm cần khắc vào đầu: staging tồn tại để bạn diễn tập đúng thứ sẽ chạy trên production. Nếu staging khác production về code, cấu hình hoặc phiên bản database thì đó chỉ là một cái dev thứ hai, không phải staging.

Tách cấu hình: cùng một code, ba bộ biến

Nguyên tắc: một codebase, nhiều bộ cấu hình. Bạn không sửa code khi chuyển môi trường, chỉ đổi biến môi trường.

my-app/
  .env.example      <- commit file mẫu, không có giá trị thật
  .env.development  <- không commit
  .env.staging      <- không commit
  .env.production   <- không commit
  src/

Những giá trị chắc chắn phải khác nhau giữa các môi trường:

  • DATABASE_URL: ba database khác nhau, không dùng chung.
  • APP_BASE_URL: http://localhost:3000, https://staging.app.com, https://app.com.
  • API key của bên thứ ba: key sandbox cho dev/staging, key thật chỉ ở production.
  • SMTP_*: hộp thư sandbox cho dev/staging.
  • Storage bucket: bucket riêng, đừng để file test nằm lẫn trong bucket thật.

Warning: Tuyệt đối không commit file .env có giá trị thật. Nếu đã lỡ commit, hãy coi như secret đó đã bị lộ và phải đổi ngay, việc xoá commit không cứu được gì. Bài sau sẽ đi sâu phần secrets.

Bài tập 1. Liệt kê ba giá trị cấu hình trong app của bạn mà nếu dùng chung giữa dev và production sẽ gây hậu quả thật.

Xem gợi ý

Ba giá trị thường gặp nhất: DATABASE_URL, API key thanh toán hoặc email, và APP_BASE_URL (dùng sai sẽ tạo link xác nhận trỏ về localhost). Mỗi giá trị sai đều dẫn tới một sự cố thật, không chỉ là lỗi hiển thị.

Tách dữ liệu: staging không được chạm dữ liệu thật

Đây là chỗ người mới hay làm hỏng mọi thứ. Ba quy tắc:

  1. Mỗi môi trường một database riêng. Không có ngoại lệ.
  2. Nếu cần dữ liệu giống thật để test, hãy ẩn danh hoá trước khi copy sang staging, hoặc viết seed script sinh dữ liệu giả.
  3. Chặn mọi kênh gửi ra bên ngoài ở dev và staging: email, SMS, webhook thanh toán. Dùng sandbox hoặc chế độ chỉ ghi log.

Bài tập 2. App của bạn gửi email xác nhận đơn hàng. Bạn cấu hình gì ở staging để không ai nhận nhầm email test?

Xem gợi ý

Trỏ SMTP_* về một sandbox như Mailtrap, hoặc đặt EMAIL_MODE=log để email chỉ được ghi vào log. Nếu buộc phải gửi thật, giới hạn danh sách người nhận về đúng địa chỉ test của team. Kiểm tra lại bằng cách đặt một đơn hàng ở staging và xác nhận không có email nào bay ra ngoài.

Đường đi của một commit

flowchart TD
  Code["💻<br/>Commit trên main"] --> Dev["🛠️<br/>dev (tự động)"]
  Dev --> Stg["🧪<br/>staging"]
  Stg --> Gate{"✅<br/>Smoke test pass?"}
  Gate -- Có --> Prod["🚀<br/>production"]
  Gate -- Không --> Stop["⛔<br/>Dừng, sửa trước"]

Hai điểm khác biệt quan trọng giữa các môi trường:

  • dev deploy tự động, mỗi lần push đều chạy, vì hỏng thì không ai bị ảnh hưởng.
  • production có cổng: một bước xác nhận thủ công, và điều kiện để qua cổng là smoke test trên staging đã pass. Bạn sẽ viết smoke test ở bài 1.3.

Và luôn có một dấu hiệu để bạn biết mình đang ở đâu: banner màu và tên miền riêng. Sự cố kinh điển là sửa dữ liệu trên production vì tưởng đang ở staging.

Bài tập 3. Bạn vừa deploy production, sau hai phút trang trắng. Trả lời hai câu: (a) dấu hiệu nào cho biết bạn đang thật sự ở production, (b) bước rollback đầu tiên là gì?

Xem gợi ý

(a) Tên miền thật và banner production, không có nhãn STAGING. (b) Rollback về build hoặc commit trước đó ngay, đừng sửa trực tiếp trên production. Nếu tính năng lỗi đã nằm sau feature flag thì tắt flag trước, nhanh hơn rollback. Sau đó mới kiểm tra biến môi trường vừa thay đổi.

Điền nhanh trước khi đọc phần tiếp theo:

Mỗi môi trường phải có _____ riêng, không dùng chung với production. Staging chỉ có giá trị khi nó chạy đúng _____ sẽ lên production. Việc gửi email thật từ staging bị chặn bằng _____ hoặc chế độ chỉ ghi log. Câu hỏi bắt buộc trước mỗi lần deploy là: nếu bước này hỏng thì _____ thế nào. Dữ liệu copy từ production sang staging phải được _____ trước.

Ba thói quen của người phát hành kỹ tính

  • Không bao giờ test trên production. Nếu buộc phải kiểm tra, dùng một tài khoản test và một đơn hàng nhỏ nhất có thể, rồi dọn dẹp sau.
  • Staging phải giống production ở phần khó nhất: cùng phiên bản database, cùng biến môi trường về hình dạng, cùng cách build. Khác biệt duy nhất là quy mô.
  • Đừng tạo thêm môi trường thứ tư khi chưa cần. Ba môi trường là đủ cho một app nhỏ, thêm nữa chỉ nhân bản sự lộn xộn.

Lỗi phổ biến nhất của người mới: dùng chung database giữa staging và production vì "cho nhanh có dữ liệu". Đó không phải tiết kiệm thời gian, đó là một sự cố đang chờ ngày xảy ra.

Front: Ba môi trường tối thiểu của một app web là gì? Back: dev, staging và production.

Front: Ai dùng môi trường dev? Back: Chính lập trình viên, chạy local, dữ liệu giả, hỏng thì sửa rồi chạy lại.

Front: Vai trò của staging? Back: Bản diễn tập giống production để kiểm thử trước khi release thật.

Front: production là gì? Back: Môi trường người dùng thật sử dụng, sự cố ở đây là sự cố thật.

Front: Vì sao mỗi môi trường cần database riêng? Back: Để thao tác test không ghi hoặc xoá dữ liệu thật của người dùng.

Front: Điều gì tuyệt đối không được commit vào Git? Back: File .env chứa giá trị thật; chỉ commit file mẫu .env.example.

Front: Trước khi release, staging nên chạy code nào? Back: Đúng commit sẽ được đưa lên production.

Front: Vì sao chặn gửi email và SMS thật ở staging? Back: Tránh làm phiền khách hàng thật bằng dữ liệu test.

Front: Cách rẻ nhất để không nhầm staging với production? Back: Banner hoặc nhãn màu rõ ràng trên UI, kèm tên miền riêng.

Front: Deploy production khác deploy dev ở điểm nào? Back: Dev tự động, production phải qua cổng xác nhận thủ công.

Front: Câu hỏi bắt buộc trước khi cho phép deploy? Back: Nếu bước này hỏng thì rollback thế nào.

Front: Dữ liệu copy từ production sang staging phải xử lý gì? Back: Ẩn danh hoá, hoặc thay bằng dữ liệu seed giả.

Front: Có nên sửa code riêng cho từng môi trường không? Back: Không, chỉ khác cấu hình qua biến môi trường, không khác code.

Chốt lại và bước tiếp theo

Ba môi trường tách nhau ở ba điểm: cấu hình, dữ liệu và cổng deploy. Cùng một codebase, ba bộ biến, ba database, và production là nơi duy nhất có bước xác nhận thủ công.

Bài tiếp theo đi vào phần dễ làm lộ nhầm nhất trong ba điểm trên: quản lý biến môi trường và secrets, cách đặt tên, cách inject khi deploy và cách xử lý khi secret đã bị lộ.