Panduan Lengkap Environment Variables Management
Minggu, 28 Des 2025
Pernah push API key ke GitHub? Atau aplikasi crash di production karena environment variable yang lupa di-set? Kamu nggak sendiri. Environment variables management adalah skill yang sering diabaikan, padahal bisa jadi pembeda antara aplikasi yang secure dan disaster yang menunggu terjadi.
Di artikel ini, kita bahas dari dasar sampai production-ready practices untuk mengelola environment variables.
Kenapa Environment Variables Penting?
Environment variables adalah cara untuk menyimpan konfigurasi yang berbeda di setiap environment (development, staging, production) tanpa hardcode di source code.
Bayangkan kamu hardcode seperti ini:
// ❌ JANGAN PERNAH LAKUKAN INI
const stripe = new Stripe('sk_live_xxx123secretkey');
const dbUrl = 'postgresql://admin:[email protected]:5432/myapp';
Masalahnya:
- Security risk: Secret terekspos di Git history selamanya
- Tidak fleksibel: Harus ganti code untuk beda environment
- Susah di-rotate: Kalau key bocor, harus deploy ulang
Dengan environment variables:
// ✅ Cara yang benar
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const dbUrl = process.env.DATABASE_URL;
Struktur .env Files
Basic .env File
# .env
DATABASE_URL=postgresql://localhost:5432/myapp_dev
REDIS_URL=redis://localhost:6379
API_KEY=dev_api_key_12345
JWT_SECRET=super_secret_jwt_key
Multiple Environment Files
Best practice adalah punya beberapa file untuk environment berbeda:
project/
├── .env # Default values (committed, no secrets)
├── .env.local # Local overrides (gitignored)
├── .env.development # Development environment
├── .env.staging # Staging environment
├── .env.production # Production values (gitignored, atau di CI/CD)
└── .env.example # Template untuk developer baru
.env.example Template
File ini di-commit ke repo sebagai dokumentasi:
# .env.example
# Copy file ini ke .env.local dan isi dengan values yang sesuai
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
# Authentication
JWT_SECRET=your-jwt-secret-here
JWT_EXPIRES_IN=7d
# External APIs
STRIPE_SECRET_KEY=sk_test_xxx
SENDGRID_API_KEY=SG.xxx
# Feature Flags
ENABLE_NEW_DASHBOARD=false
Client vs Server Environment Variables
Ini sangat penting untuk framework seperti Next.js, Nuxt, atau SvelteKit.
Next.js Convention
# Server-side only (tidak terekspos ke browser)
DATABASE_URL=postgresql://...
STRIPE_SECRET_KEY=sk_live_xxx
# Client-side accessible (terekspos ke browser!)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxx
NEXT_PUBLIC_API_URL=https://api.example.com
Rule sederhana:
NEXT_PUBLIC_*= bisa diakses di browser (JANGAN taruh secrets!)- Tanpa prefix = hanya server-side
Kenapa Ini Penting?
// ❌ BAHAYA - secret terekspos ke browser
// Di component React
const apiKey = process.env.STRIPE_SECRET_KEY; // undefined di client
// ✅ AMAN - hanya di server
// Di API route atau getServerSideProps
export async function getServerSideProps() {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
// ...
}
Framework Lain
| Framework | Client Prefix | Server Default |
|---|---|---|
| Next.js | NEXT_PUBLIC_ | No prefix |
| Nuxt 3 | NUXT_PUBLIC_ | NUXT_ |
| Vite | VITE_ | Tidak ada akses server |
| Create React App | REACT_APP_ | Semua terekspos! |
| SvelteKit | PUBLIC_ | No prefix |
Dotenv dan Environment Loading
Basic Setup dengan dotenv
npm install dotenv
// src/config.ts
import dotenv from 'dotenv';
// Load .env file
dotenv.config();
// Atau load file spesifik
dotenv.config({ path: '.env.local' });
Loading Order (Next.js Style)
// Load berdasarkan NODE_ENV
import dotenv from 'dotenv';
import path from 'path';
const envFiles = [
`.env.${process.env.NODE_ENV}.local`,
`.env.local`,
`.env.${process.env.NODE_ENV}`,
'.env',
];
envFiles.forEach((file) => {
dotenv.config({ path: path.resolve(process.cwd(), file) });
});
Environment Validation dengan Zod
Jangan percaya environment variables tanpa validasi. Lebih baik app crash saat startup daripada runtime error yang random.
Setup Zod Validation
npm install zod
// src/env.ts
import { z } from 'zod';
const envSchema = z.object({
// Database
DATABASE_URL: z.string().url(),
// Server
NODE_ENV: z.enum(['development', 'staging', 'production']).default('development'),
PORT: z.coerce.number().default(3000),
// Authentication
JWT_SECRET: z.string().min(32, 'JWT_SECRET harus minimal 32 karakter'),
JWT_EXPIRES_IN: z.string().default('7d'),
// External Services
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
STRIPE_WEBHOOK_SECRET: z.string().startsWith('whsec_'),
// Optional with defaults
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
ENABLE_CACHE: z.coerce.boolean().default(true),
});
// Parse dan validate
const parsed = envSchema.safeParse(process.env);
if (!parsed.success) {
console.error('❌ Invalid environment variables:');
console.error(parsed.error.flatten().fieldErrors);
process.exit(1);
}
export const env = parsed.data;
Penggunaan
import { env } from './env';
// Type-safe dan validated!
console.log(env.DATABASE_URL); // string
console.log(env.PORT); // number
console.log(env.ENABLE_CACHE); // boolean
T3 Env (Recommended untuk Next.js)
npm install @t3-oss/env-nextjs zod
// src/env.mjs
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
server: {
DATABASE_URL: z.string().url(),
STRIPE_SECRET_KEY: z.string().min(1),
},
client: {
NEXT_PUBLIC_API_URL: z.string().url(),
},
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
},
});
Secrets Management untuk Production
Vercel
# Via CLI
vercel env add STRIPE_SECRET_KEY production
vercel env add DATABASE_URL production
# Pull env ke local
vercel env pull .env.local
Di Vercel Dashboard:
- Settings → Environment Variables
- Bisa set per environment (Production, Preview, Development)
- Sensitive values otomatis encrypted
Railway
# Via CLI
railway variables set STRIPE_SECRET_KEY=sk_live_xxx
# Atau link ke service
railway link
railway variables
Railway punya fitur Reference Variables:
# Reference dari database service
DATABASE_URL=${{Postgres.DATABASE_URL}}
AWS (Parameter Store & Secrets Manager)
Parameter Store (untuk config biasa):
aws ssm put-parameter \
--name "/myapp/production/API_KEY" \
--value "secret_value" \
--type SecureString
import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm";
const ssm = new SSMClient({ region: "ap-southeast-1" });
async function getParameter(name: string) {
const command = new GetParameterCommand({
Name: name,
WithDecryption: true,
});
const response = await ssm.send(command);
return response.Parameter?.Value;
}
const apiKey = await getParameter("/myapp/production/API_KEY");
Secrets Manager (untuk secrets yang sering di-rotate):
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";
const client = new SecretsManagerClient({ region: "ap-southeast-1" });
async function getSecret(secretName: string) {
const command = new GetSecretValueCommand({ SecretId: secretName });
const response = await client.send(command);
return JSON.parse(response.SecretString || '{}');
}
const dbCredentials = await getSecret("myapp/database");
HashiCorp Vault
Untuk enterprise-grade secrets management:
import Vault from 'node-vault';
const vault = Vault({
apiVersion: 'v1',
endpoint: 'https://vault.example.com:8200',
token: process.env.VAULT_TOKEN,
});
const secret = await vault.read('secret/data/myapp/production');
const dbPassword = secret.data.data.DB_PASSWORD;
12-Factor App Principles
Dari 12factor.net, prinsip #3 tentang Config:
Store config in the environment
Apa yang Termasuk “Config”?
✅ Harus di environment variables:
- Database credentials
- API keys dan secrets
- External service URLs
- Feature flags per environment
❌ Bukan config (tetap di code):
- Routes dan URL patterns
- Dependency injection config
- Internal business logic settings
Implementasi 12-Factor
// ✅ Good: Config dari environment
const config = {
db: {
url: process.env.DATABASE_URL,
poolSize: parseInt(process.env.DB_POOL_SIZE || '10'),
},
redis: {
url: process.env.REDIS_URL,
},
features: {
newCheckout: process.env.FEATURE_NEW_CHECKOUT === 'true',
},
};
// ❌ Bad: Hardcoded per environment
const config = {
db: {
url: process.env.NODE_ENV === 'production'
? 'postgresql://prod...'
: 'postgresql://localhost...',
},
};
Gitignore Best Practices
.gitignore untuk Environment Files
# Environment files
.env
.env.local
.env.*.local
.env.development.local
.env.test.local
.env.production.local
.env.staging
# Keep example file
!.env.example
# IDE
.idea/
.vscode/
# OS
.DS_Store
Pre-commit Hook untuk Cegah Secret Leak
Install git-secrets:
# macOS
brew install git-secrets
# Setup di repo
git secrets --install
git secrets --register-aws
Atau pakai gitleaks:
# Install
brew install gitleaks
# Scan repo
gitleaks detect --source . -v
GitHub Secret Scanning
Aktifkan di Settings → Code security → Secret scanning. GitHub akan alert kalau ada secret yang ke-push.
Development vs Production Environment
Separation of Concerns
// src/config/index.ts
import { z } from 'zod';
const baseSchema = z.object({
NODE_ENV: z.enum(['development', 'staging', 'production']),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']),
});
const developmentSchema = baseSchema.extend({
DATABASE_URL: z.string().default('postgresql://localhost:5432/myapp_dev'),
DEBUG: z.coerce.boolean().default(true),
});
const productionSchema = baseSchema.extend({
DATABASE_URL: z.string().url(),
DEBUG: z.literal(false).default(false),
SENTRY_DSN: z.string().url(),
});
export const env = process.env.NODE_ENV === 'production'
? productionSchema.parse(process.env)
: developmentSchema.parse(process.env);
Environment-Specific Behavior
// src/lib/logger.ts
import { env } from '../config';
export const logger = {
debug: (...args: unknown[]) => {
if (env.LOG_LEVEL === 'debug') {
console.debug('[DEBUG]', ...args);
}
},
info: (...args: unknown[]) => {
console.info('[INFO]', ...args);
},
error: (...args: unknown[]) => {
console.error('[ERROR]', ...args);
// Kirim ke Sentry di production
if (env.NODE_ENV === 'production') {
// Sentry.captureException(args[0]);
}
},
};
Environment Variables di Docker
Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# Jangan hardcode ENV di Dockerfile!
# ENV DATABASE_URL=xxx ❌
# Gunakan ARG untuk build-time variables
ARG NODE_ENV=production
ENV NODE_ENV=$NODE_ENV
EXPOSE 3000
CMD ["node", "dist/index.js"]
Docker Compose
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- LOG_LEVEL=debug
env_file:
- .env.local
depends_on:
- db
- redis
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
POSTGRES_DB: ${DB_NAME:-myapp}
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
postgres_data:
Docker Secrets (Swarm Mode)
# docker-compose.prod.yml
version: '3.8'
services:
app:
image: myapp:latest
secrets:
- db_password
- stripe_key
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
STRIPE_KEY_FILE: /run/secrets/stripe_key
secrets:
db_password:
external: true
stripe_key:
external: true
// Read from file in production
import fs from 'fs';
function getSecret(envVar: string): string {
const fileEnv = `${envVar}_FILE`;
if (process.env[fileEnv]) {
return fs.readFileSync(process.env[fileEnv], 'utf8').trim();
}
return process.env[envVar] || '';
}
const dbPassword = getSecret('DB_PASSWORD');
Common Mistakes dan Cara Menghindarinya
1. Commit .env ke Git
# Kalau sudah terlanjur commit
git rm --cached .env
echo ".env" >> .gitignore
git commit -m "Remove .env from tracking"
# Tapi secret sudah di history! Harus rotate semua keys.
Solusi: Pakai pre-commit hook dan secret scanning.
2. Tidak Validasi Environment Variables
// ❌ Crash di runtime
const port = parseInt(process.env.PORT); // NaN kalau undefined!
// ✅ Validate saat startup
const port = z.coerce.number().default(3000).parse(process.env.PORT);
3. Hardcode Conditional per Environment
// ❌ Susah maintain
const apiUrl = process.env.NODE_ENV === 'production'
? 'https://api.prod.com'
: process.env.NODE_ENV === 'staging'
? 'https://api.staging.com'
: 'http://localhost:3001';
// ✅ Satu source of truth
const apiUrl = process.env.API_URL;
4. Lupa Set di Production
// Checklist sebelum deploy
const requiredEnvs = [
'DATABASE_URL',
'JWT_SECRET',
'STRIPE_SECRET_KEY',
];
requiredEnvs.forEach((env) => {
if (!process.env[env]) {
throw new Error(`Missing required env: ${env}`);
}
});
5. Expose Secret di Client-Side
// ❌ NEXT_PUBLIC_ berarti terekspos ke browser!
NEXT_PUBLIC_DATABASE_URL=xxx // BAHAYA!
NEXT_PUBLIC_API_SECRET=xxx // BAHAYA!
// ✅ Hanya public keys untuk client
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxx
NEXT_PUBLIC_ANALYTICS_ID=G-XXXXXXX
6. Tidak Rotate Secrets
Buat reminder untuk rotate secrets secara berkala:
- API keys: setiap 90 hari
- Database passwords: setiap 30-90 hari
- JWT secrets: setiap 180 hari
7. Log Environment Variables
// ❌ JANGAN log secrets!
console.log('Config:', process.env);
console.log('DB URL:', process.env.DATABASE_URL);
// ✅ Mask sensitive values
const safeConfig = {
NODE_ENV: process.env.NODE_ENV,
DATABASE_URL: process.env.DATABASE_URL?.replace(/\/\/.*@/, '//***@'),
API_KEY: process.env.API_KEY ? '***' : 'not set',
};
console.log('Config:', safeConfig);
Checklist Environment Variables
Sebelum deploy, pastikan:
- Semua
.env*files (kecuali.env.example) ada di.gitignore - Environment variables di-validate saat startup
- Tidak ada hardcoded secrets di code
- Client-side variables tidak mengandung secrets
- Semua required env vars sudah di-set di production
- Pre-commit hook untuk detect secrets
- Secret scanning enabled di GitHub
- Dokumentasi
.env.exampleup to date - Secrets rotation schedule
Kesimpulan
Environment variables management bukan sekadar bikin file .env. Ini tentang:
- Security - Jangan pernah commit secrets
- Validation - Fail fast dengan Zod
- Separation - Client vs Server, Dev vs Prod
- Automation - Secret scanning dan pre-commit hooks
- Documentation -
.env.exampleyang selalu update
Mulai dari yang simple: setup .gitignore yang benar, validasi dengan Zod, dan pakai managed secrets di production platform seperti Vercel atau Railway.
Kalau ada pertanyaan atau mau diskusi lebih lanjut, reach out di Twitter atau comment di bawah. Happy coding! 🔐