مختبر عملي: بناء حاويات قابلة للتكرار — Pinning و Verifying و Diffing للصور

نظرة عامة

إذا قمت ببناء نفس ملف Dockerfile مرتين وحصلت على صور مختلفة، فلن تتمكن من التحقق من سلامة البناء. البناء غير القابل للتكرار يعني أنه لا توجد لديك طريقة للتأكد من أن العنصر البرمجي الذي يعمل في الإنتاج قد تم إنتاجه فعلاً من الكود المصدري الذي قمت بمراجعته. يمكن للمهاجمين استغلال هذا الغموض لحقن كود خبيث أثناء عملية البناء دون اكتشافه.

يرشدك هذا المختبر العملي عبر مصادر عدم القابلية للتكرار في بناء الحاويات، ويوضح تقنيات للقضاء على كل منها، ويبيّن كيفية التحقق من القابلية للتكرار تلقائياً في خطوط أنابيب CI/CD. بنهاية هذا المختبر، سيكون لديك ملف Dockerfile قابل للتكرار بالكامل وسير عمل GitHub Actions يثبت ذلك مع كل commit.

المتطلبات الأساسية

  • Docker مع BuildKit — Docker Desktop 23.0+ يأتي مع تفعيل BuildKit افتراضياً. تحقق باستخدام docker buildx version.
  • diffoscope — قم بالتثبيت باستخدام pip install diffoscope. هذه الأداة تقوم بمقارنة عميقة ومتكررة للملفات والأرشيفات.
  • crane — قم بالتثبيت من go-containerregistry. تُستخدم لفحص صور الحاويات والسجلات والتعامل معها.
  • Cosign — قم بالتثبيت من Sigstore. تُستخدم لتوقيع صور الحاويات والتحقق منها.
  • مستودع اختبار يحتوي على Dockerfile (سنقوم بإنشائه في خطوة الإعداد).
  • Go 1.22+ مثبت محلياً (اختياري، للاختبار المحلي خارج Docker).

إعداد البيئة

أنشئ مستودع اختبار جديد مع تطبيق Go بسيط. هذا يمنحنا مشروعاً واقعياً ومبسطاً للعمل معه خلال المختبر.

الخطوة 1: تهيئة المشروع

mkdir repro-build-lab && cd repro-build-lab
git init
go mod init github.com/example/repro-build-lab

الخطوة 2: إنشاء تطبيق Go

أنشئ الملف cmd/app/main.go:

package main

import (
	"fmt"
	"net/http"
	"os"
)

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello from repro-build-lab v1\n")
	})

	http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		fmt.Fprintf(w, "ok\n")
	})

	fmt.Printf("Listening on :%s\n", port)
	http.ListenAndServe(":"+port, nil)
}

الخطوة 3: إنشاء Dockerfile غير قابل للتكرار عمداً

يحتوي هذا الملف Dockerfile على كل خطأ شائع يؤدي إلى بناءات غير قابلة للتكرار:

# Intentionally non-reproducible Dockerfile
FROM golang:latest

WORKDIR /src

# Floating package versions
RUN apt-get update && apt-get install -y curl

# Embeds current timestamp into the image
RUN echo "Built at $(date)" > /build-info

COPY . .

RUN go build -o /app ./cmd/app

EXPOSE 8080
CMD ["/app"]

لاحظ المشاكل:

  • FROM golang:latest — الصورة الأساسية تتغير دون تحذير.
  • apt-get install -y curl — لا يوجد تثبيت إصدار محدد، لذا الإصدار المثبت يتغير.
  • echo "Built at $(date)" — يحقن طابعاً زمنياً مختلفاً في كل بناء.
  • لا يوجد .dockerignore — الملفات المحلية مثل .git/ تتسرب إلى سياق البناء، مما يغير تجزئات الطبقات.

قم بعمل commit للمشروع الأولي:

git add -A
git commit -m "Initial non-reproducible project"

التمرين 1: إثبات عدم القابلية للتكرار

قبل إصلاح أي شيء، دعنا نثبت أن ملف Dockerfile الحالي ينتج صوراً مختلفة في كل بناء.

الخطوة 1: بناء الصورة مرتين

# First build
docker build --no-cache -t myapp:build1 .

# Wait a moment so the timestamp differs
sleep 2

# Second build
docker build --no-cache -t myapp:build2 .

