❤️ AZDIGI chính thức cập nhật hệ thống blog mới hoàn chỉnh. Tuy nhiên có thể một số bài viết bị sai lệch hình ảnh, hãy ấn nút Báo cáo bài viết ở cuối bài để AZDIGI cập nhật trong thời gian nhanh nhất. Chân thành cám ơn.

Bài 8, bạn đã dựng thành công LEMP Stack (Nginx + PHP-FPM + MariaDB) bằng Docker Compose, tự build image, tự viết config, kiểm soát hoàn toàn từng thành phần. Trong quá trình đó, bạn đã dùng file .envenvironment để truyền database credentials, PHP settings vào container.

Biến môi trường và file .env
Minh họa: Quản lý biến môi trường với file .env

Nhưng mình chỉ dùng ở mức cơ bản: hardcode vài giá trị, tạo một file .env duy nhất. Khi project lớn hơn : chạy trên nhiều môi trường (dev/staging/prod), có nhiều API keys, database credentials , bạn sẽ cần quản lý environment variables bài bản hơn.

Bài này sẽ cover toàn bộ những gì bạn cần biết: từ cú pháp cơ bản đến multi-environment setup, từ Docker secrets đến best practices cho production.

Tại sao cần biến môi trường?

Trước khi đi vào cú pháp, hãy hiểu tại sao biến môi trường (environment variables) lại quan trọng đến vậy.

Tách config khỏi code

Nguyên tắc 12-Factor App nói rõ: config phải tách khỏi code. Database password không nên nằm trong source code. Port, hostname, API endpoint, tất cả nên là biến môi trường.

# ❌ Hardcode trong code
DB_HOST = "192.168.1.100"
DB_PASS = "super_secret_123"
# ✅ Đọc từ environment variable
DB_HOST = os.getenv("DB_HOST")
DB_PASS = os.getenv("DB_PASS")

Bảo mật

Khi config nằm ngoài code, bạn có thể:

  • Commit code lên Git mà không lộ secrets
  • Mỗi người trong team có credentials riêng
  • Rotate (đổi) password mà không cần sửa code, rebuild image

Linh hoạt giữa các môi trường

Cùng một codebase, cùng một Docker image, nhưng chạy khác nhau tuỳ môi trường:

# Development
DEBUG=true
LOG_LEVEL=debug
DB_HOST=localhost
# Production
DEBUG=false
LOG_LEVEL=error
DB_HOST=prod-db.internal

Một image, nhiều cách chạy, đó là sức mạnh của environment variables.

Cách sử dụng environment trong Docker

Docker cung cấp nhiều cách để truyền biến môi trường vào container. Mình sẽ đi từ cơ bản đến nâng cao.

Cách 1: docker run -e

Cách đơn giản nhất, truyền trực tiếp khi chạy container:

# Truyền từng biến
docker run -e MYSQL_ROOT_PASSWORD=secret -e MYSQL_DATABASE=myapp mysql:8
# Truyền biến từ shell hiện tại (không có =VALUE)
export API_KEY="abc123"
docker run -e API_KEY myapp

Cách này OK cho test nhanh, nhưng khi có 10-20 biến thì command dài khủng khiếp. Đó là lúc Docker Compose toả sáng.

Cách 2: environment trong docker-compose.yml

Trong Docker Compose, bạn có 2 format để khai báo environment:

Map format (key: value), dễ đọc, rõ ràng:

services:
  app:
    image: myapp:latest
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: myapp
      DEBUG: "true"

List format (- KEY=VALUE), giống cách dùng docker run -e:

services:
  app:
    image: myapp:latest
    environment:
      - MYSQL_ROOT_PASSWORD=secret
      - MYSQL_DATABASE=myapp
      - DEBUG=true

Cả hai format hoạt động giống nhau. Mình thường dùng map format vì dễ đọc hơn, nhưng đây là sở thích cá nhân, bạn chọn cái nào quen thì dùng.

