Docker, uygulamalarınızı konteynırlar içerisine alarak, her koşulda istenildiği gibi çalışmalarını sağlayan bir araçtır. Peki Docker bunu nasıl sağlar? Docker ile nasıl cross-platform bir uygulama geliştirebiliriz? Benimle kal, mevzu oldukça basit.

Giriş

Öncelikle https://docs.docker.com/get-docker/ sayfası üzerinden Docker’ı edinmemiz gerek. Bu işlem tüm işletim sistemlerinde farklı olduğu için burada değinemiyorum fakat bize Docker Engine veya Docker Desktop gerekli (bu yazıda Docker Compose’dan bahsetmeyeceğim, bu yüzden kendisine ihtiyacımız yok). Kurulumu tamamladıktan sonra sıradaki aşamaya geçebiliriz.

Not: docker --version komutu ile kurulumun başarılı olduğundan emin olabilirsin.

Başlangıç

Bu blog yazısı için beraber basit bir Node.js projesi oluşturacağız, fakat yazıyı takip edebilmek için Node.js veya JavaScript bilmen gerekmiyor.

Projemiz için birkaç dizin oluşturarak işe koyulalım: mkdir ~/projects/dockertut && cd ~/projects/dockertut

Ardından şu ufak Node.js kodunu app.js isminde bir dosya olarak kaydedelim. Bu bizim basit uygulamamız olsun:

const http = require('http');
const fs = require('fs');
const path = require('path');

const hostname = '127.0.0.1';
const port = 80;
const INDEX_FILE = 'data/index.txt';

fs.mkdirSync(path.dirname(INDEX_FILE), (err) => {});

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');

  // INDEX_FILE'dan indexi al
  let index = fs.existsSync(INDEX_FILE) ? fs.readFileSync(INDEX_FILE) : 0;

  // Mevcut indexi kullanıcıya göster
  res.end(`Index: ${index}`);

  // Indexe bir ekleyip INDEX_FILE'a kaydet
  fs.writeFile(INDEX_FILE, ++index, (_)=>{});
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

Docker konteynırları; imajlar (image), ağlar (network) ve volumelardan (saklama alanları) oluşurlar. İmajlar bir konteynırın hangi içerikle üretileceğini belirlememizi sağlar. Örneğin biz bu örnekte kendi imajımızı oluştururken Node.js’in Alpine Linux ile birlikte gelen Docker imajını kullanacağız. Bu imaj ile birlikte hazır bir Alpine Linux kurulumu ve beraberinde gelen Node.js paketi kullanımımıza hazır olacak.

Dockerfile

İmajlar, Dockerfile isimli bir dosyadan aldıkları talimatlar doğrultusunda oluşturulurlar. touch Dockerfile diyerek bir tane oluşturduktan sonra, kendisini metin editörümüz ile düzenlemeye başlayalım.

Bizim imajımız başka bir imajın bir nevi uzantısı olacak. Dockerfile’ımızın ilk satırında bu imajı belirtmemiz gerekiyor ki, sonraki adımlara geçtiğimizde elimizde üzerinde çalışabileceğimiz bir sistem olsun. Bunun için de FROM direktifini kullanıyoruz:

FROM node:13-alpine

Sırada uygulamamızı konteynırın içerisine taşımak var. Kendisinin ebediyen barınacağı dizine gittikten sonra, app.js dosyasını yerel makineden konteynıra aktaralım:

FROM node:13-alpine
WORKDIR /appdir
COPY app.js ./

WORKDIR direktifi ile mevcut çalışma dizinimizi değiştiriyoruz. /appdir adında bir klasör var olmasa dahi WORKDIR bizim için bu klasörü oluşturacak. Devamında COPY ile dosyayı yerelden alıp konteynırdaki çalışma dizinimize yerleştiriyoruz.

Uygulamamız 80 portu üzerinde çalışacak fakat bu port şu anda konteynırın dışından erişilemez durumda. Portu dışarıdan erişilebilir hale getirmek için EXPOSE direktifini ekleyelim:

FROM node:13-alpine
WORKDIR /appdir
COPY app.js ./
EXPOSE 80

Son olarak uygulamamızın konteynır içerisinde hangi komut ile çalışacağını ENTRYPOINT direktifi ile tanımlayalım:

FROM node:13-alpine
WORKDIR /appdir
COPY app.js ./
EXPOSE 80
ENTRYPOINT [ "node", "app.js" ]

ENTRYPOINT direktifi, imajımız çalıştırıldığında hangi komutun gerçekleştirileceğini belirtmemizi sağlar ve genellikle Dockerfile’ın sonunda bulunur. Eğer ki imajın hazırlığı sırasında komut girmek istersek RUN direktifini kullanabiliriz.

