Node.js Uygulamasını Docker ile Containerize Etme

Geliştirme ortamında “bende çalışıyor” problemi, her sysadmin’in kabusudur. Uygulamayı production’a taşıdığında Node.js versiyonu farklı, npm paketleri eksik, environment variable’lar yanlış… Docker tam olarak bu kabusu ortadan kaldırmak için var. Node.js uygulamalarını container’a almak, hem geliştirme hem de production süreçlerini ciddi ölçüde kolaylaştırıyor. Bu yazıda gerçek dünya senaryolarıyla Node.js uygulamanı nasıl düzgün containerize edeceğini adım adım anlatacağım.

Neden Node.js ve Docker Birlikte Kullanılır?

Node.js uygulamaları doğası gereği taşınabilir olmak ister, ama pratikte durum hiç de öyle değil. Farklı sunucularda farklı Node.js versiyonları, global npm paketleri, sistem kütüphaneleri… Bunların hepsi baş ağrısı kaynağı.

Docker ile birlikte şunları elde ediyorsun:

  • Tutarlı ortam: Geliştirme, test ve production aynı image üzerinde çalışır
  • Bağımlılık izolasyonu: Her uygulama kendi bağımlılıklarıyla birlikte gelir
  • Kolay ölçeklendirme: Kubernetes veya Docker Swarm ile kolayca scale edebilirsin
  • Hızlı deployment: Image bir kez build edilir, her yere aynı şekilde deploy edilir
  • Rollback kolaylığı: Eski image’a dönmek tek komut meselesi

Örnek Node.js Uygulaması Oluşturma

Önce üzerinde çalışacağımız basit ama gerçekçi bir Express.js uygulaması hazırlayalım. Bu uygulama basit bir REST API sunacak.

mkdir nodejs-docker-app
cd nodejs-docker-app
npm init -y
npm install express dotenv helmet morgan
npm install --save-dev nodemon

Uygulamamızın ana dosyasını oluşturalım:

cat > app.js << 'EOF'
const express = require('express');
const helmet = require('helmet');
const morgan = require('morgan');
require('dotenv').config();

const app = express();
const PORT = process.env.PORT || 3000;
const NODE_ENV = process.env.NODE_ENV || 'development';

app.use(helmet());
app.use(morgan('combined'));
app.use(express.json());

app.get('/health', (req, res) => {
  res.status(200).json({
    status: 'healthy',
    environment: NODE_ENV,
    timestamp: new Date().toISOString(),
    uptime: process.uptime()
  });
});

app.get('/api/users', (req, res) => {
  const users = [
    { id: 1, name: 'Ahmet Yilmaz', email: '[email protected]' },
    { id: 2, name: 'Fatma Demir', email: '[email protected]' },
    { id: 3, name: 'Mehmet Kaya', email: '[email protected]' }
  ];
  res.json({ success: true, data: users });
});

app.listen(PORT, () => {
  console.log(`Uygulama ${NODE_ENV} modunda ${PORT} portunda calisiyor`);
});

module.exports = app;
EOF

package.json dosyasını güncelleyelim:

cat > package.json << 'EOF'
{
  "name": "nodejs-docker-app",
  "version": "1.0.0",
  "description": "Node.js Docker ornek uygulamasi",
  "main": "app.js",
  "scripts": {
    "start": "node app.js",
    "dev": "nodemon app.js",
    "test": "echo "Tests running..." && exit 0"
  },
  "dependencies": {
    "dotenv": "^16.0.0",
    "express": "^4.18.0",
    "helmet": "^7.0.0",
    "morgan": "^1.10.0"
  },
  "devDependencies": {
    "nodemon": "^3.0.0"
  }
}
EOF

Dockerfile Yazma – Temel Yaklaşım

İlk Dockerfile’ı yazarken yapılan en büyük hata, her şeyi tek bir stage’e doldurmak. Production image’ına devDependencies, build araçları, gereksiz dosyalar giriyor. Önce basit versiyonla başlayalım, sonra optimize edelim.

cat > Dockerfile << 'EOF'
# Base image olarak resmi Node.js Alpine imajini kullaniyoruz
FROM node:20-alpine

# Calisma dizinini belirliyoruz
WORKDIR /app