Lưu ý quan trọng: Với map format, giá trị true, false, yes, no cần được đặt trong dấu ngoặc kép "true" để tránh YAML parse thành boolean.

Cách 3: env_file trong docker-compose.yml

Khi có nhiều biến, tốt hơn là tách ra file riêng:

services:
  app:
    image: myapp:latest
    env_file:
      - ./app.env
db:
    image: mysql:8
    env_file:
      - ./db.env

File app.env:

# App configuration
APP_NAME=MyApp
APP_ENV=production
APP_DEBUG=false
APP_PORT=8080
# Database connection
DB_HOST=db
DB_PORT=3306
DB_NAME=myapp
DB_USER=appuser
DB_PASS=secure_password_here

Khác biệt quan trọng: env_file truyền biến vào bên trong container: container sẽ thấy các biến này. Còn file .env (sẽ nói ở phần tiếp) được Docker Compose dùng để thay thế biến trong chính file docker-compose.yml. Hai cái này khác nhau, đừng nhầm!

File .env chi tiết

File .env là một trong những tính năng hay nhất của Docker Compose. Hiểu rõ nó sẽ giúp bạn quản lý config cực kỳ gọn gàng.

Cú pháp

# Đây là comment — bị bỏ qua
# Cú pháp: KEY=VALUE (không có space quanh dấu =)

# ✅ Đúng MYSQL_ROOT_PASSWORD=secret123 APP_PORT=8080 APP_NAME=My Docker App

# ❌ Sai — có space quanh = MYSQL_ROOT_PASSWORD = secret123

# Giá trị có space → dùng quotes APP_DESCRIPTION="My awesome Docker application"

# Dòng trống bị bỏ qua

# Giá trị rỗng OPTIONAL_KEY=

Vị trí và cách hoạt động

File .env phải nằm cùng thư mục với file docker-compose.yml. Docker Compose tự động load file này mà không cần khai báo gì thêm.

my-project/
├── docker-compose.yml
├── .env                  ← Docker Compose tự động đọc file này
├── app.env               ← File này cần khai báo env_file: mới được đọc
└── src/

Khi Docker Compose đọc .env, nó dùng các giá trị để thay thế biến trong docker-compose.yml:

File .env:

MYSQL_VERSION=8.0
APP_PORT=8080

File docker-compose.yml:

services:
  db:
    image: mysql:${MYSQL_VERSION}    # → mysql:8.0
    ports:
      - "${APP_PORT}:80"             # → 8080:80

Thứ tự ưu tiên (Override)

Khi cùng một biến được định nghĩa ở nhiều nơi, Docker Compose theo thứ tự ưu tiên từ cao đến thấp:

1. Shell environment variable     ← Cao nhất
2. docker-compose.yml (environment:)
3. env_file
4. Dockerfile (ENV)
5. .env file                      ← Thấp nhất

Ví dụ thực tế:

# Trong .env
APP_PORT=8080

# Nhưng bạn set ở shell trước khi chạy export APP_PORT=9090

# Docker Compose sẽ dùng 9090 (shell env > .env) docker compose up -d

Đây là tính năng cực hữu ích: file .env chứa giá trị mặc định, nhưng bạn có thể override bất kỳ lúc nào bằng shell environment mà không cần sửa file.

Biến trong docker-compose.yml

Docker Compose hỗ trợ nhiều cú pháp để sử dụng biến trong file docker-compose.yml. Đây là những cú pháp bạn sẽ dùng thường xuyên:

${VARIABLE} – Cơ bản

services:
  web:
    image: nginx:${NGINX_VERSION}
    ports:
      - "${HOST_PORT}:80"

Nếu biến không tồn tại, Docker Compose sẽ thay bằng chuỗi rỗng (và có thể gây lỗi).

${VARIABLE:-default} – Giá trị mặc định

services:
  web:
    image: nginx:${NGINX_VERSION:-1.25}
    ports:
      - "${HOST_PORT:-8080}:80"
    environment:
      LOG_LEVEL: ${LOG_LEVEL:-info}

