Konteyner tabanlı bir uygulamayı canlıya aldıktan sonra ilk fark edilen tuhaflık genelde şudur: sunucuda date yazdığında saat 14:30 görünürken, uygulamanın logunda aynı olay 11:30 olarak durur. Sipariş kayıtları üç saat geriye düşer, gece yarısı çalışacak zamanlanmış görev sabaha karşı üçte tetiklenir, raporlarda "dünkü" satışlar bugüne kayar. Docker konteynerde saat dilimi ayarı yapılmadığında yaşanan şey tam olarak budur: konteyner içi saat UTC'dir, sunucununki Europe/Istanbul'dur ve aradaki üç saatlik fark her yere sızar.
İkinci klasik sorun da karakter kodlamasıdır. Konsola bastığın Türkçe metinlerde ğ ve ş yerine soru işareti görürsün, sort komutu Türkçe alfabeye göre sıralamaz, strtoupper fonksiyonu i harfini İ yerine I yapar. Bu rehberde her iki sorunun da kökenini, kalıcı çözümünü ve hangi yaklaşımın hangi durumda tercih edilmesi gerektiğini anlatacağım. Dockerfile yazım disiplinine yabancıysan Dockerfile en iyi pratikler yazısı burada anlatılanların çoğu için iyi bir zemin sağlar.
Konteyner Neden UTC'de Çalışıyor#
Docker imajları taşınabilir olmak için tasarlanır; İstanbul'da da Frankfurt'ta da aynı davranmaları beklenir. Bu yüzden neredeyse bütün resmî temel imajlar (Debian, Ubuntu, Alpine, AlmaLinux) saat dilimi bilgisi olmadan, yani UTC varsayımıyla gelir. Konteyner çekirdeği paylaşsa da dosya sistemi kendine aittir ve saat dilimini belirleyen şey çekirdek değil, /etc/localtime dosyasıdır. Sunucunda o dosya Europe/Istanbul'a bağlıyken konteyner içinde ya hiç yoktur ya da UTC'yi gösterir.
Farkı gözünle görmek için iki komut yeterli:
# Sunucunun saati
date
# Mon Aug 25 14:30:12 +03 2026
# Aynı anda konteyner içindeki saat
docker run --rm debian:12 date
# Mon Aug 25 11:30:14 UTC 2026
# Sunucunun tanımlı saat dilimi
timedatectl show --property=Timezone --value
# Europe/Istanbul
Burada altını çizmek istediğim bir şey var: çekirdek saati her ikisinde de aynıdır. Konteyner yanlış "an"da değil, doğru anı yanlış etikette gösteriyor. Unix zaman damgası (epoch) her iki tarafta da birebir aynıdır. Bu ayrım önemlidir, çünkü sorun bir senkronizasyon sorunu değil bir gösterim sorunudur ve NTP kurmakla çözülmez. Türkiye 2016'dan beri yaz saati uygulamadığı için Europe/Istanbul kalıcı olarak UTC+03'tür; yine de sabit +03:00 yerine bölge adını kullanmak her zaman daha doğrudur, çünkü kural değişirse tzdata güncellemesiyle otomatik düzelir.
Saat Dilimini Ayarlamanın Üç Yöntemi#
Uygulamada üç yol var ve üçü de farklı durumlarda doğru.
1. TZ ortam değişkeni (en temiz yöntem). glibc tabanlı bir imajda TZ değişkeni tanımlıysa ve tzdata paketi kuruluysa saat anında doğru gösterilir:
docker run --rm -e TZ=Europe/Istanbul debian:12 date
# Mon Aug 25 14:30:12 +03 2026
Ama tzdata yoksa bu değişken sessizce yok sayılır ve saat UTC kalır. Alpine tabanlı ince imajlarda tzdata varsayılan olarak bulunmaz; en sık yaşanan "TZ verdim ama çalışmadı" durumunun sebebi budur.
2. İmaja tzdata kurup /etc/localtime bağlamak (üretim için önerilen). Saat dilimini imajın kendi içine gömersin; nerede çalışırsa çalışsın doğru davranır:
FROM debian:12-slim
ENV TZ=Europe/Istanbul \
DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends tzdata \
&& ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
&& echo $TZ > /etc/timezone \
&& rm -rf /var/lib/apt/lists/*
Alpine tarafında karşılığı şudur:
FROM alpine:3.20
ENV TZ=Europe/Istanbul
RUN apk add --no-cache tzdata \
&& cp /usr/share/zoneinfo/$TZ /etc/localtime \
&& echo $TZ > /etc/timezone
# Not: yalnızca tek bölge gerekiyorsa tzdata paketini
# kopyaladıktan sonra kaldırıp imajı ~3 MB küçültebilirsin:
# && apk del tzdata
3. Sunucunun saat dosyalarını salt okunur bağlamak (hızlı çözüm). Değiştiremediğin bir üçüncü parti imajda işe yarar:
docker run -d --name app \
-v /etc/localtime:/etc/localtime:ro \
-v /etc/timezone:/etc/timezone:ro \
ucuncuparti/uygulama:2.3
Bu yöntem pratiktir ama iki tuzağı vardır. Birincisi, konteyner artık çalıştığı sunucuya bağımlı hâle gelir; başka bir sunucuya taşındığında davranışı değişir. İkincisi, Alpine gibi musl tabanlı imajlarda /etc/timezone dosyası beklenmeyebilir ve bazı uygulamalar bunu yok sayar. Üç yöntemi karşılaştıralım:
| Yöntem | Taşınabilirlik | İmaj boyutu | Ne zaman kullan |
|---|---|---|---|
Yalnızca TZ değişkeni | Yüksek | Değişmez | Temel imajda tzdata zaten varsa |
tzdata kurup imaja gömmek | Yüksek | +2-3 MB | Kendi yazdığın üretim imajları |
| Host dosyalarını bağlamak | Düşük | Değişmez | Değiştiremediğin hazır imajlar |
Locale, UTF-8 ve Türkçe Karakter Sorunları#
Saat dilimini çözdükten sonra sıra kodlamaya gelir. Çoğu ince temel imaj yalnızca POSIX (yani C) locale ile gelir; bu da LANG tanımsızken karakter kümesinin ASCII varsayılması demektir. Sonuç: UTF-8 Türkçe metinler terminalde bozuk görünür, bazı diller sort çıktısını yanlış sıralar.
Debian/Ubuntu tarafında tam bir Türkçe locale üretmek istersen:
FROM debian:12-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends locales \
&& sed -i 's/# tr_TR.UTF-8 UTF-8/tr_TR.UTF-8 UTF-8/' /etc/locale.gen \
&& locale-gen \
&& rm -rf /var/lib/apt/lists/*
ENV LANG=tr_TR.UTF-8 \
LANGUAGE=tr_TR:tr \
LC_ALL=tr_TR.UTF-8
Alpine musl kütüphanesi kullandığı için tam locale desteği sunmaz. Orada yapılacak en sağlıklı şey, dil bazlı bir locale zorlamak yerine yalnızca kodlamayı UTF-8'e sabitlemektir:
FROM alpine:3.20
ENV LANG=C.UTF-8
Şimdi çok önemli bir uyarı: tr_TR.UTF-8 locale'ini uygulama genelinde açmak bazen sorunu çözmek yerine yenisini yaratır. Türkçe alfabede noktasız ı ve noktalı İ bulunduğu için, tr_TR locale altında i harfinin büyüğü I değil İ olur. Bu, yazılım dünyasında meşhur bir hata kaynağıdır: "ID".toLowerCase() ifadesi ıd üretir, veritabanı sütun adları eşleşmez, INDEX kelimesi ındex hâline gelir ve SQL sorgusu bozulur.
Pratik kural şudur: veri ve protokol katmanında C.UTF-8 kullan, dile duyarlı işlemleri (sıralama, büyük-küçük harf dönüşümü, tarih biçimi) uygulama kodunda açıkça Türkçe kültürüyle yap. Yani PHP'de mb_strtoupper($s, 'UTF-8'), .NET'te CultureInfo("tr-TR"), Java'da Locale.forLanguageTag("tr-TR") gibi. Böylece hem UTF-8 güvenliğini korursun hem de toUpperCase sürprizlerinden kaçınırsın.
Kurulumun sonucunu doğrulamak için:
docker run --rm -e TZ=Europe/Istanbul firmaniz/app:1.0 sh -c 'date; locale; echo "ığüşöçİĞÜŞÖÇ"'
# Mon Aug 25 14:30:12 +03 2026
# LANG=C.UTF-8
# ...
# ığüşöçİĞÜŞÖÇ
Uygulama Katmanı Kendi Saatini Ayrıca Tutar#
En sinsi kısım burasıdır: işletim sistemi saatini düzelttiğin hâlde uygulama hâlâ UTC gösteriyorsa, sebep uygulamanın kendi saat dilimi ayarına sahip olmasıdır. Sık kullanılan çalışma zamanlarının davranışı şöyle:
| Ortam | TZ değişkenini dinler mi | Ek ayar |
|---|---|---|
| Node.js | Evet | Ek ayar gerekmez |
| Python | Evet | zoneinfo ile açık dönüşüm önerilir |
| PHP | Hayır | php.ini içinde date.timezone |
| Java | Kısmen | -Duser.timezone=Europe/Istanbul |
| MySQL / MariaDB | Hayır | default-time-zone ayarı |
| PostgreSQL | Evet | timezone parametresi de ayarlanabilir |
PHP tarafında imaja küçük bir ini dosyası koymak en temiz yöntemdir:
; /usr/local/etc/php/conf.d/zz-timezone.ini
date.timezone = Europe/Istanbul
MySQL için Compose içinde komut satırı parametresi vermek yeterlidir:
services:
db:
image: mysql:8
command: --default-time-zone=+03:00 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
environment:
TZ: Europe/Istanbul
MYSQL_ROOT_PASSWORD: ornek-guclu-parola
Doğrulama basittir: veritabanına bağlanıp SELECT NOW(), @@global.time_zone; çalıştırdığında dönen değer sunucununkiyle uyuşmalıdır. Uyuşmuyorsa hangi katmanın hâlâ UTC'de olduğunu bulana kadar yukarıdan aşağı ilerle: önce date, sonra çalışma zamanı, en son veritabanı.
Burada bir mimari not düşmek istiyorum: birçok ekip için en sağlıklı yaklaşım konteynerleri UTC'de bırakmak, tarihleri veritabanında UTC saklamak ve saat dilimini yalnızca kullanıcıya gösterirken uygulamaktır. Böylece sunucu taşıdığında, yeni bir bölgeye açıldığında veya bir ülke yaz saati kuralını değiştirdiğinde verinin kendisi bozulmaz. Saat dilimini konteynere gömmek, çoğunlukla logları ve zamanlanmış görevleri insan gözüyle okumayı kolaylaştırmak içindir; veri modelinin temeli olmamalıdır.
Compose ve Çok Servisli Kurulumlarda Merkezî Ayar#
Onlarca servisin varsa her birine ayrı ayrı TZ yazmak yerine YAML çapa (anchor) yapısını kullanabilirsin:
x-ortak-ortam: &ortak-ortam
TZ: Europe/Istanbul
LANG: C.UTF-8
services:
web:
image: firmaniz/web:2.0
environment:
<<: *ortak-ortam
worker:
image: firmaniz/worker:2.0
environment:
<<: *ortak-ortam
QUEUE_NAME: siparis
cron:
image: firmaniz/cron:2.0
environment:
<<: *ortak-ortam
Zamanlanmış görevlerin doğru saatte çalışması özellikle kritiktir: konteyner UTC'deyse 0 2 * * * satırı Türkiye saatiyle 05:00'te tetiklenir. Compose dosyası yapısına daha yakından bakmak istersen Docker Compose kullanımı yazısı iyi bir başlangıç; cron ifadelerinin mantığını tazelemek istersen Linux cron görevleri yazısına bakabilirsin.
Log tarafında da aynı fark kendini gösterir: Docker'ın log dosyasına yazdığı zaman damgası her zaman UTC'dir ve konteynerin TZ ayarından etkilenmez. docker logs -t çıktısındaki saat ile uygulamanın kendi bastığı saat farklı görünüyorsa panik yapma, bu beklenen davranıştır. Ayrıntısı Docker log driver yapılandırması yazısında.
Sık Yapılan Hatalar ve Tuzaklar#
- Alpine imajında
TZveriptzdatakurmamak. Değişken sessizce yok sayılır, hata mesajı çıkmaz, saat UTC kalır. Her zamanapk add --no-cache tzdataile birlikte kullan. /etc/localtimebağlarken:royazmamak. Konteyner içindeki bir süreç sunucunun saat dosyasını değiştirebilir hâle gelir; salt okunur bağlamak zorunlu bir alışkanlık olmalı.LC_ALL=tr_TR.UTF-8ayarını kör noktada bırakmak. Kabuk betiklerinde[ "$a" = "I" ]gibi karşılaştırmalar veawk/sortdavranışı sessizce değişir; üretimdeC.UTF-8çok daha öngörülebilirdir.- Veritabanını konteynerle aynı anda ama farklı yöntemle ayarlamak. Uygulama
Europe/Istanbul, MySQLSYSTEM(yani UTC) kalırsaNOW()ile PHPdate()arasında üç saat fark oluşur ve bu fark yalnızca bazı kayıtlarda görünür. - Sunucu saatini
hwclockile elle ileri almak. Konteynerlerin hepsi çekirdek saatini paylaştığı için bu, sorunu çözmez, tüm zaman damgalarını bozar. Sunucu saati her zaman NTP ile UTC'ye senkron kalmalı; ayarlanacak olan yalnızca gösterim bölgesidir. - Yaz saati varsayımı yapmak.
+03:00sabitini koda gömersen, kural bir gün değiştiğinde her yeri tek tek düzeltmen gerekir. Bölge adı (Europe/Istanbul) kullanmak bu riskitzdatagüncellemesine devreder.
Sıkça Sorulan Sorular#
Docker konteynerde saat dilimi nasıl değiştirilir#
En hızlı yol konteyneri -e TZ=Europe/Istanbul ile çalıştırmaktır, ancak bunun işe yaraması için imajda tzdata paketinin kurulu olması gerekir. Kalıcı çözüm için Dockerfile içinde tzdata kurup /usr/share/zoneinfo/Europe/Istanbul dosyasını /etc/localtime konumuna bağlamalısın. Değiştiremediğin hazır bir imajda ise sunucunun /etc/localtime dosyasını salt okunur bağlayabilirsin.
TZ değişkenini verdim ama saat hâlâ UTC, neden#
Büyük olasılıkla imajda saat dilimi veritabanı yok. Alpine ve birçok ince slim imaj tzdata paketini içermez ve bu paket olmadan TZ değişkeni hiçbir işe yaramaz, üstelik uyarı da vermez. İkinci ihtimal, uygulamanın kendi saat dilimi ayarına sahip olmasıdır: PHP date.timezone, Java user.timezone ve MySQL time_zone ayarları TZ değişkenini dinlemez.
Konteyner saatini NTP ile senkronize etmem gerekir mi#
Hayır. Konteynerler çekirdeğin saatini paylaşır, yani ayrı bir saat tutmazlar. Sunucuda chrony veya systemd-timesyncd çalışıyorsa bütün konteynerler otomatik olarak doğru andadır. Konteyner içine NTP istemcisi kurmak hem gereksizdir hem de ek yetki gerektirdiği için güvenlik açısından istenmez.
Türkçe karakterler neden soru işareti olarak görünüyor#
Genellikle locale tanımsız olduğu için terminal ve uygulama ASCII varsayar. Çözüm, imaj içinde en azından LANG=C.UTF-8 tanımlamaktır; bu kodlamayı UTF-8'e sabitler ve dile özgü kuralları devreye sokmadan sorunu çözer. Veritabanı tarafında da karakter kümesinin utf8mb4 ve karşılaştırmanın utf8mb4_unicode_ci olduğundan emin ol.
Konteynerleri UTC'de mi bırakmalıyım yoksa yerel saate mi almalıyım#
İkisi farklı katmanlarda doğru olabilir. Veriyi (veritabanı sütunları, API yanıtları, olay kayıtları) UTC'de saklamak uzun vadede en güvenli yöntemdir; bölge değişimlerine ve sunucu taşımalarına dayanıklıdır. Buna karşılık log çıktıları ve zamanlanmış görevler için yerel saat okunabilirliği ciddi biçimde artırır. Yaygın pratik, veriyi UTC tutup gösterimi yerel saatte yapmaktır.
Zamanlanmış görevim yanlış saatte çalışıyor, nereye bakmalıyım#
Önce konteyner içinde date çalıştırıp saatin ne gösterdiğini kontrol et. UTC ise cron ifadelerin üç saat kaymış demektir. Konteynere TZ ve tzdata ekledikten sonra cron servisini yeniden başlatman gerekir, çünkü birçok cron uygulaması saat dilimini başlangıçta okur. Görev hiç çalışmıyorsa sorun saat dilimi değil olabilir; cron çalışmıyor yazısındaki tanı adımlarını izle.
Kapanış#
Saat dilimi ve locale, kurulum sırasında iki satırla çözülen ama atlanınca aylarca yanlış raporlar üreten konulardan. Aklında tutman gereken dört şey şunlar: TZ değişkeni tek başına yeterli değildir, tzdata kurulu olmalıdır; uygulama çalışma zamanı ve veritabanı kendi saat ayarını ayrıca tutar; tr_TR locale'i büyük-küçük harf dönüşümünde sürpriz yapar, kodlama için C.UTF-8 daha güvenlidir; ve veriyi UTC saklayıp gösterimi yerelleştirmek uzun vadede en dayanıklı yaklaşımdır.
Bu ayarları her sunucuda tek tek yapmakla uğraşmak istemiyorsan Clou.TR tarafında hazır bir zemin bulabilirsin. Tam root erişimli VDS ve sanal sunucu paketlerimizde kendi imaj standartlarını kurabilir, kurulum ve bakımı bize devretmek istersen sunucu yönetimi hizmetimize göz atabilir, konteyner altyapısına geçişi planlıyorsan site taşıma ekibimizden destek alabilirsin.