# Once package dosyalarini kopyaliyoruz (layer cache icin)
COPY package*.json ./

# Sadece production bagimliklarini kuruyoruz
RUN npm ci --only=production

# Uygulama dosyalarini kopyaliyoruz
COPY . .

# Uygulamanin dinledigi portu belirtiyoruz
EXPOSE 3000

# Non-root kullanici olusturuyoruz (guvenlik)
RUN addgroup -g 1001 -S nodejs && 
    adduser -S nodeuser -u 1001 -G nodejs

USER nodeuser

# Uygulamayi baslatiyoruz
CMD ["node", "app.js"]
EOF

.dockerignore Dosyasini Oluşturma

.dockerignore dosyasini atlamak, Docker build sürecini yavaşlatır ve gereksiz dosyaların image’a girmesine neden olur. Bu dosya .gitignore gibi çalışır:

cat > .dockerignore << 'EOF'
node_modules
npm-debug.log
.git
.gitignore
.env
.env.*
*.md
.DS_Store
Dockerfile
docker-compose*.yml
coverage
.nyc_output
.vscode
.idea
tests
__tests__
*.test.js
*.spec.js
EOF

node_modules dizinini mutlaka .dockerignore‘a ekle. Hem build hızlandırır hem de host makinedeki node_modules’ün container’a kopyalanmasını engeller. Host ve container’ın farklı işletim sistemleri olabileceğini unutma, bazı native modüller platform bağımlı derlenebilir.

Multi-Stage Build ile Production-Ready Dockerfile

Gerçek production senaryolarında multi-stage build kullanmak şart. Development bağımlılıkları, test araçları, build araçları production image’ına girmemeli.

cat > Dockerfile.production << 'EOF'
# Stage 1: Build asamasi
FROM node:20-alpine AS builder

WORKDIR /app

# Package dosyalarini kopyala
COPY package*.json ./

# Tum bagimlilikları kur (dev dahil, build icin lazim olabilir)
RUN npm ci

# Kaynak kodu kopyala
COPY . .

# Eger TypeScript veya build adiminiz varsa burada calistirin
# RUN npm run build

# Stage 2: Production asamasi
FROM node:20-alpine AS production

# Guvenlik guncellestirmeleri icin
RUN apk update && apk upgrade && apk add --no-cache dumb-init

WORKDIR /app

# Non-root kullanici
RUN addgroup -g 1001 -S nodejs && 
    adduser -S nodeuser -u 1001 -G nodejs

# Sadece production bagimliklarini kopyala
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

# Builder stage'inden uygulama dosyalarini kopyala
COPY --from=builder --chown=nodeuser:nodejs /app/app.js ./
COPY --from=builder --chown=nodeuser:nodejs /app/package.json ./

USER nodeuser

EXPOSE 3000

# dumb-init ile PID 1 sorununu cozyoruz
CMD ["dumb-init", "node", "app.js"]
EOF

dumb-init kullanımı önemli bir detay. Node.js uygulamaları container içinde PID 1 olarak çalıştığında, SIGTERM sinyalini düzgün işlemeyebilir. dumb-init bu sorunu çözer, graceful shutdown sağlar.

Docker Image Build Etme ve Test Etme

Image’ı build edelim ve test edelim:

# Temel image'i build et
docker build -t nodejs-app:latest .

# Production image'i build et
docker build -f Dockerfile.production -t nodejs-app:production .

# Image boyutlarini karsilastir
docker images | grep nodejs-app

# Uygulamayi calistir
docker run -d 
  --name nodejs-container 
  -p 3000:3000 
  -e NODE_ENV=production 
  -e PORT=3000 
  --restart unless-stopped 
  nodejs-app:production

# Container loglarini takip et
docker logs -f nodejs-container

# Health check endpoint'ini test et
curl http://localhost:3000/health

# API endpoint'ini test et
curl http://localhost:3000/api/users

# Container icine gir ve kontrol et
docker exec -it nodejs-container sh

Docker Compose ile Geliştirme Ortamı

Tek başına docker run komutu geliştirme için pratik değil. Docker Compose ile hem geliştirme hem de production ortamını ayrı ayrı tanımlayalım:

