Dockerfile İçinde Latest Tag Kullanmayı Bıraktım
Aynı satır, ertesi gün başka bir digest çekiyor

Cuma 17:10'da pipeline yeşildi. Aynı commit Pazartesi sabahı test aşamasında kırıldı. Diff yoktu. Dockerfile değişmemişti. Değişen tek şey, registry'nin node:20 ve latest diye sunduğu işaretin artık başka bir manifesti göstermesiydi.
İlk refleks 'Node bozulmuş' oldu. Değil. Cuma akşamı çekilen katmanda OpenSSL ve ca-certificates bir çiftti; Pazartesi çekilen katmanda ikisi de kaymıştı. Uygulama kodu aynıydı. Temel imaj aynı isimle başka bir şey olmuştu. latest bir sürüm değil, hareket eden bir işaret.
latest bir numara değil
docker pull bir isim çözüyor. İsim, registry'de bir tag kaydı. Kayıt bugün bir digest, yarın başka digest gösterebilir. Bunu 'otomatik güvenlik yaması' diye satarlar. Pratikte build'in girdilerinden birini tarih ile değiştirir. Kaynak kontrolünde görünmez.
Aynı şey node:20 için de geçerli. Major.minor da hareket eder; sadece daha yavaş. node:20.11.1-bookworm-slim daha az oynar, yine oynayabilir. Oynamayan tek şey digest'tir: node@sha256:.... Çirkin durur. Yalan söylemez.
Multi-arch manifest listesi işi biraz daha bulanıklaştırır. Aynı tag, amd64 ve arm64 için ayrı digest taşır. Host mimarisi değişince 'aynı tag' yine başka blob'dur. Bunu unutup 'bizde çalışıyor' demek, CI runner'ın mimarisi ile laptop'un mimarisini karıştırmaktır.
Cuma imajı, Pazartesi imajı
Kırılan iş aslında test değil, derleme ortamıydı. Native bir bağımlılık, temel imajdaki header'a bakıyordu. Cuma imajında header vardı. Pazartesi imajında paket sürümü değişmiş, header kaymıştı. Uygulama testi haklı olarak patladı. Suçlu test değildi.
docker image inspect ile RepoDigests'e bakınca iki günün imajı aynı repository adı altında iki hash'ti. Bunu gördükten sonra 'yeniden dene' butonuna basmak anlamsızlaştı. Yeniden denemek, hareket eden tag'i bir kez daha çekmek demek. Bazen şans eseri eski digest gelir, bazen gelmez. Şans, pipeline değildir.
Daha kötüsü: production'daki konteyner Cuma digest'inde çalışmaya devam ediyordu. Yeni replica Pazartesi digest'ini aldı. Aynı servis, iki host, iki kullanıcı alanı. Birinde TLS el sıkışması geçti, diğerinde sertifika deposu eksik kaldı. Bunu 'aralıklı 502' diye izledik. Aralıklı değildi; filoyu ikiye bölmüştük.
Pinlediğim üç katman
Üç seviye var ve üçünü de 'pin' diye satmak yanıltıcı. Major bırakmak neredeyse latest. Minor, ayda bir sürpriz. Patch + distro varyantı çoğu ekip için yeterli. Digest, yayın imajı ve temel imaj için durduğum yer.
- Lokal deneme: hareket eden tag'e izin var, dosyaya yazılmaz.
- CI build: patch tag veya digest. İkisinden biri kayıtlı olmalı.
- Üretime giden imaj: kendi registry'imizde imza + digest. latest yok.
Kendi ürettiğim uygulama imajına latest basmayı da kestim. app:latest çekmek, hangi git SHA'sının çalıştığını siler. app:1.4.2 ve app@sha256:... duruyor. latest'i 'şu anki' diye tutan yerler var; orada bile latest'i deploy anahtarı yapmıyorum. Deploy anahtarı digest.
# Hareket eder. Cuma ve Pazartesi aynı satır değildir.
FROM node:latest AS build
WORKDIR /src
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:latest
WORKDIR /app
COPY --from=build /src/dist ./dist
CMD ["node", "dist/server.js"]
Aynı dosyanın durduğu hali. Temel imaj artık bir cümle: hangi distro, hangi patch, hangi digest. Build aşaması ile çalışma aşaması ayrı pinlenir. İkisini de latest yapmak, iki kez kura çekmektir.
FROM node:20.11.1-bookworm-slim@sha256:45a3ee83465c11953e80d126ca9b6675c176e7690401f8989607138892567973 AS build
WORKDIR /src
COPY package.json package-lock.json ./
RUN npm ci --omit=dev=false
COPY . .
RUN npm run build
FROM node:20.11.1-bookworm-slim@sha256:45a3ee83465c11953e80d126ca9b6675c176e7690401f8989607138892567973
WORKDIR /app
ENV NODE_ENV=production
RUN useradd --system --uid 10001 --create-home app
USER app
COPY --from=build --chown=app:app /src/dist ./dist
CMD ["node", "dist/server.js"]
Digest'i docker buildx imagetools inspect node:20.11.1-bookworm-slim satırı verir. Kopyala-yapıştır. İncelemeden yapıştırma. Multi-arch listesinde platform satırını oku.
Compose aynı işareti tekrarlar
Dockerfile'ı pinleyip compose'ta image: nginx:latest bırakmak, tuzağı bir dosyadan diğerine taşır. docker compose pull sunucuda latest'i çözer. İki sunucu, iki günde, iki nginx. Biri CVE kapatmıştır, biri config testini kırar.
services:
web:
image: nginx:1.25.4-bookworm@sha256:9c3fe2fda073984596bae46aeee1e687fb3f57d857c4cc8a326917d63d5b8e3e
restart: unless-stopped
ports:
- "8080:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
Compose'ta da digest duruyor. Tag'i insan okusun diye bırakabilirsin; çözülen şey yine digest olmalı. docker compose images çıktısındaki hash, deploy kaydına yazılır. 'nginx latest çektik' cümlesi kayıt değildir.
CI tag'i dondurmazsa Dockerfile yetmez
GitHub Actions runner her işte temiz diskle gelir. docker pull her seferinde registry'ye sorar. Yerel cache yoksa latest her pipeline'da günceldir. Bu, 'CI daha doğru' değil, 'CI her gün başka temel imaj deneyebilir' demektir.
name: api-image
on:
push:
branches: [main]
tags: ["v*.*.*"]
env:
NODE_DIGEST: sha256:45a3ee83465c11953e80d126ca9b6675c176e7690401f8989607138892567973
jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}/api:${{ github.sha }}
build-args: |
NODE_DIGEST=${{ env.NODE_DIGEST }}
cache-from: type=gha
cache-to: type=gha,mode=max
Digest'i workflow env'inde tutmak, FROM satırını ARG ile beslemek içindir. İkisini birden güncellemeden 'ilerledik' demiyorum. Tek yerde güncelle, PR aç, changelog'a bak. İki yerin kayması, latest'ten daha sinsi; çünkü ikisi de 'pinli' görünür.
Güncellemeyi insana bırakmamak
Digest pinlemek, yama almamak değil. Yama alma işini docker pull latest rastgeleliğinden çıkarıp bir PR'a çevirmek. Renovate ve Dependabot Dockerfile digest'ini izleyebilir. PR'da eski hash, yeni hash, bazen de upstream notu durur. Ben karar veririm. Registry karar vermez.
Haftalık bir bot PR'si, Cuma-Pazartesi sürprizinden ucuz. Bot'u kör merge etmek latest'e dönüşür. Bot'u okumak, temel imajı ürün kararı gibi ele almaktır. OpenSSL yaması evet, Node ABI kırılması hayır. İkisini ayıran insan.
Rollback digest ister
Tag hareket edince 'bir önceki latest' diye bir şey kalmaz. Registry o ismi yeni blob'a kaydırmıştır. Eski blob hâlâ duruyor olabilir; ismin artık onu göstermemesi, onu kaybettiğin anlamına gelir. Digest ile yayınladıysan önceki digest hâlâ bir adrestir. Deploy kaydı o adresi tutar.
Bunu rollback yazısında ayrı açacağım. Burada tek cümle: latest ile geri dönmek, tag'i geri çekmeye çalışmaktır. Tag'i geri çekmek, başka bir ortamın 'şu anki' sandığı şeyi de kaydırır. Digest ile geri dönmek, bilinen bir blob'u tekrar koşturmaktır. İkincisi sıkıcı, birincisi tiyatro.
Ne zaman latest durabilir
Throwaway bir deneme konteyneri. docker run --rm -it ile bir aracı denemek. Orada latest zarar vermez; çünkü çıktı bir artifact değil. Artifact üreten her yerde latest durmaz: Dockerfile, compose, Helm values, Actions, cron ile çalışan host pull'u.
Dokümantasyonda latest göstermek de ayrı bir sızıntı. Kopyalanan ilk şey o satır olur. Örnek dosyada da pin duruyor. Okuyan kişi hash'i kendi registry'sinden alır. 'Kısaltılmış örnek' diye latest yazmak, kısa yoldan yanlış öğretmek.
Kontrol listesi
- FROM satırında latest, distro'suz major, veya sadece '20' yok.
- Üretilen uygulama imajı git SHA veya semver + digest ile etiketlenir.
- compose ve Helm aynı digest'i taşır; 'latest çek' runbook'ta durmaz.
- Temel imaj güncellemesi PR ile gelir, gece pull ile gelmez.
- Deploy kaydı tag değil digest yazar.
Bot PR'si nasıl duruyor
Renovate'i Dockerfile digest'ine bağlamak, 'pinledik artık yama almıyoruz' itirazını kapatır. Bot haftada bir PR açar. PR'da eski hash, yeni hash, bazen upstream changelog linki. Ben okurum. OpenSSL yaması evet. Node ABI kırılması hayır. Kör merge, latest'i insan eliyle tekrar etmek.
{
"extends": ["config:recommended"],
"dockerfile": {
"enabled": true
},
"packageRules": [
{
"matchManagers": ["dockerfile"],
"matchDatasources": ["docker"],
"pinDigests": true,
"automerge": false
}
]
}
Dependabot benzer işi yapar. İkisinden biri yeter. İkisini birden açmak çift PR'dır. Kural: digest pin açık, automerge kapalı, gruplama yok. Temel imajı 'minor bump ile diğer kütüphaneler' diye paketlemek, inceleme dikkatini dağıtır.
Helm ve values aynı işareti taşır
Dockerfile'ı pinleyip Helm values'da tag: latest bırakmak, tuzağı üçüncü dosyaya taşır. Chart'ın image.repository + image.tag + image.digest üçlüsünde digest kazanır. Tag insan okusun diye durur. Küme çektiği şey digest'tir.
image:
repository: ghcr.io/local/api
tag: "1.4.2"
digest: sha256:7c2d1b0e4a9f2c8b1d6e0a4f3c9b8e7d6c5b4a3928170654433221100ffeedd
Kustomize images transformer'ı aynı işi yapar. CI, digest'i oraya yazar. Elle values düzenlemek, Cuma-Pazartesi hikâyesini küme diline çevirir. İki replica, iki pull zamanı, iki 'şu anki' latest. Bunu bir kez yaşadım; servis mesh değil, tag idi.
İşaretsiz digest ve registry temizlik
Digest pinlemenin sessiz düşmanı garbage collection. Registry, tag'i olmayan blob'u siler. latest kayınca eski digest 'hâlâ adrestir' dedim — tag başka yere bakıyorsa o blob untagged kalır. Retention 7 günse, Cuma'nın imajı Perşembe yoktur. Rollback cümlesi boşalır.
Bu yüzden yayın imajını kendi registry'nizde semver + digest ile tag'leyin. Upstream node@sha256:... için de bir kopya alın: docker buildx imagetools create ile kendi repo'nuzda aynı blob'a ikinci bir isim. Upstream GC sizi ilgilendirmez. Kendi GC politikanız, 'son 20 semver ve son 50 sha' gibi sayıya bağlanır. Sayı yoksa temizlik cesaret ister, cesaret de yanlış blob'u yer.
İmzalamak (cosign) pin'in kuzeni. İmza, digest üzerinedir, latest üzerine değil. latest'i imzalamak, her hareketinde yeni imza demektir; doğrulayan taraf 'şu anki'yi kabul eder. Digest + imza, 'bu blob, bu anahtar' der. Tag o cümlede yoktur.
FROM scratch istisnası değil
FROM scratch tag taşımaz, boştur. Asıl kaçış FROM alpine ve FROM debian gibi kısa isimler. Distro adı da latest gibi davranır. alpine:3.19.1 yazmadan alpine yazmak, Pazartesi sürprizinin ince hali. Scratch kullansanız bile kopyaladığınız binary'nin temel kütüphanesi başka bir imajdan geliyorsa, o imaj pinlenir.
Lokal docker run --rm -it node:latest hâlâ duruyor. Orada artifact yok. Artifact olan her yerde — Dockerfile, compose, Helm, Actions, cron pull — latest yok. Bu cümleyi runbook'un ilk satırına yazdım. İkinci satır: deploy kaydı digest yazar.
Ne zaman pin'i erteledim
İç registry yokken her FROM satırına 64 karakter hash yazmak, PR'ı okunmaz yapar. O ekipte önce patch tag + distro: node:20.11.1-bookworm-slim. Digest'i CI'da imagetools inspect ile kayda geçirip deploy notuna yazdık. Dosya hâlâ okunuyor, yayın hâlâ tekrarlanıyor. Digest'i dosyaya indirmek ikinci adım. İlk adım latest'i silmek.
Kanonik karşı örnek: FROM scratch üzerine kendi statik binary'nizi kopyalamak. Orada temel imaj hareket etmez. Hareket eden, COPY --from=build kaynağıdır. Build aşaması pinli değilse scratch sizi kurtarmaz. Boş FROM, boş güvenlik değil.
Özetlediğim sözleşme kısa: hareket eden isim yok, güncelleme PR ile gelir, yayın kaydı digest yazar, kendi registry'nizde semver durur. Bu dördü yoksa 'imajı sabitledik' cümlesi slayttır. Slayt, Pazartesi sabahı test kırılmaz; digest kırılır. Digest kırılırsa serinin geri kalanını okumanın da anlamı incelir. Bu yüzden latest'i önce kestim, boyutu sonra konuştum. Pin olmadan incelik tesadüftür.
Bu serinin geri kalanı image boyutuna ve compose varsayılanlarına iniyor. latest'i çıkarmadan onları düzeltmek, hareket eden bir zeminde temizlik yapmak. Önce zemini sabitle. Sonra şişen katmana bak. Sabit zemin olmadan incelik ve compose tartışması tesadüfe iner.
Yorumlar
Yorumlar (0)
Yorumlar üyelere açık. Üye ol · Giriş yap