SSD Nodes Learn
Rehberler Matt ConnorYazan Matt Connor · Güncellendi 2026-07-24

Ubuntu 24.04 Docker Compose Kurulum Rehberi

Ubuntu 24.04 üzerinde Docker Engine ve Compose v2 kurulumu, ufw port çakışmaları ve volume yedekleme yöntemleri hakkında teknik detaylar sunulmaktadır.

Ne inşa ediyorsunuz

Docker Compose, bu sitedeki hemen hemen her şeyin temel yapısını oluşturur. Nextcloud, Vaultwarden, n8n, Immich, Rocket.Chat — bu kılavuzların her biri "bu compose dosyasını yazın" talimatıyla başlar; bu sayfa ise söz konusu dosyanın gerçekte ne anlama geldiğini açıklar. Ubuntu 24.04 üzerinde Docker'ın kendi apt deposundan Docker Engine ve Compose v2 eklentisini kuracaksınız. Ardından, büyük uygulamaların kullandığı tüm kalıpları içeren iki servisli bir yapı kuracaksınız: küçük bir RSS okuyucu olan Miniflux ve PostgreSQL. Bu ikili; sabitlenmiş imajlar, healthcheck içeren bir veritabanı, isimlendirilmiş bir volume, bir .env dosyasındaki secrets ve yalnızca localhost'a açılmış bir port gibi tüm yapıları içerir.

Kurulum beş dakika sürer. Kılavuzun geri kalanı, ileride sorun yaratabilecek konuları kapsar: docker grubunun root ile aynı yetkilere sahip olması, açılan portların ufw kurallarını doğrudan geçmesi ve docker compose down üzerindeki veritabanını onay istemeden silen o tek flag.

Önkoşullar: Yeni bir Ubuntu 24.04 KVM VPS, sudo yetkisine sahip bir kullanıcı ve en az 1 GB RAM. Mevcut bir Docker kurulumu olması sorun teşkil etmez; ilk bölüm nelerin kaldırılması gerektiğini açıklar.

Ubuntu yerine Docker'ın reposundan yükleyin

İlk komuttan önce iki hatalı yöntemi elemek gerekir. Ubuntu'nun kendi docker.io paketi çalışır, ancak Docker sürümlerinin gerisinden gelir ve diğer bileşenlerin beklediği eklenti yapısına sahip değildir. Ayrıca, tire içeren bağımsız docker-compose binary dosyası Compose v1 sürümüdür: Python tabanlıdır, 2023'ten beri kullanım ömrini tamamlamıştır ve eski eğitimlerin hata vermesinin sebebi budur. Güncel Compose, boşluk içeren docker compose sürümüdür; bir CLI eklentisidir ve motor ile aynı depodan yüklenir.

Eğer sistemde bunlardan herhangi biri yüklüyse, her şeyin tek bir depodan geldiğinden emin olmak için önce temizleyin — Ubuntu'nun eklenti paketlemesi olan docker-compose-v2 dahil:

sudo apt remove -y docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc

Package 'docker.io' is not installed, so not removed yeni bir VPS üzerindeki normal çıktıdır. Ardından Docker reposunu ekleyin ve yükleyin:

sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Üç katmanı da doğrulayın:

docker --version
docker compose version
sudo docker run --rm hello-world

İlk ikisi sürüm bilgilerini yazdırır — Docker Compose version v2.x.x eklentinin mevcut olduğunu ve artık kullanılmayan v1 binary dosyası olmadığını onaylar. hello-world komutu Hello from Docker! ile sonuçlanmalıdır. Paket, servisi açılışta etkinleştirir; systemctl is-enabled docker komutu enabled çıktısını verir.

docker grubu root yetkisine sahiptir — kararı dikkatli verin

Şu anda her docker komutu sudo yetkisi gerektirir; çünkü /var/run/docker.sock adresindeki daemon soketi root kullanıcısına ve docker grubuna aittir. Bu gruba üye olunmadığında, en sık karşılaşılan Docker hatası alınır:

permission denied while trying to connect to the Docker daemon socket at
unix:///var/run/docker.sock

Standart çözüm:

sudo usermod -aG docker $USER

Grup üyeliği oturum açıldığında geçerli olur, bu nedenle hata mevcut kabukta (shell) devam eder. Bu oturum için newgrp docker komutunu çalıştırın veya oturumu kapatıp tekrar açın; id komutu, gruplarınız arasında docker öğesini listelemelidir.

Dürüstçe belirtmek gerekirse: docker grubuna üye olmak, host üzerinde root yetkisine sahip olmak demektir. Bu yetki "root benzeri" veya "yükseltilmiş" değildir; doğrudan root yetkisidir. Bu gruptaki herhangi bir kullanıcı, şifre sormadan docker run --rm -it -v /:/host alpine chroot /host komutunu çalıştırabilir ve tüm dosya sisteminin sahibi olabilir. Bu grup, izolasyon için değil, kullanım kolaylığı için mevcuttur.

Docker'ın rootless modu gerçek alternatiftir; daemon doğrudan yetkisiz kullanıcı olarak çalışır. Bu yöntemin maliyetleri şunlardır: 1024 altındaki portlar için ek yapılandırma gerekir, ağ trafiği ölçülebilir bir yük getiren bir userspace shim üzerinden çalışır ve bazı imajlar gerçek root yetkisi olmadan hatalı çalışır. Tek bir yöneticinin bulunduğu ve tek giriş yönteminin sudo yetkisine sahip olduğu bir VPS üzerinde, grup üyeliği pratikte bir şeyi değiştirmez; buradaki tüm kılavuzlar bu durumu varsayar — ancak bu yetkiyi sudo'dan daha az bir yetkiymiş gibi asla dağıtmayın.

Anatomy of a compose file

Her stack için ayrı bir dizin oluşturun — dizin adı proje adı olur; bu isim konteyner, network ve volume isimlerinin başına eklenir:

sudo mkdir -p /opt/miniflux && sudo chown $USER /opt/miniflux && cd /opt/miniflux

compose.yml oluşturun (modern isimdir; docker-compose.yml hala çalışır). Eski version: anahtarını kullanmayın; bu anahtar artık kullanılmamaktadır ve Compose bunu gördüğünde uyarı verir.

services:
  miniflux:
    image: miniflux/miniflux:2.2.9
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:8080"
    environment:
      - DATABASE_URL=postgres://miniflux:${POSTGRES_PASSWORD}@db/miniflux?sslmode=disable
      - RUN_MIGRATIONS=1
      - CREATE_ADMIN=1
      - ADMIN_USERNAME=admin
      - ADMIN_PASSWORD=${ADMIN_PASSWORD}
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      - POSTGRES_USER=miniflux
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=miniflux
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "miniflux", "-d", "miniflux"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  db-data:

Yukarıdaki her satır bir karardır. Bunları tek tek uygulayın.

Pin image versions — :latest kullanımı ve pull işlemi kontrolsüz bir yükseltmedir

postgres:latest değil, postgres:16-alpine kullanın. Bir tag sabit değildir: :latest, her pull işleminde bakımcı tarafından en son gönderilen sürüme yeniden çözümlenir. Bunu, birazdan öğreneceğiniz rutin yükseltme alışkanlığıyla — docker compose pull && docker compose up -d — birleştirirseniz, :latest ana sürüm geçişlerinin siz seçtiğiniz zaman değil, upstream yayınladığı zaman gerçekleşeceği anlamına gelir. PostgreSQL için bu varsayım değildir: 16'dan 17'ye beklenmedik bir geçiş, konteynerin uyumsuz bir veri dizini nedeniyle sürekli çökmesine (crash-looping) neden olur; çünkü Postgres ana sürüm yükseltmeleri restart ile değil, dump ve restore işlemi ile yapılır.