علامة --no-cache تجبر Docker على تنفيذ كل طبقة من الصفر، وهو أمر ضروري لهذه المقارنة. في بيئة CI/CD حقيقية، غالباً ما تعمل البناءات على runners جديدة بدون ذاكرة تخزين مؤقت.

الخطوة 2: مقارنة بصمات الصور

docker inspect --format='{{.Id}}' myapp:build1
# sha256:a1b2c3d4e5f6... (example)

docker inspect --format='{{.Id}}' myapp:build2
# sha256:f6e5d4c3b2a1... (different!)

البصمات مختلفة على الرغم من أنه لم يتغير شيء في الكود المصدري. هذا يعني أنك لا تستطيع التحقق من أن صورة معينة تم إنتاجها من commit محدد.

الخطوة 3: استخدام diffoscope لتحديد الاختلافات

# Export both images as tarballs
docker save myapp:build1 -o build1.tar
docker save myapp:build2 -o build2.tar

# Run diffoscope
diffoscope build1.tar build2.tar --html-dir diff-report

افتح diff-report/index.html في المتصفح. يكشف التقرير بالضبط ما يختلف بين البناءين:

  • الطوابع الزمنية — ملف /build-info يحتوي على تواريخ مختلفة.
  • بيانات وصفية لحزم apt — قوائم الحزم وملفات الذاكرة المؤقتة تحتوي على طوابع زمنية وقد تسحب إصدارات فرعية مختلفة.
  • ملف Go الثنائي — الملف الثنائي المُجمَّع يحتوي على مسارات بناء مضمنة ومعرّفات بناء.
  • ترتيب الطبقات والبيانات الوصفية — Docker يضمّن طوابع زمنية للإنشاء في البيانات الوصفية للطبقات.

كل من هذه يمثل مصدراً لعدم القابلية للتكرار سنقوم بالقضاء عليه في التمارين التالية.

التمرين 2: تثبيت الصورة الأساسية بواسطة Digest

أكبر مصدر للانحراف هو الصورة الأساسية. golang:latest هو هدف متحرك — يمكن أن يتغير بين البناءات، بين تشغيلات CI، أو حتى بين المناطق إذا كان السجل متسقاً في النهاية.

الخطوة 1: العثور على digest الحالي

crane digest golang:1.22
# sha256:d0902bacefdde1cf45079803dc16feeb58f3aa9df52052cc00deb2c3e5de367b

الخطوة 2: تثبيت الصورة الأساسية

حدّث سطر FROM في ملف Dockerfile:

FROM golang:1.22@sha256:d0902bacefdde1cf45079803dc16feeb58f3aa9df52052cc00deb2c3e5de367b

التنسيق هو image:tag@sha256:digest. سيقوم Docker بالسحب عبر digest، متجاهلاً الوسم. يتم الاحتفاظ بالوسم لسهولة القراءة البشرية.

الخطوة 3: إعادة البناء والمقارنة

docker build --no-cache -t myapp:pinned1 .
sleep 2
docker build --no-cache -t myapp:pinned2 .

docker inspect --format='{{.Id}}' myapp:pinned1
docker inspect --format='{{.Id}}' myapp:pinned2

البصمات لا تزال مختلفة — مصادر أخرى لعدم القابلية للتكرار لا تزال موجودة. لكن إذا قارنت الطبقات، فإن طبقة الصورة الأساسية أصبحت الآن متطابقة بين البناءين. لقد قضيت على أكبر مصدر للانحراف.

لماذا هذا مهم

بدون تثبيت digest، يمكن لوسم مخترق أو مختطف أن يستبدل صورتك الأساسية بصورة خبيثة بصمت. تثبيت digest هو ضمان تشفيري: تحصل على البايتات التي تتوقعها بالضبط، أو يفشل البناء.

التمرين 3: تثبيت إصدارات الحزم

إصدارات الحزم المتغيرة تُدخل عدم الحتمية في طبقة التبعيات. في كل مرة يتم تشغيل apt-get update، يجلب فهرس المستودع الحالي، الذي قد يسرد إصدارات حزم مختلفة.

