← Kursa Dön
📄 Text · 35 min

Bind Mounts — Geliştirme Ortamı Kullanımı

Named volume'ları öğrendik — veritabanı verileri gibi kalıcı şeyler için mükemmeller. Ama geliştirme yaparken farklı bir ihtiyacın var: kodu değiştirdiğinde container'da anında yansımasını istiyorsun. Her değişiklikte image rebuild etmek, container yeniden başlatmak — bu kabusu kimse yaşamak istemez.

İşte bind mount tam bu sorunu çözüyor. Host'taki bir dizini doğrudan container'ın içine "bağlarsın." Dosya host'ta değişir → container'da anında yansır. Container'da değişir → host'ta anında yansır. İki yönlü, anlık senkronizasyon. Geliştirme ortamının vazgeçilmezi.

Bunu bir mimarın çizim masası gibi düşün. Müşteriye teslim edilecek teknik çizimleri kasada saklarsın (Named Volume). Ama üzerinde aktif çalıştığın çizimi kasaya koymazsın — masanın üstünde, elinin altında olmalı. İşte bind mount senin çizim masan.


Bind Mount Nasıl Çalışır?

Named volume'da Docker nereye koyacağını kendisi bilir. Bind mount'ta ise sen dizini belirliyorsun:

docker run -d \
    -v $(pwd)/src:/app/src \
    -p 3000:3000 \
    node:20-alpine sh -c "cd /app && node src/server.js"

$(pwd)/src:/app/src ne demek? "Şu anki dizindeki src klasörünü, container'ın /app/src dizinine bağla." Artık host'taki src/ ile container'daki /app/src/ aynı dosyaları gösteriyor.

Host'ta bir dosya değiştirdiğinde container hemen görür. Container'da bir dosya oluşturduğunda host'ta hemen belirir. Senkronizasyon değil aslında — aynı dosyalar, sadece farklı yerlerden erişiliyor.


Hot Reload Geliştirme Ortamı

Bind mount'un en güçlü kullanımı bu: kod değiştir → otomatik yansısın → tarayıcıda gör. Yeniden build yok, yeniden başlatma yok.

Node.js + Nodemon

Hadi gerçek bir proje kuralım:

mkdir node-dev && cd node-dev

cat > package.json << 'EOF'
{
  "name": "docker-dev-example",
  "scripts": {
    "dev": "nodemon --watch src src/server.js",
    "start": "node src/server.js"
  },
  "dependencies": { "express": "^4.18.0" },
  "devDependencies": { "nodemon": "^3.0.0" }
}
EOF

mkdir src
cat > src/server.js << 'EOF'
const express = require('express');
const app = express();

app.get('/', (req, res) => {
    res.json({ message: 'Merhaba Docker Dev!', version: '1.0' });
});

app.listen(3000, '0.0.0.0', () => {
    console.log('Server running on port 3000');
});
EOF

Şimdi Docker ile geliştirme ortamını ayağa kaldıralım:

docker run -it --rm \
    --name node-dev \
    -v $(pwd):/app \
    -v /app/node_modules \
    -w /app \
    -p 3000:3000 \
    node:20-alpine \
    sh -c "npm install && npm run dev"

Bu komutu açıklayayım. -v $(pwd):/app ile proje dizinini container'a bağlıyoruz — bu bind mount. -v /app/node_modules ile node_modules'ü anonymous volume'a alıyoruz — birazdan nedenini açıklayacağım. -w /app çalışma dizinini ayarlıyor. Ve son olarak npm install yapıp nodemon ile başlatıyoruz.

Şimdi tarayıcıda http://localhost:3000 adresini aç — JSON yanıtını göreceksin. Sonra src/server.js dosyasını düzenle:

app.get('/', (req, res) => {
    res.json({ message: 'Güncellendi!', version: '2.0' });
});

Kaydet. Terminal'de nodemon'un restart ettiğini göreceksin. Tarayıcıyı yenile — yeni mesaj! Image rebuild etmedin, container yeniden başlatmadın — dosya değişti, nodemon yakaladı, otomatik restart etti.

Kritik satır: -v /app/node_modules — Bu neden var? Çünkü $(pwd):/app tüm proje dizinini mount eder, bu da host'taki node_modules'ü container'a taşır. Ama sorun şu: macOS'ta npm install edersen Linux native modülleri (bcrypt gibi) çalışmaz. Bu anonymous volume, container'ın kendi node_modules'ünü kullanmasını sağlar, host'takiyle çakışmayı önler.

React / Vite Frontend

docker run -it --rm \
    --name react-dev \
    -v $(pwd)/src:/app/src \
    -v $(pwd)/public:/app/public \
    -v $(pwd)/index.html:/app/index.html \
    -p 5173:5173 \
    -e CHOKIDAR_USEPOLLING=true \
    node:20-alpine \
    sh -c "cd /app && npm run dev -- --host 0.0.0.0"

Burada tüm projeyi değil, sadece değişen dizinleri mount ediyoruz: src/, public/, index.html. node_modules mount etmiyoruz — container'ın kendi kopyasını kullansın. Bu hem daha performanslı hem de uyumluluk sorunu yaratmaz.

CHOKIDAR_USEPOLLING=true ise macOS ve Windows'ta dosya değişikliğini algılamak için gerekli — birazdan detaylıca anlatacağım.

Python + Flask

docker run -it --rm \
    -v $(pwd):/app \
    -w /app \
    -p 5000:5000 \
    -e FLASK_DEBUG=1 \
    python:3.12-slim \
    sh -c "pip install -r requirements.txt && flask run --host=0.0.0.0"

Flask'ın FLASK_DEBUG=1 modu, dosya değişikliğinde otomatik restart yapar. Bind mount ile birleşince mükemmel hot reload ortamı oluşur.


Konfigürasyon Dosyaları Mount Etme

Bind mount'un ikinci büyük kullanım alanı: container'a özel konfigürasyon vermek. Önemli olan konfigürasyonları read-only mount etmek — container'ın yanlışlıkla veya kötü niyetle değiştirmesini önlemek için.

Nginx Custom Config

cat > nginx.conf << 'EOF'
events { worker_connections 1024; }
http {
    server {
        listen 80;
        location / {
            root /usr/share/nginx/html;
            try_files $uri $uri/ /index.html;
        }
        location /api/ {
            proxy_pass http://api:3000/;
        }
    }
}
EOF

docker run -d --name web \
    -v $(pwd)/nginx.conf:/etc/nginx/nginx.conf:ro \
    -v $(pwd)/html:/usr/share/nginx/html:ro \
    -p 80:80 \
    nginx:alpine

:ro — read-only. Container bu dosyaları okuyabilir ama değiştiremez. Dene:

docker exec web sh -c 'echo "hack" > /etc/nginx/nginx.conf'
# sh: can't create /etc/nginx/nginx.conf: Read-only file system

Init Script'leri

PostgreSQL'e başlangıçta tablo oluşturmak istiyorsun:

mkdir init-scripts

cat > init-scripts/01-create-tables.sql << 'EOF'
CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);
EOF

cat > init-scripts/02-seed-data.sql << 'EOF'
INSERT INTO users (username) VALUES ('admin'), ('demo')
ON CONFLICT (username) DO NOTHING;
EOF

docker run -d --name postgres \
    -v $(pwd)/init-scripts:/docker-entrypoint-initdb.d:ro \
    -v pgdata:/var/lib/postgresql/data \
    -e POSTGRES_PASSWORD=secret \
    postgres:16

PostgreSQL, /docker-entrypoint-initdb.d/ dizinindeki SQL dosyalarını sadece ilk başlatmada çalıştırır. Dosyalar alfabetik sırayla çalışır — bu yüzden 01-, 02- gibi prefix'ler kullandık. Ve tabii ki :ro ile mount ettik çünkü container'ın bu dosyaları değiştirmesine gerek yok.


Tek Dosya Mount Etme — Dikkatli Ol!

Dizin yerine tek bir dosya da mount edebilirsin:

docker run -d \
    -v $(pwd)/my-nginx.conf:/etc/nginx/nginx.conf:ro \
    nginx:alpine

Ama burada bir tuzak var. Editörün (vim, nano, VS Code) dosyayı nasıl kaydettiğine bağlı olarak, container değişikliği görmeyebilir!

Neden? Çoğu editör dosyayı "kaydet" dediğinde aslında eski dosyayı silip yenisini oluşturur. Bu, dosyanın inode'unu değiştirir. Container ise hâlâ eski inode'a bakıyor — yeni dosyayı görmez.