Nếu NGINX_VERSION không được set, Docker Compose sẽ dùng 1.25. Cú pháp này giúp file compose hoạt động ngay cả khi không có file .env.

${VARIABLE:?error} – Bắt buộc phải có

services:
  db:
    image: mysql:8
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:?Bạn phải set DB_ROOT_PASSWORD trong .env}

Nếu DB_ROOT_PASSWORD không được set, Docker Compose sẽ dừng lại và báo lỗi với message bạn chỉ định. Cực kỳ hữu ích cho những biến quan trọng mà bạn không muốn ai quên.

Tổng hợp lại:

┌──────────────────────────────┬─────────────────────────────────────────┐
│ Cú pháp                     │ Ý nghĩa                                │
├──────────────────────────────┼─────────────────────────────────────────┤
│ ${VAR}                       │ Dùng giá trị VAR, rỗng nếu không có   │
│ ${VAR:-default}              │ Dùng "default" nếu VAR không có/rỗng  │
│ ${VAR:+alternate}            │ Dùng "alternate" nếu VAR CÓ giá trị   │
│ ${VAR:?error message}        │ Báo lỗi nếu VAR không có              │
└──────────────────────────────┴─────────────────────────────────────────┘

Thực hành: Multi-environment setup

Đây là phần hay nhất, bạn sẽ thấy sức mạnh thực sự của environment variables khi dùng cùng một compose file cho nhiều môi trường.

Bước 1: Tạo cấu trúc project

mkdir -p ~/multi-env-demo && cd ~/multi-env-demo

Bước 2: Viết docker-compose.yml dùng chung

services:
  app:
    image: nginx:${NGINX_VERSION:-1.25-alpine}
    ports:
      - "${APP_PORT:?Phải set APP_PORT}:80"
    environment:
      APP_ENV: ${APP_ENV:-development}
      APP_DEBUG: ${APP_DEBUG:-true}
      LOG_LEVEL: ${LOG_LEVEL:-debug}
      DB_HOST: db
      DB_NAME: ${DB_NAME:-myapp}
      DB_USER: ${DB_USER:-appuser}
      DB_PASS: ${DB_PASS:?Phải set DB_PASS}
    depends_on:
      - db

db: image: mysql:${MYSQL_VERSION:-8.0} environment: MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:?Phải set DB_ROOT_PASSWORD} MYSQL_DATABASE: ${DB_NAME:-myapp} MYSQL_USER: ${DB_USER:-appuser} MYSQL_PASSWORD: ${DB_PASS:?Phải set DB_PASS} volumes: - db_data:/var/lib/mysql

volumes: db_data:

Chú ý: file compose không chứa bất kỳ giá trị nhạy cảm nào. Tất cả đều là biến, giá trị thực sẽ nằm trong file .env.

Bước 3: Tạo file .env cho từng môi trường

File .env.dev: cho development:

# Development Environment
APP_ENV=development
APP_DEBUG=true
APP_PORT=8080
LOG_LEVEL=debug

# Database DB_NAME=myapp_dev DB_USER=devuser DB_PASS=dev_password_123 DB_ROOT_PASSWORD=dev_root_123

# Versions NGINX_VERSION=1.25-alpine MYSQL_VERSION=8.0

File .env.staging: cho staging:

# Staging Environment
APP_ENV=staging
APP_DEBUG=true
APP_PORT=8888
LOG_LEVEL=warning

# Database DB_NAME=myapp_staging DB_USER=staging_user DB_PASS=staging_secure_pass_456 DB_ROOT_PASSWORD=staging_root_456

# Versions NGINX_VERSION=1.25-alpine MYSQL_VERSION=8.0

File .env.prod: cho production:

# Production Environment
APP_ENV=production
APP_DEBUG=false
APP_PORT=80
LOG_LEVEL=error

# Database DB_NAME=myapp_prod DB_USER=prod_user DB_PASS=xK9#mP2$vL5nQ8wR DB_ROOT_PASSWORD=rT7&jN4!hF6bY3cD

# Versions NGINX_VERSION=1.25-alpine MYSQL_VERSION=8.0

Bước 4: Chạy với từng môi trường

# Chạy Development — debug on, port 8080
docker compose --env-file .env.dev up -d

# Chạy Staging — debug on, port 8888 docker compose --env-file .env.staging up -d

# Chạy Production — debug off, port 80 docker compose --env-file .env.prod up -d

Cùng một file docker-compose.yml, nhưng behavior hoàn toàn khác nhau:

┌─────────────┬──────────────┬──────────┬───────────┐
│ Môi trường  │ Debug        │ Port     │ Log Level │
├─────────────┼──────────────┼──────────┼───────────┤
│ Development │ true         │ 8080     │ debug     │
│ Staging     │ true         │ 8888     │ warning   │
│ Production  │ false        │ 80       │ error     │
└─────────────┴──────────────┴──────────┴───────────┘

Kiểm tra biến đã truyền đúng chưa

# Xem compose file sau khi thay thế biến
docker compose --env-file .env.dev config

# Xem biến bên trong container đang chạy docker compose --env-file .env.dev exec app env | grep APP_

# Output: # APP_ENV=development # APP_DEBUG=true

Lệnh docker compose config cực kỳ hữu ích để debug, nó in ra file compose sau khi đã thay thế tất cả biến. Nếu có biến nào thiếu hoặc sai, bạn sẽ thấy ngay.

Docker secrets vs environment variables

Environment variables tiện lợi, nhưng có một vấn đề bảo mật quan trọng mà bạn cần biết.

Vấn đề với environment variables

Environment variables không thực sự bí mật. Bất kỳ ai có quyền truy cập Docker đều có thể xem:

# Xem TẤT CẢ env vars của container — bao gồm passwords
docker inspect my-container --format='{{json .Config.Env}}' | jq .
[
  "MYSQL_ROOT_PASSWORD=super_secret_123",
  "DB_PASS=another_secret",
  "API_KEY=sk-1234567890"
]
# Hoặc qua process list (trong container)
cat /proc/1/environ | tr '\0' '\n'

Điều này có nghĩa: nếu ai đó truy cập được Docker daemon, họ sẽ thấy tất cả secrets ở dạng plaintext.

Docker secrets – Giải pháp tốt hơn

Docker Secrets (hỗ trợ trong Docker Swarm và Compose) lưu trữ dữ liệu nhạy cảm an toàn hơn:

services:
  db:
    image: mysql:8
    environment:
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
    secrets:
      - db_root_password
secrets:
  db_root_password:
    file: ./secrets/db_root_password.txt

File ./secrets/db_root_password.txt chỉ chứa password:

xK9#mP2$vL5nQ8wR

So sánh 2 cách:

┌───────────────────────┬─────────────────────────┬─────────────────────────┐
│                       │ Environment Variables   │ Docker Secrets          │
├───────────────────────┼─────────────────────────┼─────────────────────────┤
│ Lưu trữ              │ Plaintext trong process  │ Mount as file trong     │
│                       │ environment              │ /run/secrets/           │
│ Visible qua inspect   │ ✅ Có                   │ ❌ Không                │
│ Visible trong logs    │ ⚠️ Có thể               │ ❌ Không                │
│ Dễ dùng              │ ✅ Rất dễ               │ ⚠️ App phải hỗ trợ     │
│ Production-ready      │ ⚠️ Cẩn thận            │ ✅ Recommended          │
└───────────────────────┴─────────────────────────┴─────────────────────────┘

Best practices cho production

  • Passwords, API keys, tokens → Dùng Docker secrets hoặc external secret manager (HashiCorp Vault, AWS Secrets Manager)
  • App config (ports, log levels, feature flags) → Environment variables là OK
  • Không bao giờ log environment variables trong application
  • Nhiều Docker images hỗ trợ cả 2: MYSQL_PASSWORD (env var) và MYSQL_PASSWORD_FILE (secret). Ưu tiên dùng _FILE variant khi có thể
  • Dùng least privilege: mỗi service chỉ nhận những biến nó cần, không share env file giữa các service

Patterns phổ biến

Dưới đây là những patterns bạn sẽ gặp đi gặp lại khi dùng environment variables trong Docker:

Database credentials

# .env
DB_HOST=db
DB_PORT=3306
DB_NAME=myapp
DB_USER=appuser
DB_PASS=secure_password

# docker-compose.yml services: app: environment: DATABASE_URL: mysql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_NAME}

db: image: mysql:8 environment: MYSQL_DATABASE: ${DB_NAME} MYSQL_USER: ${DB_USER} MYSQL_PASSWORD: ${DB_PASS} MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}

API keys

# .env
STRIPE_SECRET_KEY=sk_live_xxx
SENDGRID_API_KEY=SG.xxx
GOOGLE_MAPS_KEY=AIza_xxx
# docker-compose.yml
services:
  app:
    env_file:
      - ./api-keys.env    # Tách riêng file cho API keys
    environment:
      APP_ENV: ${APP_ENV:-production}

Feature flags

# .env
FEATURE_NEW_UI=true
FEATURE_BETA_API=false
FEATURE_DARK_MODE=true
# App đọc biến và bật/tắt feature tương ứng
services:
  app:
    environment:
      FEATURE_NEW_UI: ${FEATURE_NEW_UI:-false}
      FEATURE_BETA_API: ${FEATURE_BETA_API:-false}
      FEATURE_DARK_MODE: ${FEATURE_DARK_MODE:-false}

App configuration

# .env
APP_ENV=production
APP_DEBUG=false
APP_LOG_LEVEL=error
APP_TIMEZONE=Asia/Ho_Chi_Minh
APP_LOCALE=vi
WORKERS=4
MAX_UPLOAD_SIZE=50M
# docker-compose.yml
services:
  app:
    environment:
      APP_ENV: ${APP_ENV:-development}
      APP_DEBUG: ${APP_DEBUG:-true}
      LOG_LEVEL: ${APP_LOG_LEVEL:-info}
      TZ: ${APP_TIMEZONE:-UTC}
      PHP_WORKERS: ${WORKERS:-2}
      UPLOAD_MAX_SIZE: ${MAX_UPLOAD_SIZE:-10M}

Đừng quên .gitignore!

Đây là phần nhiều người quên, và hậu quả có thể rất nghiêm trọng. File .env chứa secrets KHÔNG ĐƯỢC commit lên Git.

# .gitignore

# Environment files chứa secrets .env .env.dev .env.staging .env.prod *.env

# Secret files secrets/

# NHƯNG commit file example để team biết cần những biến gì !.env.example

Tạo file .env.example: chứa tên biến nhưng không chứa giá trị thực:

# .env.example — Copy thành .env và điền giá trị thực
# cp .env.example .env

APP_ENV=development APP_DEBUG=true APP_PORT=8080

# Database — THAY ĐỔI GIÁ TRỊ NÀY DB_NAME=myapp DB_USER=your_db_user DB_PASS=your_db_password DB_ROOT_PASSWORD=your_root_password

# API Keys — Lấy từ dashboard của provider STRIPE_SECRET_KEY=sk_test_xxx SENDGRID_API_KEY=SG.xxx

Workflow chuẩn:

  1. Dev mới join team → clone repo
  2. cp .env.example .env
  3. Điền giá trị thực vào .env
  4. docker compose up -d → Chạy ngay!

Tip: Nếu lỡ commit .env rồi thì sao? Xoá file khỏi Git (nhưng giữ file local) rồi thêm vào .gitignore:

# Xoá khỏi Git tracking nhưng giữ file trên máy
git rm --cached .env

# Thêm vào .gitignore echo ".env" >> .gitignore

# Commit thay đổi git add .gitignore git commit -m "Remove .env from tracking, add to .gitignore"

Và nhớ: nếu password đã bị commit, hãy đổi password ngay lập tức. Git history vẫn giữ giá trị cũ dù bạn đã xoá file.

📚 Serie Docker từ A đến Z

  1. Bài 1: Docker là gì? Tại sao nên dùng Docker trên VPS
  2. Bài 2: Cài đặt Docker và Docker Compose trên VPS Ubuntu
  3. Bài 3: Làm quen với Docker – Các lệnh cơ bản cần biết
  4. Bài 4: Docker Image & Dockerfile – Tự tạo Image riêng
  5. Bài 5: Docker Volume & Network – Quản lý dữ liệu và mạng
  6. Bài 6: Docker Compose là gì? Cài đặt và cú pháp cơ bản
  7. Bài 7: Deploy WordPress + MySQL + phpMyAdmin bằng Docker Compose
  8. Bài 8: Deploy LEMP Stack (Nginx + PHP-FPM + MariaDB) bằng Docker Compose
  9. Bài 9: Biến môi trường & file .env trong Docker Compose (đang đọc)
  10. Bài 10: Reverse Proxy với Nginx Proxy Manager + SSL tự động
  11. Bài 11: Deploy ứng dụng Node.js / Python với Docker Compose
  12. Bài 12: Backup & Restore dữ liệu Docker Volume
  13. Bài 13: Monitoring Docker với Portainer, Uptime Kuma và cAdvisor
  14. Bài 14: Docker Logging – Quản lý log hiệu quả
  15. Bài 15: Bảo mật Docker trên VPS
  16. Bài 16: CI/CD đơn giản – Auto deploy với Webhook + Docker Compose
  17. Bài 17: Docker Compose trong thực tế – Tổng hợp project mẫu

Tổng kết

Qua bài này, bạn đã nắm được toàn bộ cách quản lý environment variables trong Docker:

  • 3 cách truyền biến: docker run -e, environment: trong compose, và env_file:
  • File .env: Docker Compose tự động load, dùng để thay thế biến trong compose file
  • Cú pháp biến: ${VAR}, ${VAR:-default}, ${VAR:?error}
  • Multi-environment: dùng --env-file để chạy cùng compose file trên dev/staging/prod
  • Docker secrets: an toàn hơn env vars cho passwords và API keys
  • Patterns: database credentials, API keys, feature flags, app config
  • .gitignore: luôn ignore file .env chứa secrets, dùng .env.example cho team

Đến đây, bạn đã có một stack Docker khá hoàn chỉnh: biết build image, dùng volumes và networks, viết Docker Compose, quản lý environment variables. Nhưng tất cả vẫn chạy qua IP và port, chưa có domain name, chưa có HTTPS.

Trong Bài 10, mình sẽ hướng dẫn bạn setup Reverse Proxy với Nginx Proxy Manager + SSL miễn phí: để container của bạn có domain riêng, HTTPS tự động, và sẵn sàng cho production thực sự. Hẹn gặp bạn ở bài tiếp theo! 🚀

👈 Bài trước: Deploy LEMP Stack (Nginx + PHP-FPM + MariaDB) bằng Docker Compose

👉 Bài tiếp: Reverse Proxy với Nginx Proxy Manager + SSL tự động

Chia sẻ:
Bài viết đã được kiểm duyệt bởi AZDIGI Team

Về tác giả

Thạch Phạm

Thạch Phạm

Đồng sáng lập và Giám đốc điều hành của AZDIGI. Có hơn 15 năm kinh nghiệm trong phổ biến kiến thức liên quan đến WordPress tại thachpham.com, phát triển website và phát triển hệ thống.

Hơn 10 năm phục vụ 80.000+ khách hàng

Bắt đầu dự án web của bạn với AZDIGI