الخيار أ: تثبيت إصدارات حزم Debian

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
      curl=7.88.1-10+deb12u8 && \
    rm -rf /var/lib/apt/lists/*

للعثور على الإصدار المتاح حالياً في صورتك الأساسية:

docker run --rm golang:1.22 apt-cache policy curl

الخيار ب: استخدام Alpine مع حزم مثبتة

حزم Alpine لديها سلاسل إصدارات أبسط وصور أصغر:

FROM golang:1.22-alpine@sha256:<alpine-digest>

RUN apk add --no-cache curl=8.5.0-r0

الخيار ج: بناء متعدد المراحل (مُفضَّل)

أفضل نهج هو تجنب تثبيت الحزم في الصورة النهائية تماماً. استخدم بناءً متعدد المراحل حيث تحتوي مرحلة البناء على الأدوات وتكون مرحلة التشغيل بسيطة:

# Build stage — tools are only needed here
FROM golang:1.22@sha256:d0902bacefdde1cf45079803dc16feeb58f3aa9df52052cc00deb2c3e5de367b AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /app ./cmd/app

# Runtime stage — no apt-get, no floating packages
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app /app
CMD ["/app"]

مع هذا النهج، لا تحتوي صورة التشغيل على أي استدعاءات لمدير الحزم، مما يقضي على فئة كاملة من عدم القابلية للتكرار.

إعادة البناء والمقارنة

docker build --no-cache -t myapp:pinpkg1 .
sleep 2
docker build --no-cache -t myapp:pinpkg2 .

docker inspect --format='{{.Id}}' myapp:pinpkg1
docker inspect --format='{{.Id}}' myapp:pinpkg2

طبقات الحزم أصبحت الآن متطابقة بين البناءين. الاختلافات المتبقية تأتي من الطوابع الزمنية وملف Go الثنائي نفسه.

التمرين 4: إزالة الطوابع الزمنية والمحتوى غير الحتمي

الطوابع الزمنية هي المصدر الأكثر وضوحاً لعدم القابلية للتكرار. أي أمر يلتقط الوقت الحالي ينتج نتيجة مختلفة في كل بناء.

الخطوة 1: إزالة الطوابع الزمنية الصريحة

احذف السطر الذي يكتب وقت البناء:

# REMOVE this line:
# RUN echo "Built at $(date)" > /build-info

إذا كنت بحاجة إلى بيانات وصفية للبناء، مررها كتسمية بقيمة ثابتة مشتقة من المصدر:

ARG BUILD_COMMIT
LABEL org.opencontainers.image.revision=${BUILD_COMMIT}

الخطوة 2: تعيين SOURCE_DATE_EPOCH

SOURCE_DATE_EPOCH هو متغير بيئة موحد يخبر أدوات البناء باستخدام طابع زمني ثابت بدلاً من الوقت الحالي. العديد من الأدوات تحترمه، بما في ذلك tar و gzip و zip ومُجمِّع Go.

ARG SOURCE_DATE_EPOCH
ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}

قم بالبناء مع الطابع الزمني لآخر git commit:

docker build \
  --build-arg SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) \
  --no-cache \
  -t myapp:repro .

هذا يضمن أن البناءات من نفس الـ commit تستخدم دائماً نفس الطابع الزمني، بغض النظر عن وقت تشغيل البناء فعلياً.

الخطوة 3: استخدام مخرجات BuildKit OCI

يمكن لـ BuildKit إنتاج صور بتنسيق OCI مع إنشاء طبقات أكثر حتمية:

docker buildx build \
  --build-arg SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) \
  --output type=oci,dest=myapp.tar \
  --no-cache \
  .

تنسيق مخرجات OCI يتجنب بعض البيانات الوصفية غير الحتمية التي يتضمنها تنسيق صورة Docker الافتراضي.

التمرين 5: بناءات Go القابلة للتكرار

يضمّن Go عدة أجزاء من المعلومات غير الحتمية في الملفات الثنائية المُجمَّعة افتراضياً: مسارات الملفات المحلية، ومعرّف بناء فريد، ورموز تصحيح الأخطاء التي تشير إلى بيئة البناء.

الخطوة 1: استخدام علامات البناء القابل للتكرار

RUN CGO_ENABLED=0 go build \
    -trimpath \
    -ldflags="-s -w -buildid=" \
    -o /app ./cmd/app

إليك ما يفعله كل علم:

العلم الغرض
CGO_ENABLED=0 يعطّل cgo، مما ينتج ملفاً ثنائياً مرتبطاً استاتيكياً. يتجنب الاعتماد على مكتبات C النظامية التي قد تختلف بين البناءات.
-trimpath يزيل جميع مسارات نظام الملفات المحلي من الملف الثنائي المُجمَّع. بدون هذا، يحتوي الملف الثنائي على مسارات مثل /src/cmd/app/main.go من بيئة البناء.
-ldflags="-s -w" يزيل جدول الرموز (-s) ومعلومات تصحيح DWARF (-w). هذه تحتوي على بيانات خاصة ببيئة البناء.
-ldflags="-buildid=" يعيّن معرّف البناء إلى فارغ. عادةً ما ينشئ Go معرّف بناء فريد يتغير بين البناءات حتى مع مصدر متطابق.

الخطوة 2: التحقق من قابلية تكرار الملف الثنائي

# Build twice
docker build --no-cache -t myapp:go1 .
docker build --no-cache -t myapp:go2 .

# Extract and hash the binary from each image
docker create --name tmp1 myapp:go1
docker cp tmp1:/app ./app1
docker rm tmp1

docker create --name tmp2 myapp:go2
docker cp tmp2:/app ./app2
docker rm tmp2

sha256sum app1 app2

يجب أن تكون تجزئات SHA-256 لـ app1 و app2 متطابقة. الملف الثنائي لـ Go أصبح الآن قابلاً للتكرار بت ببت.

التمرين 6: ملف Dockerfile القابل للتكرار بالكامل

الآن دعنا نجمع كل تقنية في ملف Dockerfile واحد قابل للتكرار بالكامل.

ملف Dockerfile الكامل

# syntax=docker/dockerfile:1

# ---- Build Stage ----
FROM golang:1.22@sha256:d0902bacefdde1cf45079803dc16feeb58f3aa9df52052cc00deb2c3e5de367b AS builder

ARG SOURCE_DATE_EPOCH
ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}

WORKDIR /src

# Cache dependency downloads
COPY go.mod go.sum ./
RUN go mod download && go mod verify

# Copy source and build
COPY . .
RUN CGO_ENABLED=0 go build \
    -trimpath \
    -ldflags="-s -w -buildid=" \
    -o /app ./cmd/app

# ---- Runtime Stage ----
FROM gcr.io/distroless/static-debian12:nonroot@sha256:6ec5aa99dc335b19f6c2bcb8e09cf92404e56f0db4e2f58cf92c4536e1548415

ARG SOURCE_DATE_EPOCH
ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}

COPY --from=builder /app /app

USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app"]

ملف .dockerignore الكامل

.git
.github
.gitignore
*.md
README*
LICENSE
docker-compose*.yml
Makefile
.env
.env.*
*.tar
*.log
tmp/
build/
diff-report/

ملف .dockerignore بالغ الأهمية. بدونه، يتسرب مجلد .git/ إلى سياق البناء. بما أن .git/ يحتوي على طوابع زمنية وملفات قفل وبيانات وصفية متغيرة أخرى، فإنه يجعل كل سياق بناء فريداً حتى عندما يكون المصدر متطابقاً.

البناء والتحقق

SOURCE_EPOCH=$(git log -1 --format=%ct)

# Build twice
docker buildx build \
  --build-arg SOURCE_DATE_EPOCH=${SOURCE_EPOCH} \
  --output type=oci,dest=build1.tar \
  --no-cache .

docker buildx build \
  --build-arg SOURCE_DATE_EPOCH=${SOURCE_EPOCH} \
  --output type=oci,dest=build2.tar \
  --no-cache .

# Compare
sha256sum build1.tar build2.tar

مع تطبيق جميع تقنيات القابلية للتكرار، يجب أن تتطابق تجزئات SHA-256 لأرشيفي OCI أو تكون قريبة جداً. أي اختلافات متبقية ستكون في البيانات الوصفية لتكوين الصورة ويمكن حلها باستخدام علامة --source-date-epoch في BuildKit (متاحة في BuildKit 0.13+):

docker buildx build \
  --build-arg SOURCE_DATE_EPOCH=${SOURCE_EPOCH} \
  --source-date-epoch ${SOURCE_EPOCH} \
  --output type=oci,dest=build-final.tar \
  --no-cache .

التمرين 7: التحقق من القابلية للتكرار في CI/CD

القابلية للتكرار تكون ذات قيمة فقط إذا تحققت منها باستمرار. بناء قابل للتكرار اليوم يمكن أن يصبح غير قابل للتكرار غداً إذا أضاف شخص ما تبعية متغيرة أو طابعاً زمنياً. الحل هو البناء مرتين في كل تشغيل CI والتأكد من أن النتائج متطابقة.

سير عمل GitHub Actions

أنشئ الملف .github/workflows/reproducible-build.yml:

name: Verify Reproducible Build

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  verify-reproducibility:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Compute SOURCE_DATE_EPOCH
        id: epoch
        run: echo "value=$(git log -1 --format=%ct)" >> "$GITHUB_OUTPUT"

      - name: Build image (first pass)
        run: |
          docker buildx build \
            --build-arg SOURCE_DATE_EPOCH=${{ steps.epoch.outputs.value }} \
            --output type=oci,dest=build-pass1.tar \
            --no-cache \
            .

      - name: Record first digest
        id: digest1
        run: echo "sha=$(sha256sum build-pass1.tar | awk '{print $1}')" >> "$GITHUB_OUTPUT"

      - name: Build image (second pass)
        run: |
          docker buildx build \
            --build-arg SOURCE_DATE_EPOCH=${{ steps.epoch.outputs.value }} \
            --output type=oci,dest=build-pass2.tar \
            --no-cache \
            .

      - name: Record second digest
        id: digest2
        run: echo "sha=$(sha256sum build-pass2.tar | awk '{print $1}')" >> "$GITHUB_OUTPUT"

      - name: Compare digests
        run: |
          echo "Build 1: ${{ steps.digest1.outputs.sha }}"
          echo "Build 2: ${{ steps.digest2.outputs.sha }}"
          if [ "${{ steps.digest1.outputs.sha }}" != "${{ steps.digest2.outputs.sha }}" ]; then
            echo "::error::Builds are NOT reproducible! Digests differ."
            echo "Running diffoscope to identify differences..."
            pip install diffoscope
            diffoscope build-pass1.tar build-pass2.tar --text diff-output.txt || true
            cat diff-output.txt
            exit 1
          fi
          echo "Builds are reproducible. Digests match."

      - name: Upload diff report on failure
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: reproducibility-diff
          path: diff-output.txt

      - name: Sign the verified image
        if: github.ref == 'refs/heads/main'
        env:
          COSIGN_EXPERIMENTAL: "true"
        run: |
          # Load the OCI image into Docker
          docker load -i build-pass1.tar
          # In production, push to a registry and sign with Cosign:
          # cosign sign --yes $REGISTRY/$IMAGE@$DIGEST
          echo "Image verified as reproducible and ready for signing."

يقوم سير العمل هذا بما يلي في كل push و pull request:

  1. يسحب الكود ويُعدّ BuildKit.
  2. يحسب SOURCE_DATE_EPOCH من الطابع الزمني لآخر commit.
  3. يبني الصورة من الصفر (المرور الأول) ويسجل البصمة.
  4. يبني الصورة من الصفر مرة أخرى (المرور الثاني) ويسجل البصمة.
  5. يقارن البصمتين. إذا اختلفتا، تفشل المهمة ويتم تشغيل diffoscope لإنتاج تقرير فروقات مفصل.
  6. عند النجاح على الفرع الرئيسي، تكون الصورة المُتحقق منها جاهزة للتوقيع باستخدام Cosign.

هذا هو أقوى ضمان يمكنك الحصول عليه: كل تشغيل CI يثبت أن بناءك قابل للتكرار. إذا أدخل مطور عدم حتمية، ينكسر البناء فوراً.

التمرين 8: مقارنة الصور بين الإصدارات

البناءات القابلة للتكرار تمنحك أيضاً القدرة على مقارنة الفروقات بين الإصدارات والتحقق من أن التغييرات المتوقعة فقط هي الموجودة. هذا أمر بالغ الأهمية لتدقيق الإصدارات: تريد التأكد من أن ترقية الإصدار غيّرت فقط الملف الثنائي للتطبيق، وليس الصورة الأساسية أو حزم النظام.

الخطوة 1: بناء الإصدار 1

SOURCE_EPOCH=$(git log -1 --format=%ct)

docker buildx build \
  --build-arg SOURCE_DATE_EPOCH=${SOURCE_EPOCH} \
  --output type=oci,dest=image-v1.tar \
  --no-cache .

الخطوة 2: إجراء تغيير في الكود

عدّل cmd/app/main.go لتغيير سلسلة الإصدار:

fmt.Fprintf(w, "Hello from repro-build-lab v2\n")

قم بعمل commit للتغيير:

git add cmd/app/main.go
git commit -m "Bump to v2"

الخطوة 3: بناء الإصدار 2

SOURCE_EPOCH=$(git log -1 --format=%ct)

docker buildx build \
  --build-arg SOURCE_DATE_EPOCH=${SOURCE_EPOCH} \
  --output type=oci,dest=image-v2.tar \
  --no-cache .

الخطوة 4: المقارنة باستخدام diffoscope

diffoscope image-v1.tar image-v2.tar --html-dir version-diff-report

افتح التقرير. يجب أن ترى أن الاختلافات الوحيدة هي:

  • الملف الثنائي لتطبيق Go — لأننا غيّرنا الكود المصدري.
  • قيمة SOURCE_DATE_EPOCH — لأن الطابع الزمني لـ commit تغير.

طبقات الصورة الأساسية وبيئة التشغيل distroless وجميع الطبقات الأخرى يجب أن تكون متطابقة تماماً.

الخطوة 5: مقارنة الطبقات باستخدام crane

# Load images and push to a local registry for crane inspection
docker run -d -p 5000:5000 --name registry registry:2

# Load and push v1
docker load -i image-v1.tar
docker tag myapp:latest localhost:5000/myapp:v1
docker push localhost:5000/myapp:v1

# Load and push v2
docker load -i image-v2.tar
docker tag myapp:latest localhost:5000/myapp:v2
docker push localhost:5000/myapp:v2

# List layers for each version
crane manifest localhost:5000/myapp:v1 | jq '.layers[].digest'
crane manifest localhost:5000/myapp:v2 | jq '.layers[].digest'

قارن بصمات الطبقات. سترى أن جميع الطبقات متطابقة باستثناء الطبقة التي تحتوي على ملف Go الثنائي. هذا بالضبط ما تريده: ترقية الإصدار يجب أن تغير فقط طبقة التطبيق، لا شيء آخر.

إذا رأيت تغييرات غير متوقعة في الطبقات (على سبيل المثال، طبقة الصورة الأساسية تختلف)، فهذا يعني أن شيئاً ما كسر القابلية للتكرار ويحتاج إلى تحقيق. هذه المقارنة طبقة بطبقة هي تقنية تدقيق قوية تعمل فقط عندما تكون بناءاتك قابلة للتكرار.

التنظيف

# Remove test images
docker rmi myapp:build1 myapp:build2 myapp:pinned1 myapp:pinned2 \
  myapp:pinpkg1 myapp:pinpkg2 myapp:go1 myapp:go2 myapp:repro 2>/dev/null

# Remove OCI tarballs
rm -f build1.tar build2.tar build-pass1.tar build-pass2.tar \
  image-v1.tar image-v2.tar myapp.tar

# Remove extracted binaries
rm -f app1 app2

# Remove diff reports
rm -rf diff-report version-diff-report

# Stop and remove the local registry
docker stop registry && docker rm registry 2>/dev/null

# Remove the test project (optional)
cd .. && rm -rf repro-build-lab

النقاط الرئيسية

  • ثبّت الصور الأساسية بواسطة digest وليس بالوسم. الوسوم هي مؤشرات قابلة للتغيير. البصمات هي ضمانات تشفيرية. استخدم crane digest للعثور على digest الحالي وقم بتحديثه عمداً من خلال PR، وليس بصمت أثناء البناء.
  • ثبّت جميع إصدارات الحزم أو تجنب مديري الحزم في صورة التشغيل. البناءات متعددة المراحل مع صور تشغيل distroless أو scratch تقضي على فئة كاملة من عدم القابلية للتكرار.
  • أزل جميع مصادر الطوابع الزمنية. استخدم SOURCE_DATE_EPOCH المشتق من الطابع الزمني لـ git commit. لا تقم أبداً بتشغيل date أو timestamp أو أوامر مشابهة في Dockerfile.
  • استخدم علامات المُجمِّع القابلة للتكرار. لـ Go: -trimpath و -ldflags="-s -w -buildid=" و CGO_ENABLED=0. اللغات الأخرى لديها خيارات مشابهة.
  • تحقق من القابلية للتكرار في CI/CD بالبناء مرتين والمقارنة. هذه هي الطريقة الوحيدة لضمان بقاء بنائك قابلاً للتكرار مع تطور المشروع. إذا اختلفت البصمات، أفشل البناء.
  • استخدم diffoscope لتدقيق التغييرات بين الإصدارات. البناءات القابلة للتكرار تمكّن مقارنات صور ذات معنى. يمكنك التحقق من أن الإصدار يحتوي فقط على التغييرات التي قصدتها — لا شيء أكثر.

الخطوات التالية

الآن بعد أن أصبح بإمكانك إنتاج صور حاويات قابلة للتكرار، استكشف كيفية بناء سلسلة كاملة للسلامة والمصدر حولها: