Node.js ile Environment Variable Yönetimi
Production ortamına deploy ettiğin uygulamanın database şifresi ya da API key’i kaynak koduna gömülü halde GitHub’a gitmiş mi? O an hissettirdiği panik duygusunu yaşamamak için environment variable yönetimini doğru öğrenmek şart. Node.js ekosisteminde bu konuyu düzgün halletmek hem güvenlik hem de farklı ortamlar arasında sorunsuz geçiş yapabilmek açısından kritik.
Environment Variable Nedir ve Neden Önemli?
Environment variable’lar, işletim sistemi seviyesinde tutulan anahtar-değer çiftleridir. Uygulaman çalışırken bu değerlere process.env nesnesi üzerinden erişirsin. Temel fikir şu: ortama özgü konfigürasyon bilgilerini (veritabanı bağlantı adresi, port numarası, API anahtarları, gizli şifreler) kaynak kodundan ayırmak.
Bu ayrımın sağladığı faydalar:
- Güvenlik: Hassas bilgilerin git repository’sine gitmesini önlersin
- Esneklik: Aynı kod tabanı development, staging ve production ortamlarında farklı konfigürasyonlarla çalışır
- 12-Factor App prensibi: Endüstri standardı uygulama geliştirme metodolojisine uyarsın
- Operasyonel kolaylık: Kodu yeniden deploy etmeden konfigürasyonu değiştirebilirsin
process.env ile Temel Kullanım
Node.js’te herhangi bir environment variable’a erişmek için process.env kullanırsın. Basit bir örnek:
# Terminal üzerinden geçici olarak set etmek
export DB_HOST=localhost
export DB_PORT=5432
export DB_NAME=myapp
node app.js
# Tek satırda uygulama başlatırken set etmek
DB_HOST=localhost DB_PORT=5432 node app.js
JavaScript tarafında bunu şöyle okursun:
# app.js içeriği (node ile çalıştırılacak)
cat > app.js << 'EOF'
const dbHost = process.env.DB_HOST;
const dbPort = process.env.DB_PORT || 5432;
const dbName = process.env.DB_NAME || 'development_db';
const nodeEnv = process.env.NODE_ENV || 'development';
console.log(`Ortam: ${nodeEnv}`);
console.log(`Veritabanı: ${dbHost}:${dbPort}/${dbName}`);
if (!dbHost) {
console.error('HATA: DB_HOST tanımlanmamış!');
process.exit(1);
}
EOF
node app.js
Burada dikkat etmen gereken bir şey var: process.env üzerinden okunan her değer string tipinde gelir. Port numarası veya benzeri sayısal değerleri kullanmadan önce dönüştürmen gerekir.
dotenv Paketi ile Çalışmak
Gerçek dünyada her ortam değişkenini terminal üzerinden export etmek pratik değil. Bu sorunu çözmek için .env dosyası ve dotenv paketi kullanılır.
# dotenv paketini kur
npm install dotenv
# Geliştirme bağımlılığı olarak kurabilirsin de
npm install --save-dev dotenv
Proje kök dizininde .env dosyası oluşturursun:
cat > .env << 'EOF'
NODE_ENV=development
PORT=3000
DB_HOST=localhost
DB_PORT=5432
DB_USER=appuser
DB_PASSWORD=supersecretpassword
DB_NAME=myapp_dev
JWT_SECRET=my-very-long-and-random-jwt-secret-key
REDIS_URL=redis://localhost:6379
SENDGRID_API_KEY=SG.xxxxxxxxxxxxxxxxxxxx
EOF
Uygulamanın entry point dosyasında, başka hiçbir şeyden önce dotenv’i yüklersin:
cat > server.js << 'EOF'
// Bu satır EN ÜSTTE olmalı
require('dotenv').config();
const express = require('express');
const app = express();
const port = parseInt(process.env.PORT, 10) || 3000;
const jwtSecret = process.env.JWT_SECRET;
if (!jwtSecret) {
throw new Error('JWT_SECRET environment variable tanımlanmamış!');
}
app.get('/health', (req, res) => {
res.json({
status: 'ok',
environment: process.env.NODE_ENV,
timestamp: new Date().toISOString()
});
});
app.listen(port, () => {
console.log(`Sunucu ${process.env.NODE_ENV} modunda port ${port} üzerinde çalışıyor`);
});
EOF
.gitignore ile Güvenlik
.env dosyasını oluşturduktan sonra yapman gereken ilk ve en kritik adım onu git takibinden çıkarmak:
# .gitignore dosyasına ekle
cat >> .gitignore << 'EOF'
# Environment variables
.env
.env.local
.env.*.local
.env.production
.env.staging
EOF
# Eğer .env zaten commit'lendiyse git cache'den temizle
git rm --cached .env
git commit -m "Remove .env from tracking"
Bunun yerine .env.example veya .env.template adında, gerçek değerler içermeyen bir şablon dosyası oluştururun ve bunu git’e eklersin:
cat > .env.example << 'EOF'
NODE_ENV=development
PORT=3000
DB_HOST=
DB_PORT=5432
DB_USER=
DB_PASSWORD=
DB_NAME=
JWT_SECRET=
REDIS_URL=
SENDGRID_API_KEY=
EOF
git add .env.example
git commit -m "Add .env.example template"
Ekibe yeni katılan birisi projeyi clone’ladığında cp .env.example .env yapıp kendi değerlerini doldurur. Bu yaklaşım hem güvenli hem de onboarding sürecini kolaylaştırır.
Ortama Göre Farklı Konfigürasyon Dosyaları
Büyük projelerde development, test, staging ve production için ayrı konfigürasyon dosyaları tutmak gerekir. dotenv bunu destekler:
# Ortam bazlı .env dosyaları
touch .env.development
touch .env.test
touch .env.staging
# .env.production GIT'E GİTMEZ!
# .env.development içeriği
cat > .env.development << 'EOF'
DB_HOST=localhost
DB_PORT=5432
LOG_LEVEL=debug
CACHE_TTL=60
EOF
# .env.test içeriği
cat > .env.test << 'EOF'
DB_HOST=localhost
DB_PORT=5433
DB_NAME=myapp_test
LOG_LEVEL=error
CACHE_TTL=0
EOF
Node.js tarafında hangi dosyanın yükleneceğini NODE_ENV‘e göre belirlersin:
cat > config/env.js << 'EOF'
const path = require('path');
const dotenv = require('dotenv');
const nodeEnv = process.env.NODE_ENV || 'development';
// Önce ortama özgü dosyayı yükle, sonra genel .env'i
const envFile = path.resolve(process.cwd(), `.env.${nodeEnv}`);
const defaultEnvFile = path.resolve(process.cwd(), '.env');
dotenv.config({ path: envFile });
dotenv.config({ path: defaultEnvFile });
console.log(`Konfigürasyon yüklendi: ${nodeEnv} ortamı`);
module.exports = {
nodeEnv,
port: parseInt(process.env.PORT, 10) || 3000,
db: {
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT, 10) || 5432,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
name: process.env.DB_NAME
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '7d'
}
};
EOF
Konfigürasyon Validasyonu
Environment variable’ları doğrudan process.env.XYZ şeklinde kullanmak yerine bir validasyon katmanı eklemek gerçek dünya uygulamalarında hayat kurtarır. joi veya envalid paketi bu iş için biçilmiş kaftan:
npm install envalid
cat > config/validate-env.js << 'EOF'
const { cleanEnv, str, port, url, bool, num } = require('envalid');
const env = cleanEnv(process.env, {
NODE_ENV: str({
choices: ['development', 'test', 'staging', 'production'],
default: 'development'
}),
PORT: port({ default: 3000 }),
DB_HOST: str({ desc: 'PostgreSQL host adresi' }),
DB_PORT: port({ default: 5432 }),
DB_USER: str(),
DB_PASSWORD: str(),
DB_NAME: str(),
JWT_SECRET: str({
desc: 'JWT token imzalama anahtarı',
docs: 'https://wiki.internal/jwt-setup'
}),
REDIS_URL: url({ default: 'redis://localhost:6379' }),
EMAIL_ENABLED: bool({ default: false }),
MAX_CONNECTIONS: num({ default: 10 })
});
// envalid, eksik veya hatalı değer varsa
// açıklayıcı hata mesajıyla process'i durdurur
module.exports = env;
EOF
Uygulama başlarken bu validasyon çalışırsa ve DB_HOST tanımlı değilse şöyle bir hata alırsın:
# envalid'in ürettiği hata çıktısı örneği
Invalid or missing environment variables:
DB_HOST: PostgreSQL host adresi (required)
JWT_SECRET: JWT token imzalama anahtarı
Refer to https://wiki.internal/jwt-setup for more info.
Bu yaklaşım sayesinde uygulaman eksik konfigürasyonla yarı çalışır hale gelmek yerine başlamayı reddeder. Production’da bir API key eksik olduğu için uygulamanın sessiz sedasız hata vermesi yerine açık bir hata mesajıyla durması çok daha tercih edilir bir durumdur.
Docker ve Container Ortamlarında Environment Variable
Uygulamanı Docker ile çalıştırıyorsan environment variable yönetimi biraz farklı işler. .env dosyasını container’ın içine kopyalamak yerine dışarıdan geçirirsin:
# docker run ile environment variable geçme
docker run -d
--name myapp
-p 3000:3000
-e NODE_ENV=production
-e DB_HOST=postgres
-e DB_PASSWORD=secretpassword
myapp:latest
# Ya da --env-file ile .env dosyasını kullan
docker run -d
--name myapp
-p 3000:3000
--env-file .env.production
myapp:latest
Docker Compose kullanıyorsan docker-compose.yml içinde şöyle tanımlarsın:
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
app:
build: .
ports:
- "${PORT:-3000}:3000"
environment:
- NODE_ENV=production
- DB_HOST=postgres
- DB_PORT=5432
env_file:
- .env.production
depends_on:
- postgres
- redis
postgres:
image: postgres:15
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
pgdata:
EOF
Burada Dockerfile içinde COPY .env . gibi bir satır kesinlikle olmamalı. Bu kritik bir güvenlik açığıdır.
PM2 ile Production Ortamında Environment Variable
Linux sunucularında Node.js uygulamalarını PM2 ile yönetiyorsan ecosystem dosyası üzerinden environment variable’ları yönetebilirsin:
cat > ecosystem.config.js << 'EOF'
module.exports = {
apps: [
{
name: 'myapp',
script: './server.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'development',
PORT: 3000
},
env_production: {
NODE_ENV: 'production',
PORT: 8080,
DB_HOST: 'prod-db.internal',
DB_PORT: 5432
},
env_staging: {
NODE_ENV: 'staging',
PORT: 8081,
DB_HOST: 'staging-db.internal'
}
}
]
};
EOF
# Production ortamıyla başlat
pm2 start ecosystem.config.js --env production
# Mevcut değişkenleri görüntüle
pm2 env 0
Dikkat: Hassas değerleri (şifreler, API key’leri) ecosystem dosyasına yazmaktan kaçın. Bu dosya da git’e gidebilir. Şifreler için sisteme önceden export yapmış olduğun değerleri process.env üzerinden okuman daha güvenlidir.
Systemd Servislerinde Environment Variable
Uygulamanı systemd servisi olarak çalıştırıyorsan environment variable’ları service dosyasında veya ayrı bir dosyada tanımlarsın:
# /etc/systemd/system/myapp.service
cat > /etc/systemd/system/myapp.service << 'EOF'
[Unit]
Description=My Node.js Application
After=network.target
[Service]
Type=simple
User=nodeapp
WorkingDirectory=/opt/myapp
EnvironmentFile=/etc/myapp/environment
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
# /etc/myapp/environment dosyası (600 izinleriyle koru)
mkdir -p /etc/myapp
cat > /etc/myapp/environment << 'EOF'
NODE_ENV=production
PORT=3000
DB_HOST=localhost
DB_PASSWORD=supersecretpassword
JWT_SECRET=very-long-random-string
EOF
# Sadece root okuyabilsin
chmod 600 /etc/myapp/environment
chown root:root /etc/myapp/environment
systemctl daemon-reload
systemctl enable myapp
systemctl start myapp
Gizli Değerler için Vault veya Secret Manager
Kurumsal ortamlarda environment variable’ları direkt dosyaya yazmak yerine bir secret manager kullanmak çok daha güvenlidir. HashiCorp Vault, AWS Secrets Manager veya Azure Key Vault gibi çözümler bu iş için kullanılır. Node.js uygulamasının başlangıcında bu servislerden değerleri çekip process.env‘e yazabilirsin:
cat > config/load-secrets.js << 'EOF'
const https = require('https');
async function loadSecretsFromVault() {
// Bu örnek AWS Secrets Manager için basitleştirilmiş gösterimdir
// Gerçek kullanımda @aws-sdk/client-secrets-manager paketi kullanılır
if (process.env.NODE_ENV !== 'production') {
console.log('Development ortamında yerel .env kullanılıyor');
require('dotenv').config();
return;
}
try {
// AWS SDK ile secret çekme (pseudocode)
const secretValue = await getSecretFromAWS(process.env.SECRET_NAME);
const secrets = JSON.parse(secretValue);
// Çekilen değerleri process.env'e yaz
Object.entries(secrets).forEach(([key, value]) => {
process.env[key] = value;
});
console.log('Secrets başarıyla yüklendi');
} catch (error) {
console.error('Secret yükleme hatası:', error.message);
process.exit(1);
}
}
module.exports = { loadSecretsFromVault };
EOF
Sık Yapılan Hatalar
Production ortamlarında tekrar tekrar karşılaştığım hatalar şunlar:
- Boolean değerleri string olarak okumak:
process.env.DEBUG === trueher zamanfalsedöner çünküprocess.envher şeyi string verir. Doğrusu:process.env.DEBUG === 'true' - .env dosyasını production sunucusuna kopyalamak: Sunucuya direkt environment variable set edin
- Tüm ortam değişkenlerini tek dosyada tutmak: Dosya büyüdükçe yönetimi zorlaşır, modüler yaklaşım benimse
- Default değersiz zorunlu değişkenler: Zorunlu değişkenler için mutlaka başlangıçta validasyon yap
- process.env değerlerini cache’lememek: Yüksek frekanslı işlemlerde
process.env.XYZher erişimde okunur, performans kritik yerlerde bir değişkene al
Sonuç
Environment variable yönetimi, kulağa basit gelen ama yanlış yapıldığında çok ciddi güvenlik açıklarına ve operasyonel baş ağrılarına yol açan bir konu. Temel prensipler şunlar: hassas bilgileri kaynak kodundan ayır, .env dosyalarını asla git’e commit’leme, uygulama başlarken tüm zorunlu değişkenleri validate et ve ortama göre farklı konfigürasyon dosyaları kullan.
dotenv paketi küçük ve orta ölçekli projeler için yeterli. Kurumsal ortamlarda ve mikroservis mimarilerinde Vault veya cloud secret manager’lara geçmek hem güvenlik hem de merkezi yönetim açısından doğru tercih. Systemd ile çalışan servisler için EnvironmentFile direktifi, Docker ortamları için --env-file parametresi günlük iş akışını kolaylaştırır.
Bu konuda en çok pişman olunan şey genellikle “başından düzgün kursaydım” cümlesiyle başlayan bir post-mortem toplantısında anlaşılıyor. Projenin başından itibaren bu yapıyı kurmak, sonradan refactor etmeye çalışmaktan kat kat kolay.