En azından ana sürümü sabitleyin (postgres:16-alpine, 16.x yama sürümlerini takip eder) ve uygulamaları miniflux/miniflux:2.2.9 gibi tam bir sürüme sabitleyin — dosya yazılırken projenin releases sayfasını kontrol edin ve güncel olanı kullanın. Bu durumda yükseltme, git diff içinde görülebilen, bilerek yaptığınız tek satırlık bir düzenleme haline gelir.

Publish to 127.0.0.1, because Docker walks around ufw

"127.0.0.1:8080:8080" — host adresi, host portu, konteyner portu. Çoğu eğitimde, 0.0.0.0:8080:8080 için kısaltma olan "8080:8080" yazılır: bu, kamuya açık olan dahil tüm arayüzlerde dinleme yapılması anlamına gelir.

İşte tuzak buradadır ve neredeyse herkesin başına bir kez gelir. Docker, bir portu yayınlarken, paket filtrelemeden önce hedefi konteynerin dahili IP'sine yeniden yazan bir DNAT kuralı yazar; böylece paket FORWARD yolunu izler ve ufw kurallarınızın bulunduğu INPUT noktasına asla dokunmaz. sudo ufw deny 8080 başarılı raporu verir, ufw status portun reddedildiğini gösterir ancak servis tüm internete hala yanıt verir. Firewall'unuz bozuk değildir; tasarım gereği devre dışı bırakılmaktadır. Neden Docker ufw'yi devre dışı bırakır ve konteyner trafiği gerçek anlamda nasıl filtrelenir mekanizmayı ve halka açık kalması gereken portlar için DOCKER-USER çözümünü açıklar.

Tüm sorunu ortadan kaldıran alışkanlık: Özel bir nedeniniz yoksa yayınlanan portları 127.0.0.1 adresine bağlayın (bind) ve dünyaya açılması gereken her şey için önüne bir reverse proxy koyun. Traefik reverse proxy guide sayfasının bir sonraki adımı tam olarak bunu inşa eder: 80 ve 443 portlarına sahip olan ve diğer her şeyi hostname ve TLS ile yönlendiren tek bir konteyner. (Eski bir Traefik v2 kurulumundan mı geliyorsunuz? Traefik v2 to v3 migration guide yeniden adlandırmaları ve kural değişikliklerini kapsar.)

Stack başlatıldıktan sonra bağlamayı doğrulayın: sudo ss -tlnp | grep 8080, 0.0.0.0:8080 veya *:8080 değil, 127.0.0.1:8080 göstermelidir.

Named volumes vs bind mounts

db-data:/var/lib/postgresql/data bir named volume'dur: Docker, /var/lib/docker/volumes/ altında bir dizin oluşturur, yönetir ve bunu konteyner içine bağlar. Alternatif ise, host üzerinde seçtiğiniz bir yolu eşleyen bind mount, yani ./data:/var/lib/postgresql/data'dir.

Pratikte geçerli olan ayrım: Sadece veriye dokunan konteynerler için named volumes — her şeyden önce veritabanları; çünkü Docker, volume'u imajın beklediği sahiplik ile başlatır ve dosya izinleri sorunsuz çalışır. Host üzerinden müdahale ettiğiniz dosyalar için bind mounts — bir metin düzenleyici ile düzenlediğiniz konfigürasyon dosyaları, rsync ile aktardığınız bir medya kütüphanesi veya yolunun açıkça görünmesini istediğiniz her şey. Klasik bind-mount hatası sahiplik sorunudur: konteyner UID 999 olarak çalışır, host dizininiz UID 1000 tarafından sahiplenilir ve uygulama başlangıçta loglarında permission denied hatasıyla çöker. Named volumes, verilerin Docker tarafından yönetilen bir yolda yaşaması pahasına, bu tür hataları büyük ölçüde ortadan kaldırır — aşağıda detaylandırılmıştır.

environment and .env — keep secrets out of git

${POSTGRES_PASSWORD} kabuğunuzdan (shell) okunmaz; Compose bunu compose.yml'in yanında bulunan .env adlı bir dosyadan yorumlar. Bunu oluşturun:

cat > .env <<'EOF'
POSTGRES_PASSWORD=change-me-to-something-long
ADMIN_PASSWORD=change-me-too
EOF
chmod 600 .env
echo ".env" >> .gitignore

openssl rand -hex 24 ile gerçek değerler oluşturun. Bilerek base64 değil, hex kullanın: bu şifre DATABASE_URL bağlantı dizesinin içine yerleşir; base64'ün ürettiği /, + ve = karakterleri URL ayrıştırmasını bozar — bu hata bir sözdizimi hatası olarak değil, bir kimlik doğrulama hatası olarak karşınıza çıkar ve bir akşamınızı çalar. .gitignore satırı ilk commit işleminden önce eklenmelidir: compose dosyası yayınlanmak ve versiyonlanmak için güvenlidir, ancak .env dosyası asla güvenli değildir; git geçmişine girmiş bir sır, döndürülmesi (rotate) gereken bir sırdır. Stack'i bir değişken eksik olarak başlatırsanız, Compose yüksek sesle uyarır ve boş bir dize ile devam eder — bu da Postgres şifresi için bozuk bir kurulum demektir:

WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string.

docker compose config, tamamen yorumlanmış dosyayı yazdırır — konteynerlerin gerçekte ne alacağını kontrol etmenin en hızlı yoludur; çıktısının sırlarınızı içerdiğini unutmayın.

depends_on waits for nothing — unless you add a healthcheck

Sıradan bir depends_on: [db] sadece başlatma sırasını kontrol eder: Compose önce Postgres'i, bir an sonra ise uygulamayı başlatır; o sırada Postgres henüz bağlantıları kabul etmeye saniyeler uzaktadır. Uygulama veritabanına ulaşır, başarısız olur ve yazılma şekline bağlı olarak çöker veya yeniden denemeye başlar.

Güvenilir versiyon, yukarıdaki dosyanın kullandığı versiyondur: db servisi bir healthcheck tanımlar (Postgres tam olarak bunun için pg_isready ile birlikte gelir) ve uygulama condition: service_healthy ile birlikte depends_on bildirir. Compose veritabanını başlatır, kontrolü her 10 saniyede bir sorgular ve yalnızca kontrol geçildikten sonra Miniflux'u başlatır. Eğer veritabanı hiçbir zaman sağlıklı hale gelmezse — hatalı şifre, bozuk volume — uygulama asla başlamaz ve Compose size hangi bağımlılığın başarısız olduğunu söyler:

dependency failed to start: container miniflux-db-1 is unhealthy

Bu mesaj sizi gerçek hatanın bulunduğu docker compose logs db noktasına yönlendirir.

restart: unless-stopped

Her iki servis için restart: unless-stopped, konteynerlerin bir çökme sonrası ve VPS yeniden başlatıldıktan sonra geri geleceği, ancak docker compose stop komutunu bilerek çalıştırdıysanız kapalı kalacağı anlamına gelir. Alternatif olan always, konteynerleri manuel durdurmadan sonra bile canlandırır — bu nadiren istediğiniz bir durumdur. Bir restart politikası olmazsa, saat sabah 4'teki bir kernel güncellemesi yeniden başlatması, siz fark edene kadar servislerinizi sessizce devre dışı bırakır.

Günlük komutlar

Günlük işleyiş için proje dizininden çalıştırılması gereken beş komut vardır.

docker compose up -d        # create and start; idempotent, recreates only what changed
docker compose ps           # status, ports, and health of this project's containers
docker compose logs -f miniflux   # follow one service's logs; --tail 100 for recent history
docker compose pull && docker compose up -d   # upgrade to the pinned tags
docker compose down         # stop and remove containers and the network