cat > docker-compose.yml << 'EOF'
version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: production
    container_name: nodejs-app
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - PORT=3000
    env_file:
      - .env.production
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    networks:
      - app-network
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M

  # Development ortami icin ayri servis
  app-dev:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: nodejs-app-dev
    ports:
      - "3001:3000"
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - NODE_ENV=development
    command: npm run dev
    networks:
      - app-network
    profiles:
      - development

networks:
  app-network:
    driver: bridge
EOF

Development modunda çalıştırmak için:

# Production modda calistir
docker-compose up -d

# Development modda calistir (hot reload ile)
docker-compose --profile development up app-dev

# Servislerin durumunu kontrol et
docker-compose ps

# Logları takip et
docker-compose logs -f app

# Servisi yeniden baslat
docker-compose restart app

# Her seyi durdur ve sil
docker-compose down --volumes

Environment Variable Yonetimi

Hassas bilgileri Docker image’ına gömmek büyük bir güvenlik açığı. Environment variable’ları doğru yönetmek kritik önem taşıyor:

# .env.production dosyasini olustur (git'e ekleme!)
cat > .env.production << 'EOF'
NODE_ENV=production
PORT=3000
DB_HOST=postgres-server
DB_PORT=5432
DB_NAME=myapp_db
DB_USER=myapp_user
DB_PASSWORD=super_secret_password
JWT_SECRET=your_jwt_secret_here
REDIS_URL=redis://redis-server:6379
EOF

# .env.production'i .gitignore'a ekle
echo ".env.production" >> .gitignore
echo ".env.*" >> .gitignore

# Docker secrets kullanimi (Swarm modunda)
echo "super_secret_password" | docker secret create db_password -

# Secret'i kullanarak container baslatma
docker service create 
  --name nodejs-app 
  --secret db_password 
  -e DB_PASSWORD_FILE=/run/secrets/db_password 
  nodejs-app:production

Uygulama kodunda secret dosyasını okumak için küçük bir yardımcı fonksiyon ekleyebilirsin:

cat >> app.js << 'EOF'

// Docker Secret veya environment variable'dan deger oku
function getSecret(secretName, envName) {
  const secretPath = `/run/secrets/${secretName}`;
  try {
    const fs = require('fs');
    return fs.readFileSync(secretPath, 'utf8').trim();
  } catch (err) {
    return process.env[envName];
  }
}

// Kullanim
const dbPassword = getSecret('db_password', 'DB_PASSWORD');
EOF

Image Boyutunu Optimize Etme

Production image’larının küçük olması deployment hızını artırır ve güvenlik saldırı yüzeyini azaltır. Birkaç pratik optimizasyon:

# Mevcut image boyutunu kontrol et
docker images nodejs-app

# Alpine base image ile boyutu dusur
# node:20 yerine node:20-alpine kullanmak ~150MB tasarruf saglar

# npm cache'i temizle (build sirasinda)
# RUN npm ci --only=production && npm cache clean --force

# Gereksiz dosyalari sil
docker build 
  --no-cache 
  --build-arg NODE_ENV=production 
  -t nodejs-app:optimized 
  -f Dockerfile.production .

# Image katmanlarini incele
docker history nodejs-app:optimized

# Dive araci ile detayli analiz (once yukle: brew install dive)
dive nodejs-app:optimized

# Image'i tara ve vulnerabilities kontrol et
docker scout cves nodejs-app:production
# veya
docker scan nodejs-app:production

node:20-alpine kullanmak base image boyutunu ciddi ölçüde düşürür. Ancak Alpine’in musl libc kullandığını unutma, bazı native npm paketleri Alpine’de farklı davranabilir. Bu durumda node:20-slim daha iyi bir alternatif olabilir.

Container Log Yönetimi

Production ortamında log yönetimi kritik. Node.js uygulamanın loglarını düzgün yönetmek için:

# JSON formatinda log driver kullan
docker run -d 
  --name nodejs-app 
  --log-driver json-file 
  --log-opt max-size=10m 
  --log-opt max-file=3 
  -p 3000:3000 
  nodejs-app:production

# Logları belirli bir sure icin goster
docker logs --since 1h nodejs-app