Çözüm 1: Dosya yerine dizin mount et — dizin mount'unda bu sorun olmaz.

Çözüm 2: echo veya sed kullan — aynı inode'u günceler:

sed -i 's/old-value/new-value/' config.txt

Çözüm 3: Container'ı restart et:

docker restart myapp

Genel tavsiyem: mümkünse tek dosya yerine dizin mount et. Daha az sorun yaşarsın.


macOS ve Windows Performans Sorunu

Eğer macOS veya Windows'ta geliştirme yapıyorsan, bu bölüm senin için çok önemli.

Linux'ta bind mount performansı mükemmeldir — host ve container aynı kernel'ı kullanır, dosya erişimi doğrudan yapılır. Ama macOS ve Windows'ta Docker bir VM içinde çalışır. Host dosya sistemi (APFS veya NTFS) ile container dosya sistemi (ext4) arasında sürekli çeviri yapılır:

Linux:   Host (ext4) → Container (overlay2) → Direkt erişim → HIZLI
macOS:   Host (APFS) → VM (Linux) → Container → Çeviri → YAVAŞ
Windows: Host (NTFS) → WSL2 (ext4) → Container → Çeviri → YAVAŞ

Tipik performans farkı:

# npm install süresi
# Bind mount ile (macOS): ~120 saniye 🐌
# Named volume ile (macOS): ~15 saniye 🚀
# 8 kat fark!

Neden bu kadar fark var? Node.js projelerinde node_modules dizininde onbinlerce dosya var. Her dosya erişimi bind mount overhead'i ekliyor.

Çözüm 1 — VirtioFS (macOS, En İyi)

Docker Desktop ayarlarından General → VirtioFS seçeneğini aktif et. Apple Silicon Mac'lerde varsayılan ve en hızlı seçenek:

# VirtioFS öncesi: ~120 saniye
# VirtioFS sonrası: ~30 saniye

Çözüm 2 — Selective Sync (Sadece Gerekeni Mount Et)

Tüm projeyi mount etmek yerine sadece kaynak kodu mount et:

# ❌ Tüm proje — node_modules dahil, yavaş
docker run -v $(pwd):/app myapp

# ✅ Sadece kaynak kodu — hızlı
docker run \
    -v $(pwd)/src:/app/src \
    -v $(pwd)/public:/app/public \
    -v $(pwd)/package.json:/app/package.json \
    -v node_modules:/app/node_modules \
    myapp

Çözüm 3 — node_modules için Anonymous Volume

En yaygın ve basit pattern:

docker run \
    -v $(pwd):/app \
    -v /app/node_modules \
    myapp

İlk -v tüm projeyi mount eder. İkinci -v node_modules'ü anonymous volume ile "maskeler" — host ile sync olmaz, container kendi node_modules'ünü kullanır.


File Watching ve Polling

Container içindeki dosya izleme mekanizmaları (nodemon, webpack watch, vite) bind mount'ta farklı davranabilir.

Linux'ta dosya değişikliği inotify ile algılanır — verimli ve anında. Ama macOS/Windows bind mount'ta inotify çalışmaz çünkü dosya değişikliği host'ta oluyor, container inotify event almıyor.

Çözüm: polling mode — belirli aralıklarla dosyaların değişip değişmediği kontrol edilir:

# Nodemon
docker run -v $(pwd):/app node:20-alpine \
    npx nodemon --legacy-watch src/server.js

# Vite
# vite.config.js: server: { watch: { usePolling: true } }

# Chokidar (React, Vue, Angular)
docker run -e CHOKIDAR_USEPOLLING=true -v $(pwd):/app myapp

# Webpack 5
docker run -e WATCHPACK_POLLING=true -v $(pwd):/app myapp

Docker Compose'da:

services:
  frontend:
    volumes:
      - ./src:/app/src
    environment:
      - CHOKIDAR_USEPOLLING=true
      - WATCHPACK_POLLING=true

⚠️ Performans notu: Polling CPU kullanır. Büyük projelerde polling aralığını artır (1000ms → 3000ms) veya sadece src/ dizinini izle.


Güvenlik Konuları

Bind mount ile host dosya sistemine erişim veriyorsun. Bu güçlü ama tehlikeli olabilir.

Asla yapma — host'un hassas dizinlerini mount etme:

# ❌ TEHLİKELİ
docker run -v /etc:/host-etc myapp              # TÜM host config'leri!
docker run -v /:/host-root myapp                # TÜM host dosya sistemi!
docker run -v $HOME/.ssh:/root/.ssh myapp       # SSH anahtarların!
docker run -v /var/run/docker.sock:/var/run/docker.sock myapp  # Docker kontrolü!

Özellikle Docker socket'ı mount etmek, container'a host'taki Docker'ı kontrol etme yetkisi verir. Saldırgan bu container'a girerse tüm sistemi ele geçirebilir.

Her zaman yap — konfigürasyonları read-only mount et:

docker run -v $(pwd)/config:/etc/myapp:ro myapp
docker run -v $(pwd)/certs:/etc/ssl/certs:ro myapp

SELinux Sistemlerinde

RHEL, Fedora, CentOS gibi SELinux aktif sistemlerde bind mount'a ek label gerekir:

# :z — shared label (birden fazla container kullanabilir)
docker run -v $(pwd)/data:/data:z myapp

# :Z — private label (sadece bu container)
docker run -v $(pwd)/data:/data:Z myapp

SELinux label'sız mount → "Permission denied" hatası alırsın.


Docker Compose'da Bind Mount

Geliştirme ortamı genelde Docker Compose ile yönetilir:

# docker-compose.dev.yml
services:
  api:
    build:
      context: ./api
      dockerfile: Dockerfile.dev
    volumes:
      - ./api/src:/app/src
      - ./api/package.json:/app/package.json
      - /app/node_modules
      - /app/dist
    ports:
      - "3000:3000"
      - "9229:9229"     # Debug port
    environment:
      - NODE_ENV=development
      - CHOKIDAR_USEPOLLING=true
    command: npm run dev

  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data                    # Named volume
      - ./database/init:/docker-entrypoint-initdb.d:ro     # Bind mount, read-only
    ports:
      - "5432:5432"

volumes:
  pgdata:

Dikkat: Compose'da relative path (./ ile başlayan) otomatik olarak bind mount olarak yorumlanır. İsimsiz bir yol (pgdata:...) ise named volume.

Önemli bir not: Production'da bind mount kullanma. Geliştirmede bind mount, production'da named volume veya image katmanı — bu ayrımı kafana kazı.


Yaygın Hatalar

Hata 1 — "mount path must be absolute":

docker run -v ./src:/app/src myapp
# Error: mount path must be absolute

docker run'da relative path çalışmaz. $(pwd) kullan:

docker run -v $(pwd)/src:/app/src myapp

Not: Docker Compose'da `./ ile relative path çalışır — Compose otomatik çevirir.

Hata 2 — Boş dizin görme:

Container başladı ama mount edilen dizin boş görünüyor. Muhtemelen yolu yanlış yazdın — Docker sessizce boş dizin oluşturur, hata vermez. --mount sözdizimi kullanırsan kaynak yoksa hata verir:

docker run --mount type=bind,source=$(pwd)/nonexistent,target=/app myapp
# Error: bind source path does not exist

Hata 3 — node_modules uyumsuzluğu:

macOS'ta geliştiriyorsun, container Linux. node_modules içindeki native modüller (bcrypt, sharp) OS'e özel compile edilir. Çözüm: node_modules'ü izole et.

Hata 4 — Windows'ta satır sonu sorunu:

Windows CRLF (\r\n), Linux LF (\n) kullanır. Shell script'ler container'da çalışmaz: "/bin/sh^M: not found". Çözüm: .gitattributes dosyasında * text=auto eol=lf ayarla.


Bu Derste Ne Öğrendik?

  • Bind mount, host dizinini doğrudan container'a bağlar — iki yönlü, anlık senkronizasyon.

  • Geliştirme ortamı için ideal: kod değiştir → anında container'da yansısın (hot reload).

  • Konfigürasyon dosyaları için ideal: her zaman :ro (read-only) mount et.

  • macOS/Windows'ta performans sorunu var — VirtioFS, selective sync veya anonymous volume ile çöz.

  • node_modules gibi OS-specific dizinleri anonymous volume ile izole et.

  • Production'da bind mount kullanma — named volume veya image katmanı tercih et.

  • Host'un hassas dizinlerini asla mount etme.

Bir sonraki derste volume backup ve migration stratejilerini öğreneceğiz — otomatik yedekleme, veritabanına özel backup yöntemleri ve disaster recovery.