Docker & DevOps

    Docker ile Node.js Uygulaması Yayınlama

    Node.js uygulamasını küçük ve güvenli bir konteynere paketleyip yayına almanın rehberi.

    10 dk okuma Güncellendi: 25 Ağustos 2026

    Node.js uygulamasını sunucuya git pull ile çekip npm install çalıştırmak ve pm2 ile ayağa kaldırmak yıllardır işe yarayan bir yöntem. Ancak bu yaklaşımda sunucudaki Node sürümü, işletim sistemi kütüphaneleri ve global paketler zamanla sürüklenir; yeni sunucuya geçtiğinizde aynı ortamı yeniden kurmanız gerekir. Docker ile Node.js uygulaması yayınlamak bu sürüklenmeyi ortadan kaldırır: uygulamanız, bağımlılıkları ve çalışma zamanı tek bir değişmez pakete girer ve bu paket hangi sunucuda çalışırsa çalışsın aynı davranır.

    Bu rehberde bir Node uygulamasını üretime hazır biçimde paketleyeceğiz. Çok aşamalı derleme ile imajı küçültmeyi, .dockerignore ile hem boyut hem güvenlik kazanmayı, root olmayan kullanıcıyla çalıştırmayı, sinyal yönetimini doğru kurup konteynerin düzgün kapanmasını sağlamayı, sağlık kontrolü tanımlamayı ve önüne bir ters proxy koyup SSL ile yayına almayı adım adım göreceğiz. Sonunda da "yerelde çalışıyor, konteynerde çalışmıyor" durumlarının somut nedenlerini ele alacağım.

    Node.js Uygulamasını Konteynerlemenin Temel Kuralları#

    Node uygulamalarında konteyner hataları neredeyse hep aynı beş sebepten çıkar ve bunları baştan bilmek saatler kazandırır.

    Birincisi, uygulamanız localhost yerine 0.0.0.0 üzerinde dinlemelidir. Konteyner içinde 127.0.0.1 yalnızca konteynerin kendisidir; dışarıdan gelen istekler bu arayüze ulaşamaz. Çoğu framework varsayılan olarak tüm arayüzleri dinler ama bazıları dinlemez, bu yüzden açıkça belirtmek en güvenlisidir.

    İkincisi, node_modules klasörü asla host'tan kopyalanmamalıdır. Yerel makinenizde derlenmiş yerel (native) modüller farklı bir işletim sistemi ve mimari için derlenmiştir; konteynere taşındığında çalışmaz. Üçüncüsü, bağımlılıklar npm ci ile kurulmalıdır: bu komut kilit dosyasını birebir uygular ve kilit dosyasıyla package.json çelişirse başarısız olur — npm install ise sessizce yeniden çözer.

    Dördüncüsü, uygulamanız SIGTERM sinyaline cevap vermelidir; aksi halde her yeniden dağıtımda istekler yarıda kesilir. Beşincisi, konteyner root kullanıcıyla çalışmamalıdır. Bu maddelerin gerekçelerini ve daha genel imaj kurallarını Dockerfile en iyi pratikler yazısında toplu olarak ele aldık.

    KuralYanlışDoğru
    Dinleme adresiapp.listen(3000, "127.0.0.1")app.listen(3000, "0.0.0.0")
    Bağımlılık kurulumunpm installnpm ci --omit=dev
    node_modulesHost'tan kopyalanırKonteynerde kurulur
    Kullanıcırootnode kullanıcısı
    KapanışSinyal yok sayılırSIGTERM dinlenir

    Çok Aşamalı Dockerfile: Küçük ve Güvenli İmaj#

    Tek aşamalı bir Dockerfile, derleme araçlarını ve geliştirme bağımlılıklarını da nihai imaja taşır. Çok aşamalı derleme bunu ikiye böler: ilk aşamada derleme yapılır, ikinci aşamaya yalnızca çalışması gereken dosyalar kopyalanır. TypeScript veya bir paketleyici kullanan projelerde fark, yüzlerce megabayt olabilir.

    # --- 1. AŞAMA: bağımlılıklar ve derleme ---
    FROM node:22-alpine AS build
    WORKDIR /app
    
    # Önce yalnızca manifest dosyaları: katman önbelleği için kritik
    COPY package*.json ./
    RUN npm ci
    
    # Şimdi kaynak kod
    COPY . .
    RUN npm run build
    
    # Geliştirme bağımlılıklarını at
    RUN npm prune --omit=dev
    
    # --- 2. AŞAMA: yalnızca çalışma zamanı ---
    FROM node:22-alpine AS runtime
    WORKDIR /app
    ENV NODE_ENV=production
    
    # Sinyalleri doğru ileten küçük bir init süreci
    RUN apk add --no-cache tini
    
    # Yalnızca gerekli çıktıyı taşı
    COPY --from=build /app/node_modules ./node_modules
    COPY --from=build /app/dist ./dist
    COPY --from=build /app/package.json ./
    
    # Resmi imajda hazır gelen ayrıcalıksız kullanıcı
    USER node
    
    EXPOSE 3000
    ENTRYPOINT ["/sbin/tini", "--"]
    CMD ["node", "dist/server.js"]
    

    Buradaki en önemli ayrıntı, package*.json dosyalarının kaynak koddan önce kopyalanmasıdır. Docker her satırı bir katman olarak önbelleğe alır; kaynak kodunuzda bir satır değiştiğinde yalnızca ondan sonraki katmanlar yeniden çalışır. Manifest dosyaları ayrı kopyalandığı için npm ci katmanı, bağımlılıklar değişmediği sürece önbellekten gelir ve derleme süreniz dakikalardan saniyelere iner.

    tini kullanmanın sebebi ise şudur: konteynerde PID 1 olan süreç, Linux'ta özel muamele görür ve varsayılan sinyal işleyicileri devrede değildir. Node doğrudan PID 1 olarak çalışırsa SIGTERM sinyalini yok sayabilir ve docker stop komutu on saniye bekleyip süreci zorla öldürür. Bu, işlenmekte olan isteklerin kesilmesi demektir.

    .dockerignore ve Katman Önbelleği#

    .dockerignore dosyası hem imaj boyutunu küçültür hem de kritik bir güvenlik işlevi görür: .env dosyanızın ya da .git klasörünüzün kazara imaja girmesini engeller. İmaja giren bir sır, imajı eline geçiren herkesin eline geçmiş demektir — hatta silinmiş olsa bile eski katmanlarda kalır.

    cat > .dockerignore << 'EOF'
    node_modules
    npm-debug.log
    dist
    .git
    .gitignore
    .env
    .env.*
    *.md
    .vscode
    coverage
    Dockerfile
    docker-compose.yml
    EOF
    

    node_modules satırı özellikle önemlidir. Bu satır olmadan COPY . . komutu host'taki yüzlerce megabaytlık klasörü konteynere taşır, sonra npm ci onu zaten ezer; yani hem yavaşlarsınız hem de yanlış derlenmiş yerel modüller riski taşırsınız. İmajın gerçekte ne kadar yer kapladığını ve katmanların nasıl dağıldığını görmek için:

    docker build -t uygulama:1.0 .
    docker images uygulama
    # REPOSITORY   TAG   SIZE
    # uygulama     1.0   142MB
    
    # Hangi katman ne kadar yer kaplıyor
    docker history uygulama:1.0 --format 'table {{.Size}}\t{{.CreatedBy}}' | head -15
    

    İmajı daha da küçültmek istiyorsanız temel imaj seçimi, gereksiz dosyaların ayıklanması ve katman birleştirme gibi teknikleri Docker imaj boyutu optimizasyonu yazısında ayrıntılı anlattık.

    docker-compose ile Yayına Alma#

    Üretimde uygulamayı tek başına değil, bağımlı servisleriyle birlikte tanımlarsınız. Aşağıdaki dosya uygulamayı bir veritabanı ve Redis ile birlikte ayağa kaldırıyor; sağlık kontrolü ve kaynak limitleri de eklenmiş durumda:

    services:
      app:
        build: .
        image: uygulama:1.0
        restart: unless-stopped
        environment:
          NODE_ENV: production
          PORT: 3000
          DATABASE_URL: postgres://uygulama:parola@db:5432/uygulama
          REDIS_URL: redis://redis:6379
        healthcheck:
          test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
          interval: 30s
          timeout: 5s
          retries: 3
          start_period: 20s
        deploy:
          resources:
            limits:
              memory: 512M
              cpus: "1.0"
        depends_on:
          - db
          - redis
    
      db:
        image: postgres:16-alpine
        restart: unless-stopped
        environment:
          POSTGRES_DB: uygulama
          POSTGRES_USER: uygulama
          POSTGRES_PASSWORD: parola
        volumes:
          - pgdata:/var/lib/postgresql/data
    
      redis:
        image: redis:alpine
        restart: unless-stopped
    
    volumes:
      pgdata:
    

    Uygulama portunun dışarıya yayımlanmadığına dikkat edin. Üretimde doğru yaklaşım, portu doğrudan internete açmak yerine önüne SSL sonlandıran bir ters proxy koymaktır; böylece hem sertifika yönetimi tek yerde toplanır hem de uygulama katmanı doğrudan taranmaz. Bu proxy katmanını nasıl kuracağınızı Traefik ile ters proxy ve otomatik SSL yazısında ele aldık.

    Kaynak limitleri de üretimde ihmal edilmemesi gereken bir başlıktır. Bellek sızıntısı olan bir Node süreci, limit yoksa sunucunun tüm belleğini tüketip diğer servisleri de düşürür. memory ve cpus değerlerini nasıl seçeceğinizi Docker konteyner kaynak limitleri yazısında ayrıntılandırdık.

    Sinyal Yönetimi ve Düzgün Kapanış#

    Bir dağıtım sırasında Docker önce SIGTERM gönderir, varsayılan olarak 10 saniye bekler, sonra SIGKILL ile süreci zorla sonlandırır. Uygulamanız bu 10 saniyeyi kullanarak açık istekleri bitirmeli ve veritabanı bağlantılarını kapatmalıdır. Aksi halde her dağıtımda bir miktar istek hata alır ve bu, düşük trafikte fark edilmez ama yükte belirgin hale gelir.

    # Uygulama kodunuzda bu iki sinyali dinlemelisiniz
    # (kavramsal akış)
    # 1. SIGTERM geldi
    # 2. Sunucu yeni bağlantı kabul etmeyi durdurur
    # 3. Açık istekler tamamlanır
    # 4. Veritabanı havuzu kapatılır
    # 5. process.exit(0)
    

    Kapanışın gerçekten çalıştığını ölçmek kolaydır:

    # Kapanma süresini ölç: 10 saniyeye yakınsa sinyal dinlenmiyordur
    time docker compose stop app
    # real  0m1.240s  → düzgün kapanış
    # real  0m10.310s → SIGTERM yok sayılıyor, tini ve sinyal dinleyici ekleyin
    

    Sağlık kontrolü de dağıtım kalitesini doğrudan etkiler. Uygulamanızda hafif bir /health uç noktası bulundurun; bu uç nokta veritabanına ağır sorgu atmamalı, yalnızca sürecin ayakta ve hazır olduğunu bildirmelidir. Konteynerin sağlık durumunu şöyle izlersiniz:

    docker compose ps
    # NAME   STATUS
    # app    Up 3 minutes (healthy)
    
    docker inspect --format='{{json .State.Health}}' app | head -5
    

    Ortam Değişkenleri ve Sırların Yönetimi#

    Node uygulamalarında yapılandırma neredeyse tamamen ortam değişkenleriyle yürür ve konteynerleşme bu alışkanlığı zorunlu hale getirir. Kural nettir: yapılandırma imaja gömülmez, çalışma zamanında verilir. Aynı imajın hem staging hem üretimde çalışabilmesinin tek yolu budur.

    Compose tarafında değişkenleri ayrı bir dosyada tutup env_file ile bağlamak, uzun environment listelerinden daha okunaklıdır ve dosyanın izinlerini kısıtlayabilmenizi sağlar:

    # Sır dosyası yalnızca sahibine okunur olsun
    cat > /opt/uygulama/uretim.env << 'EOF'
    NODE_ENV=production
    PORT=3000
    DATABASE_URL=postgres://uygulama:parola@db:5432/uygulama
    JWT_SECRET=uzun-ve-rastgele-bir-deger
    EOF
    chmod 600 /opt/uygulama/uretim.env
    
      app:
        image: uygulama:1.0
        env_file:
          - /opt/uygulama/uretim.env
    

    Değerlerin konteynere gerçekten ulaşıp ulaşmadığını tahmin etmeyin, doğrulayın. Aşağıdaki komut sırların değerini basmadan yalnızca anahtarların varlığını gösterir:

    docker compose exec app node -e "console.log(Object.keys(process.env).sort().join('
    '))" | head -20
    

    Sırların imaja sızmadığını da kontrol etmekte fayda var. docker history çıktısında bir ortam değişkeni görünüyorsa, o değer imajı çeken herkes tarafından okunabilir demektir:

    docker history --no-trunc uygulama:1.0 | grep -i "ENV\|SECRET\|PASSWORD"
    # Çıktı boşsa temizsiniz
    

    Rastgele ve yeterince uzun anahtar üretmek için parola üretici ya da hash üretici araçlarımızı kullanabilirsiniz; tahmin edilebilir bir JWT_SECRET, uygulamanın en zayıf halkasına dönüşür.

    Sık Yapılan Hatalar#

    Konteyner ayakta ama porta erişilemiyor — Uygulama 127.0.0.1 dinliyordur. Sunucuyu 0.0.0.0 üzerinde başlatın. Doğrulamak için konteyner içinden bakın: docker compose exec app netstat -tlnp çıktısında adres 0.0.0.0:3000 olmalıdır.

    Yerel modül hatası: invalid ELF header — Host'taki node_modules klasörü imaja kopyalanmıştır. .dockerignore dosyasına node_modules ekleyin ve önbelleksiz yeniden derleyin: docker build --no-cache -t uygulama:1.0 .

    İmaj çok büyük — Genellikle geliştirme bağımlılıkları ve derleme araçları nihai imajda kalmıştır. Çok aşamalı derlemeye geçin, npm ci --omit=dev kullanın ve temel imaj olarak alpine veya slim varyantını seçin. Yerel modül derlemesi gerekiyorsa derleme araçlarını yalnızca ilk aşamada kurun.

    Her derleme baştan bağımlılık kuruyorCOPY . . satırı npm ci satırından önce gelmiştir, bu yüzden herhangi bir kod değişikliği önbelleği geçersiz kılar. Manifest dosyalarını ayrı ve önce kopyalayın.

    Ortam değişkenleri görünmüyor.env dosyası imaja alınmamıştır ve alınmamalıdır da. Değişkenleri çalışma zamanında environment: bölümüyle ya da env_file: ile verin. Sırları imaja gömmek, imajı paylaştığınız anda sırrı da paylaşmanız demektir.

    Konteyner sürekli yeniden başlıyor — Uygulama başlarken çöküyordur; genellikle eksik bir ortam değişkeni ya da erişilemeyen bir veritabanıdır. docker compose logs --tail=50 app çıktısı gerçek hatayı verir. Bu döngünün olası nedenlerini konteyner sürekli yeniden başlıyor yazısında topladık.

    Sıkça Sorulan Sorular#

    Docker'da pm2 kullanmalı mıyım#

    Genellikle hayır. pm2'nin sunduğu süreç izleme ve yeniden başlatma işlerini Docker'ın restart politikası zaten yapar; ikisini üst üste koymak, konteynerin çöktüğünü Docker'dan gizler ve sağlık kontrolünü anlamsızlaştırır. Çok çekirdekli bir makinede birden fazla Node süreci çalıştırmak istiyorsanız, pm2 cluster yerine aynı imajdan birden fazla konteyner çalıştırıp önüne yük dengeleyici koymak Docker'a daha uygun bir çözümdür.

    Hangi Node imajını seçmeliyim#

    Çoğu uygulama için node:22-alpine iyi bir dengedir: küçük boyut, hızlı çekim. Ancak Alpine musl kütüphanesini kullanır ve bazı yerel modüller bununla derlenmekte sorun çıkarır; böyle bir durumda node:22-slim (Debian tabanlı) daha az sürtünme yaratır. Sürüm numarasını mutlaka sabitleyin; node:latest kullanmak, bir gün habersiz büyük sürüm atlamasına yol açar.

    İmajı sunucuya nasıl gönderirim#

    İki yaygın yol var: imajı bir kayıt defterine (registry) gönderip sunucuda çekmek, ya da sunucuda doğrudan derlemek. Kayıt defteri yöntemi daha temizdir çünkü sunucuda derleme araçları ve kaynak kod bulunmasına gerek kalmaz, ayrıca aynı imajı birden fazla sunucuya dağıtabilirsiniz. Genel kayıt defterlerinde çekim sınırlarına takılabilirsiniz; bu konuyu Docker Hub pull limiti hatası yazısında ele aldık.

    Sıfır kesintili dağıtım nasıl yapılır#

    Temel yöntem şudur: yeni sürümü ayrı bir konteyner olarak başlatın, sağlık kontrolü geçene kadar bekleyin, ters proxy'nin trafiği yeni konteynere yönlendirmesini sağlayın, sonra eskisini durdurun. Traefik gibi otomatik keşif yapan bir proxy bu geçişi kendiliğinden yönetir. Uygulamanızın düzgün kapanışı dinlemesi de bu akışın olmazsa olmaz parçasıdır.

    Statik dosyaları konteynerden mi sunmalıyım#

    Küçük projelerde sorun değildir, ancak trafik arttıkça statik dosyaları Node sürecine sunduramak boşuna kaynak harcar. Bunları ters proxy katmanından ya da bir CDN üzerinden sunmak hem daha hızlıdır hem de Node sürecini asıl işine bırakır. Derleme çıktısını proxy'nin okuyabileceği bir hacme yazmak yaygın bir yöntemdir.

    Loglara nasıl bakarım ve nasıl saklarım#

    Konteynerde loglar dosyaya değil standart çıktıya yazılmalıdır; docker compose logs -f app komutu canlı akışı verir. Uzun süreli saklama için Docker'ın log sürücüsünü yapılandırıp boyut sınırı koyun, yoksa log dosyaları diski doldurur. Log okuma ve konteyner içine girme yöntemlerini Docker konteyner loglarını okuma ve exec yazısında topladık.

    Kapanış#

    Node.js uygulamasını Docker ile yayınlarken aklınızda kalması gereken birkaç şey var: manifest dosyalarını kaynak koddan önce kopyalayarak katman önbelleğini kullanmak, çok aşamalı derleme ile geliştirme bağımlılıklarını nihai imajdan atmak, .dockerignore dosyasına node_modules ve .env yazmayı asla unutmamak, root olmayan bir kullanıcıyla ve doğru sinyal yönetimiyle çalışmak, sağlık kontrolü ile kaynak limitlerini üretim öncesi tanımlamak. Bu beşi yerindeyse dağıtımlarınız hem hızlanır hem de öngörülebilir hale gelir.

    Uygulamanızı barındıracağınız yer Docker'ı tam yetkiyle çalıştırabilmelidir; VDS ve bulut sunucu paketlerimiz bu senaryo için doğrudan uygundur, yoğun kaynak isteyen işler için dedicated sunucu seçeneğine bakabilirsiniz. Sunucu bakımını, güncellemeleri ve izlemeyi kendiniz üstlenmek istemiyorsanız sunucu yönetimi hizmetimiz bu yükü alır.

    Node.jsDockerDeploy

    Uygulamaya geçmeye hazır mısınız?

    NVMe SSD, ücretsiz SSL ve %99.9 uptime garantisiyle Clou.TR hosting ve sunucu çözümleriyle projenizi hayata geçirin.