Not: Aynı zamanda ENTRYPOINT direktifinin bir alternatifi de olan CMD direktifi, ENTRYPOINT direktifine argümanlar belirtmek için kullanılabilir. Örneğin uygulamamız opsiyonlar kabul ediyor olsaydı şunu yapabilirdik:

ENTRYPOINT [ "node", "app.js" ]
CMD [ "--option1=true", "--option2=false" ]

# veya (ENTRYPOINT yerine CMD kullanabiliriz)
ENTRYPOINT [ "node", "app.js", "--option1=true", "--option2=false" ]

# veya (tavsiye edilmez) (yine CMD yerine ENTRYPOINT kullanabiliriz)
CMD node app.js --option1=true --option2=false

Build && Run!

İmajımız builde hazır. docker build -t blackness/docker101 . komutu ile build edebilir (sondaki nokta Dockerfile’ın bulunduğu dizin) ve docker run -dp 8080:80 blackness/docker101 komutu ile de çalıştırabiliriz.

docker run komutuna girdiğimiz -d parametresi detach anlamına gelir ve konteynırı arkaplanda çalıştırmamızı sağlar. -p yerel:konteynır parametresi ise konteynır ve yerel makine arasındaki 2 portu bağlamamız, konteynırın portunu expose etmemiz için gerekli. Tüm parametreleri görmek istersen docker run --help veya man docker-run komutlarından birini kullanabilirsin.

Bu aşamadan sonra tarayıcımız yardımı ile veya curl komutuyla “localhost:8080” adresine bağlı uygulamamıza erişebilmemiz gerek. Ama erişemiyoruz? Peki neden? Çünkü uygulamamız konteynır içerisindeki 127.0.0.1 adresini (localhost) dinliyor fakat bu adres konteynır dışarısına yönlendirilmiyor.

Sorunu çözmek için 127.0.0.1 değerini 0.0.0.0 ile değiştirerek Docker’ın konteynırımıza tanımladığı arayüz de dahil tüm arayüzleri tek seferde dinleyebiliriz.

Pekala gerekli değişikliği yaptık ve imajımızı tekrar çalıştırmak istiyoruz. Bunun için öncelikle docker ps komutuyla çalışan konteynırımızın ID’sini alalım. Ardından docker kill <Konteynır ID> komutuyla kendisini ortadan kaldıralım. Yaptığımız değişikliğin geçerli olabilmesi için imajımızı aynı komutlar ile yeniden build edelim ve çalıştıralım.

Artık uygulamamız çalışıyor ve sayfayı her yenilediğimizde index değerinin arttığını görebiliyoruz.

Not: İmajı yeniden build etmemizin sebebi “app.js” dosyasına yaptığımız değişikliklerin imaj içerisindeki “app.js” dosyasına yansımıyor olmasıdır. Dosyayı tekrardan imaj içerisine kopyalayarak güncellememiz gerekiyor.

Konteynırlarda Kalıcı Veri ve Volumelar

Eğer ki önceki bölümde oluşturduğumuz imajımızı yeniden başlatırsak index değerinin sıfırlandığını görebiliriz. Bunun sebebi konteynırı yokettiğimizde içerisindeki verilerin de Docker’ın geçici saklama alanından siliniyor olmasıdır. Bu durumu düzeltebilmek için “index.txt” dosyasını bir volume içerisinde kalıcı olarak saklamamız gerekiyor.

VOLUME direktifini kullanarak indexi barındıran data klasörünü bir volume’a çevirelim:

FROM node:13-alpine
WORKDIR /appdir
COPY app.js ./

VOLUME [ "/appdir/data" ]

EXPOSE 80
ENTRYPOINT [ "node", "app.js" ]

VOLUME direktifimizin bir işe yaraması için volume’u bir yere mount etmemiz gerekiyor. Bunun için de imajımızı başlatmak için kullandığımız komutu şu şekilde değiştirmeliyiz:

docker run -dp 8080:80 -v myvol:/appdir/data blackness/docker101

-v / --volume opsiyonu iki argümandan oluşmakta. Docker ilk argümanda belirttiğimiz isim ile (myvol) aynı isimde bir Docker volume’u oluşturduktan sonra bu volume’u ikinci argümanda ve VOLUME direktifinde belirtilen konuma mount ediyor. VOLUME argümanından önce volume konumunda başka bir dosya oluşturmuşsak bu dosya silinmiyor fakat üzerine mount işlemi gerçekleştirdiğimiz için erişilemez bir hale geliyor. Bundan dolayı VOLUME direktifini önce belirtip üzerinde gerçekleşecek işlemleri sonrasında yapmalıyız.

SON

Bu belki biraz uzun blog yazısında beraber küçük bir Docker imajı oluşturduk. Umarım okudukların sana birşeyler katmış ve Docker kullanmaya teşvik etmiştir. Bir sonraki yazımda belki Docker Compose’u anlatabilirim ama anlatmaya da bilirim. Bilemiyorum, hayatta hiçbir şey kesin değil.