up -d komutunun tekrar tekrar çalıştırılması güvenlidir; dosyayı mevcut durumla karşılaştırır ve yalnızca yapılandırması veya imajı değişen servisleri etkiler. Yükseltme komutları, sabitlenmiş etiketlerin (pinned tags) işaret ettiği güncel sürümleri çeker: postgres:16-alpine altındaki yama sürümleri (patch releases) indirilir; siz düzenleyene kadar tam eşleşen etiketler için hiçbir işlem yapılmaz — amaç da budur. Yükseltmelerden sonra eski imajlar birikir; disk alanını docker image prune -f ile boşaltabilirsiniz.

Yıkıcı komut hakkında önemli uyarı: docker compose down güvenlidir; konteynerler ve ağ yapısı geçicidir, verileriniz ise volume içerisindedir. docker compose down -v, isimlendirilmiş volume yapılarını da siler. Bu, veritabanınızın onay istemeksizin ve geri alınamaz şekilde anında silinmesi demektir. -v bayrağı, deneyleri sonlandırmak için mevcuttur; gerçek veriler barındıran bir stack üzerinde bu bayrağı rm -rf gibi kullanın. /var/lib/docker/volumes/ altında bir çöp kutusu bulunmamaktadır.

Çalışan bir konteyner içinde tek seferlik bir shell açmak için: docker compose exec db psql -U miniflux sizi veritabanına bağlar, docker compose exec miniflux sh ise uygulama içinde bir shell açmanızı sağlar.

Verilerinizin gerçekte bulunduğu yer

Named volume'lar proje öneki alır; bu nedenle miniflux adlı dizindeki db-data, miniflux_db-data haline gelir:

docker volume ls
docker volume inspect miniflux_db-data

inspect çıktısı, önemli olan satırı içerir:

"Mountpoint": "/var/lib/docker/volumes/miniflux_db-data/_data"

Bu dizin veritabanıdır; root yetkisine sahiptir, host dosya sistemi üzerindedir ve down, yükseltmeler ile container yeniden oluşturma işlemlerinden etkilenmez. Yedeklemelerinizin kapsaması gereken içerik tam olarak budur.

İsimlendirilmiş bir volume yedekleme

Standart yöntem, volume'u host dizini ile yan yana read-only olarak bağlayan geçici bir container kullanmak ve tar komutuyla veriyi aktarmaktır:

docker run --rm \
  -v miniflux_db-data:/data:ro \
  -v "$PWD":/backup \
  alpine:3.22 tar czf /backup/miniflux-db-$(date +%F).tar.gz -C /data .

Herhangi bir kurulum gerektirmez, sistemde çalışan bir süreç bırakmaz. Geri yükleme işlemi işlemin tersidir — aynı mount noktaları tersine çevrilmiş boş bir volume içine tar xzf aktarılır.

Veritabanları için bir uyarı: çalışan bir Postgres veri dizininin tar ile yedeklenmesi, temiz başlatılamayacak bir yazma durumu yakalayabilir. Ya tar işlemi süresince docker compose stop uygulanmalıdır ya da — daha iyisi — yapısal olarak tutarlı olan bir logical dump alınmalıdır:

docker compose exec -T db pg_dump -U miniflux miniflux | gzip > miniflux-$(date +%F).sql.gz

-T, Compose tarafından varsayılan olarak ayrılan pseudo-terminali devre dışı bırakır — dump çıktısının bir TTY üzerinden pipe edilmesi veriyi bozabilir. Bu komutlardan birini cron'a ekleyin ve sonucu VPS'den kopyalayın; verinin bulunduğu disk üzerinde tutulan bir yedek, gerçek bir yedek değildir. Nextcloud kılavuzu, tam zamanlı bir rutin oluşturmak için tam olarak bu iki yöntemi kullanır.

Hata modları ve göreceğiniz dizeler

permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock — henüz docker grubunda değilsiniz veya gruptasınız ancak oturumunuz bu işlemden önce başlatılmış. id geçerli gruplarınızı gösterir; newgrp docker mevcut kabuğu düzeltir, oturumu kapatıp tekrar açmak tüm sorunları çözer.

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? — farklı bir sorun: daemon kapalı. sudo systemctl status docker ve sudo journalctl -u docker -n 50 nedenini belirtir. Bir VPS üzerinde yaygın neden disk doluluğudur — önce df -h /var/lib/docker kontrol edilmelidir.

Bind for 127.0.0.1:8080 failed: port is already allocated — başka bir konteyner bu host portunu zaten yayınlıyor. docker ps hangi konteyner olduğunu gösterir; genellikle haftalar önce yapılan deneysel bir docker run kaynaklı eski bir konteyner buna sebep olur. Eğer docker ps temizse, portu Docker dışı bir süreç tutuyordur: sudo ss -tlnp | grep 8080 bu süreci belirtir.

yaml: line 14: did not find expected key — belirtilen satırda veya hemen üzerinde bir girinti hatası var. Compose dosyaları YAML formatındadır: iki boşlukluk girinti kullanılmalıdır, sadece boşluk kullanılmalıdır ve herhangi bir yerde tab karakteri kullanılması hataya yol açar. docker compose config dosyayı hiçbir şeyi başlatmadan doğrular; her düzenlemeden sonra çalıştırmak iyi bir alışkanlıktır.

ufw sürprizi hiçbir hata mesajı vermez, bu da onu tehlikeli kılar: dağıtım başarılı görünür, ufw status doğru görünür, ancak dışarıdan yapılan bir port taraması veritabanınıza yine de erişebilir. Yukarıdaki portlar bölümünü tekrar okuyun, her ports: girişinde eksik bir 127.0.0.1: öneki olup olmadığını kontrol edin ve curl http://your-vps-ip:8080 ile farklı bir makineden bağlantıyı doğrulayın — beklenen yanıt "connection refused" olmalıdır.

Buradan itibaren, Traefik kılavuzu bu tekil yığını tek bir HTTPS giriş noktası arkasındaki birçok uygulamaya dönüştürür ve 2026 yılında self-hosting için değer taşıyanlar bu yapı üzerinden çalıştırmak için bir liste sunar.

VPS üzerinde bir Minecraft sunucusu gibi bir oyun sunucusu, pratik yapmak için uygun bir ilk Compose projesidir.

FAQ

Why do I get "permission denied while trying to connect to the Docker daemon socket"?

Your user is not in the docker group, or was added after the current session began — membership only applies at login. Run sudo usermod -aG docker $USER, then newgrp docker or log out and back in, and confirm with id. The group grants root-equivalent access to the host, so only add users you would give sudo.

Does docker compose down delete my data?

Plain docker compose down does not — it removes containers and the project network; named volumes survive and the next up -d reattaches them. docker compose down -v is the destructive form: it deletes the named volumes, meaning your database, with no confirmation and no undo. Never run -v on a stack with real data unless you hold a verified backup.

What is the difference between docker-compose and docker compose?

docker-compose (hyphen) is Compose v1, a standalone Python binary that reached end of life in 2023 and should not be installed on new servers. docker compose (space) is Compose v2, a Go plugin for the Docker CLI, installed as docker-compose-plugin from Docker's apt repository. Commands and YAML are almost fully compatible, so when an old tutorial says docker-compose up, type docker compose up.

Why can I reach my Docker container from the internet even though ufw blocks the port?

Because Docker publishes ports with DNAT rules in iptables' PREROUTING chain, and the rewritten packets travel the FORWARD path through Docker's own chains — they never hit the INPUT chain where ufw's rules apply. ufw deny 8080 therefore does nothing to a published container port. Fix it at the source: publish to 127.0.0.1: and expose services through a reverse proxy instead.

Should I use a named volume or a bind mount?

Named volumes for data only the container touches — databases especially, since Docker sets the ownership the image expects and permissions just work. Bind mounts for files you also handle from the host: configs you edit, media you upload, anything whose path you want obvious. If a container fails at startup with permission denied on a bind mount, host-vs-container UID mismatch is the first thing to check.