Chapter 03

Configuration & Environment

Quản lý biến môi trường, cấu hình Nginx reverse proxy và connection strings

File .env

Docker Compose tự động đọc file .env cùng thư mục với docker-compose.yml. Đây là nơi tập trung mọi giá trị cấu hình:

.env
# ── PostgreSQL ──
POSTGRES_DB=myapp_db
POSTGRES_USER=myapp_user
POSTGRES_PASSWORD=S3cur3P@ssw0rd!Change-This

# ── Redis ──
REDIS_PASSWORD=R3d1sP@ss!Change-This

# ── Connection Strings (cho ASP.NET) ──
DB_CONNECTION=Host=postgres;Port=5432;Database=myapp_db;Username=myapp_user;Password=S3cur3P@ssw0rd!Change-This
REDIS_CONNECTION=redis:6379,password=R3d1sP@ss!Change-This

# ── ASP.NET ──
ASPNETCORE_ENVIRONMENT=Production
JWT_SECRET=your-jwt-secret-key-at-least-32-chars
JWT_ISSUER=https://app.example.com

# ── Domain ──
DOMAIN=app.example.com
File .env chứa mọi secrets của hệ thống. Tuyệt đối không commit lên Git. Thêm .env vào .gitignore và tạo file .env.example (không chứa giá trị thật) làm template.

File .env.example để commit lên git làm hướng dẫn:

.env.example
# Copy file này thành .env và điền giá trị thật
# cp .env.example .env

POSTGRES_DB=myapp_db
POSTGRES_USER=myapp_user
POSTGRES_PASSWORD=# Đặt password mạnh ở đây

REDIS_PASSWORD=# Đặt password mạnh ở đây

DB_CONNECTION=# Host=postgres;Port=5432;Database=...;Username=...;Password=...
REDIS_CONNECTION=# redis:6379,password=...

ASPNETCORE_ENVIRONMENT=Production
JWT_SECRET=# Ít nhất 32 ký tự
JWT_ISSUER=# https://your-domain.com
DOMAIN=# your-domain.com

Chuỗi override cấu hình ASP.NET

ASP.NET Core có hệ thống configuration phân tầng. Giá trị ở tầng sau sẽ ghi đè tầng trước:

appsettings.json
Base config
appsettings.{Env}.json
Per-environment
Environment Variables
Ưu tiên cao nhất
Trong Docker, environment variables có ưu tiên cao nhất. Nghĩa là bạn có thể giữ appsettings.json với giá trị mặc định (cho development), và override bằng env vars trong docker-compose.yml cho production. Không cần sửa file JSON.

Ví dụ mapping giữa JSON config và environment variable:

JSON
// appsettings.json — giá trị mặc định cho development
{
  "ConnectionStrings": {
    "DefaultConnection": "Host=localhost;Port=5432;Database=myapp_dev"
  },
  "Redis": {
    "ConnectionString": "localhost:6379"
  },
  "Jwt": {
    "Secret": "dev-secret-not-for-production",
    "Issuer": "https://localhost:5001"
  }
}
YAML
# docker-compose.yml — override bằng env vars cho production
backend:
  environment:
    # Dấu __ (double underscore) thay cho : trong JSON hierarchy
    - ConnectionStrings__DefaultConnection=${DB_CONNECTION}
    - Redis__ConnectionString=${REDIS_CONNECTION}
    - Jwt__Secret=${JWT_SECRET}
    - Jwt__Issuer=${JWT_ISSUER}
ASP.NET dùng __ (double underscore) để phân cấp hierarchy trong env vars. Ví dụ: ConnectionStrings__DefaultConnection tương đương ConnectionStrings:DefaultConnection trong JSON.

Nginx Reverse Proxy

Nginx đóng vai trò entry point duy nhất, phân phối traffic đến frontend (React) hoặc backend (API) dựa trên URL path:

Client
HTTPS request
Nginx :443
SSL termination
/api/* → backend:8080
/* → frontend:80
Nginx
# nginx/nginx.conf

events {
    worker_connections 1024;
}

http {
    # ── Upstream definitions ──
    upstream frontend {
        server frontend:80;
    }

    upstream backend {
        server backend:8080;
    }

    # ── Redirect HTTP → HTTPS ──
    server {
        listen 80;
        server_name app.example.com;
        return 301 https://$host$request_uri;
    }

    # ── Main HTTPS server ──
    server {
        listen 443 ssl;
        server_name app.example.com;

        # SSL certificates
        ssl_certificate     /etc/nginx/ssl/fullchain.pem;
        ssl_certificate_key /etc/nginx/ssl/privkey.pem;

        # SSL best practices
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers on;

        # Security headers
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

        # ── API requests → Backend ──
        location /api/ {
            proxy_pass http://backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            # Timeout cho long-running requests
            proxy_read_timeout 60s;
            proxy_send_timeout 60s;
        }

        # ── Everything else → Frontend ──
        location / {
            proxy_pass http://frontend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }

        # ── Gzip compression ──
        gzip on;
        gzip_types text/plain application/json application/javascript text/css;
        gzip_min_length 1000;
    }
}
Trong Docker network, các container giao tiếp với nhau bằng service name (ví dụ: frontend, backend) thay vì IP. Docker DNS tự động resolve service name thành IP của container.

Quản lý Secrets

So sánh các phương pháp quản lý secrets phổ biến:

📄

.env file

Đơn giản, phổ biến nhất
  • Dễ setup, Docker Compose hỗ trợ sẵn
  • Phù hợp cho single server
  • Cần quản lý permission file cẩn thận
  • Không phù hợp cho team lớn
🔐

Docker Secrets

Built-in, mount vào filesystem
  • Chỉ hỗ trợ Docker Swarm mode
  • Secrets lưu encrypted trên disk
  • Mount vào container dưới dạng file
  • App cần đọc từ file thay vì env var
Với deployment trên một server đơn, file .env là lựa chọn thực tế nhất. Đảm bảo: (1) file permission là 600 (chmod 600 .env), (2) nằm trong .gitignore, (3) không bao giờ log ra giá trị secrets.

Biến môi trường theo service

Chi tiết env vars quan trọng của từng service:

PostgreSQL Database +

POSTGRES_DB — Tên database sẽ được tạo tự động khi container khởi chạy lần đầu.

POSTGRES_USER — Username cho database. Tránh dùng postgres (superuser mặc định).

POSTGRES_PASSWORD — Password cho user trên. Dùng password mạnh (ít nhất 16 ký tự, mix chữ + số + ký tự đặc biệt).

PGDATA — (Optional) Thay đổi data directory bên trong container. Mặc định: /var/lib/postgresql/data.

Redis Cache +

Redis không dùng env vars mà cấu hình qua command trong docker-compose:

--requirepass — Bật authentication. Bắt buộc cho production, dù container không expose port ra ngoài.

--maxmemory 256mb — Giới hạn RAM sử dụng. Quan trọng vì Redis mặc định dùng unlimited memory.

--maxmemory-policy allkeys-lru — Khi hết memory, tự xóa keys ít sử dụng nhất.

ASP.NET API Backend +

ASPNETCORE_ENVIRONMENT — Phải là Production. Ảnh hưởng đến logging level, error pages, và config file nào được load.

ASPNETCORE_URLS — (Optional) URL Kestrel listen. Mặc định .NET 8 là http://+:8080.

ConnectionStrings__DefaultConnection — Npgsql connection string. Lưu ý dùng __ thay cho :.

Redis__ConnectionString — StackExchange.Redis connection string, format: host:port,password=xxx.

React Frontend Frontend +

React app được build thành static files nên không đọc env vars ở runtime.

Env vars được embed vào code lúc npm run build (chỉ các biến có prefix REACT_APP_).

REACT_APP_API_URL — URL của API. Với reverse proxy, thường là /api (relative path).

Nếu cần thay đổi config sau build, tạo file config.js/usr/share/nginx/html/ và load runtime.

Cấu hình Forwarded Headers

Khi ASP.NET chạy sau Nginx reverse proxy, cần cấu hình để nhận đúng IP và scheme (HTTPS) từ client:

C#
// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Cấu hình Forwarded Headers
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders = ForwardedHeaders.XForwardedFor
                             | ForwardedHeaders.XForwardedProto;
    // Trust Nginx proxy (trong Docker network)
    options.KnownNetworks.Clear();
    options.KnownProxies.Clear();
});

var app = builder.Build();

// PHẢI đặt TRƯỚC các middleware khác
app.UseForwardedHeaders();

app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
Nếu không cấu hình Forwarded Headers, ASP.NET sẽ: (1) thấy mọi request đến từ IP của Nginx container thay vì client thật, (2) nghĩ protocol là HTTP thay vì HTTPS — gây lỗi redirect loop và CORS issues.

CORS Configuration

Với reverse proxy, frontend và API cùng domain nên thường không cần CORS. Nhưng nếu cần:

C#
// Program.cs — Chỉ cần nếu frontend ở domain khác
builder.Services.AddCors(options =>
{
    options.AddPolicy("Production", policy =>
    {
        policy.WithOrigins("https://app.example.com")
              .AllowAnyMethod()
              .AllowAnyHeader()
              .AllowCredentials();
    });
});

// Sử dụng
app.UseCors("Production");
Khi dùng Nginx reverse proxy, React gọi /api/... (cùng origin) nên không cần CORS. CORS chỉ cần thiết khi frontend và API ở khác domain hoặc port.