# Son 100 satiri goster
docker logs --tail 100 nodejs-app

# Timestamp ile logları goster
docker logs -t nodejs-app 2>&1 | tail -50

# Log driver olarak syslog kullan
docker run -d 
  --log-driver syslog 
  --log-opt syslog-address=tcp://logserver:514 
  --log-opt tag="nodejs-app" 
  nodejs-app:production

Healthcheck ve Monitoring

Production container’larında healthcheck olmazsa olmaz. Orchestration sistemleri (Kubernetes, Swarm) bu bilgiyi kullanarak unhealthy container’ları yeniden başlatır:

# Dockerfile'a healthcheck ekle
cat >> Dockerfile << 'EOF'

HEALTHCHECK --interval=30s 
            --timeout=10s 
            --start-period=5s 
            --retries=3 
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
EOF

# Container health durumunu kontrol et
docker inspect --format='{{.State.Health.Status}}' nodejs-container

# Tum container'larin saglik durumunu goster
docker ps --format "table {{.Names}}t{{.Status}}t{{.Ports}}"

# Unhealthy container'ları tespit et
docker ps --filter health=unhealthy

CI/CD Pipeline ile Otomatik Build

Gerçek dünyada image build etme süreci CI/CD pipeline’ına entegre olmalı. GitHub Actions ile basit bir pipeline:

# .github/workflows/docker-build.yml olustur
mkdir -p .github/workflows
cat > .github/workflows/docker-build.yml << 'EOF'
name: Docker Build and Push

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ./Dockerfile.production
          push: true
          tags: |
            kullaniciadim/nodejs-app:latest
            kullaniciadim/nodejs-app:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
EOF

cache-from ve cache-to direktifleri build süresini dramatik ölçüde azaltır. GitHub Actions cache’ini kullanarak tekrarlayan build’larda layer cache’den yararlanabilirsin.

Sık Karşılaşılan Sorunlar ve Çözümleri

node_modules permission sorunu: Container içinde dosya yazma izni hatası alıyorsan, volume mount sırasında UID uyuşmazlığı var demektir.

# Host kullanicisinin UID'sini container'a aktar
docker run -d 
  --user $(id -u):$(id -g) 
  -v $(pwd):/app 
  nodejs-app:latest

# Veya Dockerfile'da spesifik UID kullan
# RUN adduser -u 1001 -S nodeuser

Port binding hatası: “address already in use” hatası için:

# Hangi process portu kullaniyor
sudo lsof -i :3000
sudo ss -tlnp | grep 3000

# Cakisan container'i bul ve durdur
docker ps | grep 3000
docker stop <container-id>

Out of Memory hatasi: Node.js’in varsayilan heap limiti container memory limitiyle çakışabilir:

# Node.js heap limitini belirle
docker run -d 
  -e NODE_OPTIONS="--max-old-space-size=256" 
  nodejs-app:production

# Container memory kullanimi izle
docker stats nodejs-container --no-stream

Sonuç

Node.js uygulamanı Docker ile containerize etmek ilk bakışta karmaşık görünebilir, ama doğru alışkanlıkları edinince son derece sistematik bir süreç haline geliyor. Özetlersek, production ortamı için kesinlikle multi-stage build kullan ve non-root kullanıcıyla çalış. .dockerignore dosyasını ihmal etme, hem build hızı hem de güvenlik için kritik. Environment variable’ları Docker secrets veya harici bir vault sistemiyle yönet, asla image içine gömme. Healthcheck eklemek, orchestration sistemlerinin container’larını doğru yönetmesi için şart.

En önemli nokta şu: Production image’ları mümkün olduğunca küçük ve minimal olmalı. Her ekstra paket, her gereksiz dosya potansiyel güvenlik açığı demek. Alpine base image kullan, build bağımlılıklarını final image’a taşıma, npm cache’ini temizle.

Bu alışkanlıkları edindikten sonra “bende çalışıyor” problemi tarih olacak. Aynı image geliştirme makinesinde, CI/CD pipeline’ında ve production sunucusunda birebir aynı şekilde çalışacak. Bu tutarlılık, özellikle ekip olarak çalışırken veya mikroservis mimarisine geçerken hayat kurtarıcı oluyor.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir