Environment Variables, Secrets, and Configuration
Bài 17 – Quản lý biến môi trường và cấu hình production trong Next.js Environment Variables, Secrets và Config
1. Giới thiệu về Environment Variables, Secrets và Configuration
Environment Variables và Secrets là thành phần quan trọng trong mọi ứng dụng production. Next.js hỗ trợ quản lý biến môi trường thông qua file .env và hệ thống env runtime trên các nền tảng như Vercel hay Cloudflare. Việc quản lý đúng giúp bảo mật hệ thống và tránh để lộ thông tin nhạy cảm.
Lợi ích của quản lý biến môi trường hiệu quả:
- Bảo mật: Tách biệt thông tin nhạy cảm khỏi code
- Flexibility: Dễ dàng thay đổi config mà không cần rebuild
- Maintainability: Tập trung configuration trong một nơi
- Scalability: Dễ dàng deploy trên nhiều môi trường khác nhau
Các trường hợp sử dụng phổ biến:
- Database connection strings
- API keys và secrets
- Cấu hình third-party services
- Feature flags
- Environment-specific settings
2. Nội dung chính
2.1 Environment Variables trong Next.js
Next.js hỗ trợ nhiều loại file environment khác nhau:
.env.local: File mặc định cho local development.env.production: Dành cho production environment.env.development: Dành cho development environment.env: File mặc định (ít dùng)
Quy tắc naming conventions:
- Server-side variables: Không có prefix đặc biệt
- Client-side variables: Phải có prefix
NEXT_PUBLIC_ - Secrets: Không bao giờ được sử dụng
NEXT_PUBLIC_prefix
2.2 Loading Order và Priority
Next.js load environment variables theo thứ tự ưu tiên:
process.env(đã tồn tại).env.$(NODE_ENV).local.env.local(không được include trong Git).env.$(NODE_ENV).env
2.3 Built-in Environment Variables
Next.js cung cấp một số biến môi trường built-in:
# Development
NODE_ENV=development
NEXT_TELEMETRY_DISABLED=1
# Build
NEXT_BUILD_ID=custom-build-id
NEXT_IGNORE_TYPECHECK=1
# Runtime
NEXT_SHARP_PATH=/custom/path/to/sharp
NEXT_DISABLE_MEMORY_WATCHER=1
2.4 Security Best Practices
❌ Điều cần tránh:
- Không commit file
.env.localchứa secrets - Không hardcode sensitive data trong code
- Không sử dụng client-side variables cho sensitive data
- Không expose database credentials trong client code
✅ Nên làm:
- Sử dụng secret management services (Vercel, AWS Secrets Manager)
- Rotate secrets regularly
- Implement proper access controls
- Monitor và audit secret usage
2.5 Platform-Specific Configuration
Vercel Environment Variables:
# Production Environment
VERCEL_ENV=production
VERCEL_URL=your-app.vercel.app
VERCEL_GIT_PROVIDER=github
# Build Settings
VERCEL_BUILD_CACHE_ENABLED=1
VERCEL_FORCE_NO_BUILD_CACHE=0
Cloudflare Pages:
# Cloudflare-specific
CF_PAGES=1
CF_PAGES_BRANCH=main
CF_PAGES_URL=https://your-site.pages.dev
2.6 Advanced Configuration Options
Runtime Configuration:
// next.config.js
module.exports = {
env: {
CUSTOM_KEY: process.env.CUSTOM_KEY,
},
publicRuntimeConfig: {
// Available on both server and client
staticFolder: '/static',
},
serverRuntimeConfig: {
// Only available on server
secretKey: process.env.SECRET_KEY,
},
}
TypeScript Support:
// types/environment.d.ts
declare namespace NodeJS {
interface ProcessEnv {
DATABASE_URL: string;
API_KEY_SECRET: string;
NEXT_PUBLIC_API_URL: string;
}
}
3. Ví dụ thực tế
3.1 Cấu hình Environment Variables cơ bản
# .env.local - Local development
DATABASE_URL=postgres://admin:password@localhost:5432/myapp
API_KEY_SECRET=sk_test_1234567890abcdef
JWT_SECRET=your-super-secret-jwt-key
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_APP_NAME=My Awesome App
# .env.production - Production environment
DATABASE_URL=postgres://prod_user:secure_password@prod-db.example.com:5432/prod_db
API_KEY_SECRET=sk_live_0987654321fedcba
JWT_SECRET=prod-jwt-secret-key-min-32-characters
NEXT_PUBLIC_API_URL=https://api.production.com
NEXT_PUBLIC_APP_NAME=My Production App
# .env.development - Development environment
DATABASE_URL=postgres://dev_user:dev_password@dev-db.example.com:5432/dev_db
API_KEY_SECRET=sk_dev_1122334455667788
JWT_SECRET=dev-jwt-secret-key-for-testing
NEXT_PUBLIC_API_URL=https://api.dev.example.com
NEXT_PUBLIC_APP_NAME=My Dev App
3.2 Database Configuration với Environment Variables
// lib/database.ts
import { Pool } from 'pg'
// Server-side only - không expose credentials
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
max: 20, // Maximum number of clients in the pool
idleTimeoutMillis: 30000, // Close idle clients after 30 seconds
connectionTimeoutMillis: 2000, // Return an error after 2 seconds if connection could not be established
})
export async function query(text: string, params?: any[]) {
const start = Date.now()
try {
const res = await pool.query(text, params)
const duration = Date.now() - start
console.log('Executed query', { text, duration, rows: res.rowCount })
return res
} catch (error) {
console.error('Database query error:', error)
throw error
}
}
export async function getClient() {
return await pool.connect()
}
3.3 API Routes với Environment Variables
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
import jwt from 'jsonwebtoken'
// Secret key từ environment variable
const JWT_SECRET = process.env.JWT_SECRET!
const API_KEY = process.env.API_KEY_SECRET!
export async function GET(request: NextRequest) {
try {
// Validate API key từ header
const apiKey = request.headers.get('x-api-key')
if (apiKey !== API_KEY) {
return NextResponse.json({ error: 'Invalid API key' }, { status: 401 })
}
// Validate JWT token
const authHeader = request.headers.get('authorization')
if (!authHeader?.startsWith('Bearer ')) {
return NextResponse.json({ error: 'No token provided' }, { status: 401 })
}
const token = authHeader.split(' ')[1]
const decoded = jwt.verify(token, JWT_SECRET) as { userId: string }
// Lấy users từ database
const users = await getUsersFromDatabase(decoded.userId)
return NextResponse.json({
success: true,
users,
timestamp: new Date().toISOString()
})
} catch (error) {
console.error('API Error:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
async function getUsersFromDatabase(userId: string) {
// Implementation here
return [{ id: userId, name: 'John Doe' }]
}
3.4 Client Components với Public Environment Variables
// components/ApiClient.tsx
'use client'
import { useState, useEffect } from 'react'
export default function ApiClient() {
const [data, setData] = useState<any>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
// Public environment variables - an toàn để dùng trong client
const API_URL = process.env.NEXT_PUBLIC_API_URL
const APP_NAME = process.env.NEXT_PUBLIC_APP_NAME
useEffect(() => {
fetchData()
}, [])
const fetchData = async () => {
setLoading(true)
setError(null)
try {
const response = await fetch(`${API_URL}/data`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const result = await response.json()
setData(result)
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred')
} finally {
setLoading(false)
}
}
return (
<div className="p-4 border rounded-lg">
<h2 className="text-xl font-bold mb-2">{APP_NAME} - API Client</h2>
<p className="text-sm text-gray-600 mb-4">API URL: {API_URL}</p>
{loading && <div className="text-blue-600">Loading...</div>}
{error && <div className="text-red-600">Error: {error}</div>}
{data && (
<div className="bg-gray-100 p-3 rounded">
<pre className="text-sm">{JSON.stringify(data, null, 2)}</pre>
</div>
)}
<button
onClick={fetchData}
disabled={loading}
className="mt-4 px-4 py-2 bg-blue-500 text-white rounded disabled:bg-gray-300"
>
Refresh Data
</button>
</div>
)
}
3.5 Advanced next.config.js Configuration
// next.config.js
const { PHASE_DEVELOPMENT_SERVER, PHASE_PRODUCTION_BUILD } = require('next/constants')
module.exports = (phase, { defaultConfig }) => {
// Environment-specific configurations
const isDev = phase === PHASE_DEVELOPMENT_SERVER
const isProd = phase === PHASE_PRODUCTION_BUILD
const env = {
CUSTOM_KEY: process.env.CUSTOM_KEY,
ENVIRONMENT: isDev ? 'development' : isProd ? 'production' : 'other',
VERSION: process.env.npm_package_version || '1.0.0',
}
return {
...defaultConfig,
env,
// Security headers
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'X-Frame-Options',
value: 'DENY'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin'
}
]
}
]
},
// Redirects
async redirects() {
return [
{
source: '/old-path',
destination: '/new-path',
permanent: true,
}
]
},
// Rewrites
async rewrites() {
return [
{
source: '/api/:path*',
destination: `${process.env.API_BASE_URL}/:path*`,
}
]
},
// Image optimization
images: {
domains: [
process.env.NEXT_PUBLIC_IMAGE_DOMAIN || 'localhost',
'images.unsplash.com',
'via.placeholder.com'
],
formats: ['image/webp', 'image/avif'],
},
// Internationalization
i18n: {
locales: ['en', 'vi', 'ja'],
defaultLocale: 'en',
domains: [
{
domain: process.env.NEXT_PUBLIC_DOMAIN_EN || 'example.com',
defaultLocale: 'en',
},
{
domain: process.env.NEXT_PUBLIC_DOMAIN_VI || 'example.vn',
defaultLocale: 'vi',
}
]
},
// Experimental features
experimental: {
appDir: true,
serverComponentsExternalPackages: ['pg', 'redis']
},
// Webpack configuration
webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
// Environment-specific webpack configs
if (!dev) {
config.optimization.minimize = true
}
return config
},
}
}
3.6 Environment Validation và Type Safety
// lib/env.ts
import { z } from 'zod'
// Define schema cho environment variables
const envSchema = z.object({
// Server-side only
DATABASE_URL: z.string().url(),
API_KEY_SECRET: z.string().min(10),
JWT_SECRET: z.string().min(32),
// Client-side (public)
NEXT_PUBLIC_API_URL: z.string().url(),
NEXT_PUBLIC_APP_NAME: z.string().min(1),
NEXT_PUBLIC_ENABLE_ANALYTICS: z.string().transform(val => val === 'true'),
// Optional
REDIS_URL: z.string().url().optional(),
PORT: z.string().transform(Number).default('3000'),
})
// Validate environment variables
export const env = envSchema.parse(process.env)
// Type-safe environment access
export function getEnvVar(key: keyof typeof env): string {
return env[key]
}
// Usage example
export function validateEnvironment() {
try {
const validatedEnv = envSchema.parse(process.env)
console.log('Environment variables validated successfully')
return validatedEnv
} catch (error) {
console.error('Invalid environment variables:', error)
throw new Error('Environment validation failed')
}
}
3.7 Multi-environment Setup
# scripts/setup-env.sh
#!/bin/bash
echo "Setting up environment variables..."
# Create .env files if they don't exist
if [ ! -f .env.local ]; then
cp .env.example .env.local
echo "Created .env.local from template"
fi
if [ ! -f .env.development ]; then
cp .env.example .env.development
echo "Created .env.development from template"
fi
if [ ! -f .env.production ]; then
cp .env.example .env.production
echo "Created .env.production from template"
fi
# Validate required variables
required_vars=("DATABASE_URL" "API_KEY_SECRET" "JWT_SECRET")
for var in "${required_vars[@]}"; do
if [ -z "${!var}" ]; then
echo "Warning: $var is not set"
fi
done
echo "Environment setup complete!"
# .env.example - Template file
# Copy this file to create your environment files
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/myapp
# API Configuration
API_KEY_SECRET=your-api-key-here
JWT_SECRET=your-jwt-secret-min-32-characters
# Public Variables (Client-side)
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_APP_NAME=My Application
NEXT_PUBLIC_DOMAIN=example.com
# Optional Services
# REDIS_URL=redis://localhost:6379
# SENTRY_DSN=https://your-sentry-dsn
# STRIPE_SECRET_KEY=sk_test_...
# STRIPE_PUBLISHABLE_KEY=pk_test_...
# Feature Flags
# NEXT_PUBLIC_ENABLE_ANALYTICS=false
# NEXT_PUBLIC_ENABLE_DEBUG=false
4. Kiến thức trọng tâm
4.1 Security Best Practices
✅ Nên làm:
- Luôn validate environment variables trước khi sử dụng
- Sử dụng schema validation với thư viện như Zod hoặc Joi
- Implement proper error handling cho missing variables
- Document tất cả required environment variables
- Sử dụng secret management services cho production
- Rotate secrets regularly với automated processes
❌ Không nên làm:
- Không bao giờ commit secrets vào version control
- Không expose server-side variables trong client code
- Không hardcode sensitive information
- Không ignore environment validation errors
- Không share secrets qua insecure channels (email, chat)
4.2 Common Pitfalls và Solutions
Problem 1: Variables not available at build time
// ❌ Sai - biến không có sẵn trong build time
const config = {
apiUrl: process.env.API_URL || 'http://localhost:3000'
}
// ✅ Đúng - sử dụng trong function/component
function getConfig() {
return {
apiUrl: process.env.API_URL || 'http://localhost:3000'
}
}
Problem 2: TypeScript errors với process.env
// ❌ Sai - TypeScript không biết về custom env vars
const apiKey = process.env.API_KEY
// ✅ Đúng - Define types hoặc validate
const apiKey = process.env.API_KEY as string
// Hoặc sử dụng validation
const apiKey = validateEnvVar('API_KEY', process.env.API_KEY)
Problem 3: Environment-specific behavior
// ❌ Sai - Logic phụ thuộc vào NODE_ENV
if (process.env.NODE_ENV === 'development') {
// Development logic
}
// ✅ Đúng - Sử dụng explicit feature flags
if (process.env.NEXT_PUBLIC_ENABLE_DEBUG === 'true') {
// Debug logic
}
4.3 Deployment Strategies
Vercel Deployment:
# 1. Set environment variables
vercel env add DATABASE_URL production
vercel env add API_KEY_SECRET production
vercel env add NEXT_PUBLIC_API_URL production
# 2. Deploy với specific environment
vercel --prod
# 3. Verify deployment
vercel logs your-app.vercel.app
Docker Deployment:
# Dockerfile
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
RUN npm ci --only=production
# Copy application code
COPY . .
# Set environment (sẽ được override khi run)
ENV NODE_ENV=production
ENV PORT=3000
# Build application
RUN npm run build
# Expose port
EXPOSE 3000
# Start application
CMD ["npm", "start"]
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=${DATABASE_URL}
- API_KEY_SECRET=${API_KEY_SECRET}
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
env_file:
- .env.production
4.4 Monitoring và Debugging
Environment Validation Script:
// scripts/validate-env.ts
import { env } from '@/lib/env'
function validateEnvironment() {
const requiredVars = [
'DATABASE_URL',
'API_KEY_SECRET',
'JWT_SECRET',
'NEXT_PUBLIC_API_URL'
]
const missing = requiredVars.filter(varName => !env[varName as keyof typeof env])
if (missing.length > 0) {
console.error('❌ Missing environment variables:', missing)
process.exit(1)
}
console.log('✅ All environment variables are set')
}
validateEnvironment()
Runtime Environment Monitoring:
// lib/monitoring.ts
export function logEnvironmentInfo() {
console.log('Environment Information:', {
NODE_ENV: process.env.NODE_ENV,
NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME,
API_BASE_URL: process.env.NEXT_PUBLIC_API_URL,
BUILD_TIME: new Date().toISOString()
})
}
export function checkCriticalServices() {
// Check database connection
// Check API endpoints
// Check external services
return {
database: checkDatabase(),
api: checkAPI(),
timestamp: new Date().toISOString()
}
}
5. Bài tập nhanh
Bài tập 1: Complete Environment Setup
Tạo một complete environment configuration system:
Yêu cầu:
- Setup 3 môi trường: development, staging, production
- Implement environment validation với Zod
- Tạo utility functions cho type-safe env access
- Setup Docker configuration với environment variables
- Tạo deployment scripts cho mỗi môi trường
Gợi ý cấu trúc:
├── environments/
│ ├── development.env
│ ├── staging.env
│ └── production.env
├── scripts/
│ ├── validate-env.js
│ ├── deploy-dev.sh
│ └── deploy-prod.sh
├── docker/
│ ├── Dockerfile
│ └── docker-compose.yml
└── lib/
└── env.ts
Bài tập 2: Secret Management System
Implement một secret management system:
Yêu cầu:
- Tạo API endpoint để rotate secrets
- Implement secret caching với Redis
- Setup audit logging cho secret access
- Tạo admin dashboard để manage secrets
- Implement secret encryption/decryption
Gợi ý implementation:
// lib/secrets.ts
export class SecretManager {
async getSecret(key: string): Promise<string> {
// Implement caching, encryption, audit logging
}
async rotateSecret(key: string): Promise<void> {
// Implement secret rotation logic
}
}
Bài tập 3: Feature Flag System
Tạo feature flag system dựa trên environment variables:
Yêu cầu:
- Define feature flags trong environment
- Create React hooks để sử dụng feature flags
- Implement A/B testing capabilities
- Setup feature flag analytics
- Create admin interface để manage flags
Gợi ý:
// hooks/useFeatureFlag.ts
export function useFeatureFlag(flagName: string): boolean {
const flags = useContext(FeatureFlagContext)
return flags[flagName] || false
}
6. Kết luận
Quản lý đúng biến môi trường là kỹ năng quan trọng cho mọi developer Next.js. Việc implement proper security practices, validation, và deployment strategies giúp bảo mật ứng dụng và tạo quy trình deploy chuẩn.
Key Takeaways:
- Security First: Luôn prioritize security khi handle sensitive data
- Validation: Implement proper validation cho tất cả environment variables
- Type Safety: Sử dụng TypeScript để prevent runtime errors
- Documentation: Document tất cả required variables và use cases
- Automation: Automate validation, deployment, và monitoring processes
Best Practices Summary:
- ✅ Sử dụng schema validation cho environment variables
- ✅ Implement proper error handling và logging
- ✅ Sử dụng secret management services cho production
- ✅ Setup monitoring và alerting cho environment issues
- ✅ Document và maintain environment configuration
- ✅ Test environment setup trong CI/CD pipeline
Khi nào nên sử dụng các approach khác nhau:
- Simple Projects: Basic
.envfiles với manual validation - Enterprise Projects: Secret management services với audit logging
- Microservices: Centralized configuration management
- Open Source: Template files với clear documentation
Environment variables và configuration management là foundation của mọi production application. Đầu tư thời gian để setup đúng ngay từ đầu sẽ save rất nhiều thời gian và prevent security issues trong tương lai.