קונטיינרים מצומצמים עם בנייה מרובת שלבים ו־Distroless
גרסה לימודית מתוקנת; תאריך המקור האנגלי: 2026-08-13T10:09:12.130+03:00. המהדורה בעברית משתמשת בתאריך הפרסום שלה בפועל, ולא בתאריך המקור באנגלית. כל דוגמאות הקוד והמספרים להמחשה בלבד ולא הורצו או נמדדו במסגרת ההכנה הזאת. English original
יישום קטן יכול ליצור תמונת קונטיינר גדולה כאשר שולחים איתו כלי בנייה, מטמונים וקבצים לא נחוצים. דוגמת 50 השורות ו־1.2 GB במקור הייתה המחשה, לא מדידה של יישום מזוהה. בדקו את שכבות התמונה בפועל לפני ההחלטה מה להסיר. [מקור 1]
תוכן מיותר בתמונה יכול להגדיל את עבודת ההעברה והאחסון ולהוסיף חבילות לתחזוקה. השפעות על התנעה קרה ופריסה תלויות במטמון, בהתנהגות הפלטפורמה ובאתחול היישום; גודל התמונה לבדו אינו ציון אבטחה. השאירו סביבת ריצה מצומצמת לצרכים האמיתיים ותכננו נתיב ניפוי תקלות נפרד. [מקור 1] [מקור 2]
הדפוס המעשי הוא לבנות עם הכלים הדרושים ולהעתיק לשלב הסופי רק תוצרי ריצה שנבדקו. נבחן דוגמאות Node.js ו־TypeScript ו־Go לצד הפשרות של Distroless. כל הדוגמאות לא הורצו ואינן מספקות האצה שנמדדה, שיעור הקטנה של תמונה או הבטחת אבטחה. [מקור 1]
הבנת הגישה בנייה רחבה לעומת הפצה מצומצמת
הפרידו בין הדרישות לבניית תוצר לבין הדרישות להפעלתו: [מקור 1]
שלב הבנייה הרחבה
סביבות בנייה עשויות להזדקק ל־SDKs, למהדרים, למנהלי חבילות, לכותרות ולכלי בדיקה. כללו את מה שהפרויקט צריך, אך עדיין הגבילו פרטי הזדהות, גישת רשת וסקריפטי התקנה לא מהימנים. שלב בנייה עשיר אינו היתר לשרשרת אספקה בלתי מוגבלת. [מקור 1] [מקור 2]
שלב ההפצה המצומצמת
התמונה הסופית צריכה את היישום ואת כל דרישות הריצה: מפרשים או ספריות משותפות כשנחוץ, תעודות, נתוני אזור זמן, נכסים ונתיבים לכתיבה לפי הצורך. קבצים בינאריים של Go ו־Rust אינם סטטיים לחלוטין באופן אוטומטי, ו־bytecode של Python עדיין דורש מפרש תואם. הסרת מעטפות וכלים יכולה לצמצם כלי תקיפה זמינים, אך אינה מונעת הרצת קוד שרירותי ביישום פגיע. [מקור 1] [מקור 2]
הקסם של בנייה מרובת שלבים
לפני בנייה מרובת שלבים, גישה נפוצה אחת השתמשה ב־Dockerfiles נפרדים לבנייה ולריצה ובסקריפטים לחילוץ תוצרים. בנייה מרובת שלבים מרכזת את התהליך; קבצים נפרדים לא היו האפשרות ההיסטורית היחידה. [מקור 1]
בנייה מרובת שלבים, שנוספה ב־Docker 17.05, פתרה זאת באלגנטיות. היא מאפשרת להשתמש בכמה הוראות FROM באותו Dockerfile. כל הוראה מתחילה שלב בנייה חדש עם תמונת בסיס אחרת. חשוב מכך, אפשר להעתיק באופן בררני תוצרים בין השלבים ולהשאיר מאחור את כל מה שלא רוצים בתמונה הסופית.
נניח פרויקט TypeScript עם package.json, package-lock.json שנשמר במאגר, tsconfig.json ותיקיית src, המפיק dist/index.js. התאימו את רשימת הקבצים ל־workspaces, לנכסים סטטיים, למודולים native ולהגדרות הפרויקט. לא בוצעה כאן בניית פרויקט. [מקור 1] [מקור 2]
Dockerfile טיפוסי ומנופח של Node.js
ה־Dockerfile הראשון הוא המחשה חד־שלבית שאינה מומלצת בכוונה. Node 20 שבמקור הגיע לסוף התמיכה עד מועד הבדיקה הזה; הדוגמאות המתוקנות משתמשות בענף Node 24 הנתמך ובמשפחת תמונות עקבית המבוססת על Debian. בבנייה אמיתית יש לבחור digests בלתי משתנים שנבדקו, ולא להניח שתגיות משתנות מספקות שחזור. [מקור 1] [מקור 2]
# Runtime-untested ANTI-PATTERN for comparison; not a production recommendation.
FROM node:24-bookworm-slim
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]
הגישה הזאת יכולה להשאיר בשכבות קוד מקור, תלויות פיתוח ומטמון התקנה, בהתאם לפרויקט ולהגדרות. אין טענה לתוצאה של 800 MB. גם COPY . . עלול לכלול node_modules מקומיים, .git או סודות אם לא מגבילים את הקשר הבנייה. השתמשו ב־.dockerignore שנבדק, למשל כזה שמחריג node_modules, .git, dist, .env ו־.env.*; לעולם אל תטמיעו פרטי הזדהות בשכבות. [מקור 1] [מקור 2]
הגישה של בנייה רחבה והפצה מצומצמת
הדוגמה הבאה מפרידה הידור, התקנת תלויות ייצור וריצה. npm ci דורש lockfile תואם; --omit=dev משמיט תלויות פיתוח. מודולים native חייבים להתאים ל־ABI, ל־libc ולארכיטקטורת הריצה. אם התקנת הייצור צריכה מהדרים או סקריפטים של הפרויקט, ספקו אותם בשלב התלויות ובדקו את הקבצים המתקבלים, במקום להשבית בעיוורון סקריפטי מחזור חיים. [מקור 1]
# syntax=docker/dockerfile:1
# Runtime-untested. Resolve a reviewed digest instead of a mutable tag in production.
ARG NODE_IMAGE=node:24-bookworm-slim
FROM ${NODE_IMAGE} AS builder
WORKDIR /app
COPY package.json package-lock.json ./
# Assumes no project lifecycle script needs source before this step.
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
FROM ${NODE_IMAGE} AS production-deps
WORKDIR /app
COPY package.json package-lock.json ./
# Adapt this stage for native modules and project lifecycle scripts.
RUN npm ci --omit=dev
FROM ${NODE_IMAGE} AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=production-deps --chown=node:node /app/node_modules ./node_modules
COPY --from=builder --chown=node:node /app/dist ./dist
COPY --chown=node:node package.json ./
# Add required assets explicitly; never copy credentials.
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]
# Review a separate .dockerignore, e.g.:
# node_modules
# .git
# dist
# .env
# .env.*
# Use BuildKit secret mounts for private registry credentials, not ARG/ENV.
השלב הסופי אינו כולל את שכבות הבנייה והתקנת התלויות הנפרדות, אך עדיין מכיל את תמונת הבסיס שלו ואת כל מה שהועתק במפורש. הוא אינו כולל רק קובצי יישום. שיעור ההקטנה תלוי בפרויקט בפועל; לא נמדד חיסכון של 75%. ודאו שנכסים נחוצים נמצאים ותלויות המיועדות לפיתוח בלבד אינן נכללות. [מקור 1]
השלב הבא מעבר ל־Distroless
Distroless הוא אפשרות אריזה נוספת לריצה, לא ״שלב מתקדם״ אוטומטי מעבר ל־Alpine. בחרו לפי תאימות סביבת הריצה, עדכוני אבטחה, תצפית וצרכים תפעוליים. [מקור 1]
תמונות Distroless רגילות משמיטות מנהלי חבילות ומעטפות, ואילו גרסאות שפה או סביבת ריצה מספקות תלויות נבחרות. גרסאות debug שונות וכוללות יכולות ניפוי תקלות. קונטיינר עדיין חולק את ליבת המארח, וגם מרחב משתמש מצומצם דורש עדכוני חולשות. [מקור 1]
דוגמת Go מניחה יישום Go טהור שניתן לבנות עם CGO_ENABLED=0. היא משתמשת בענף Go נתמך במקום Go 1.22 שבמקור, מסירה חבילות GCC ו־musl לא נחוצות ומכוונת ל־Linux באמצעות משתני פלטפורמת היעד של BuildKit. אם דרושים CGO או ספריות דינמיות, השתמשו בתמונת ריצה תואמת ובדקו את הקישור במקום להתייחס ל־Distroless סטטי כפתרון אוניברסלי. [מקור 1] [מקור 2] [מקור 3]
# syntax=docker/dockerfile:1
# Runtime-untested; pure-Go application only. Review and pin actual image digests.
ARG GO_IMAGE=golang:1.26-bookworm
ARG RUNTIME_IMAGE=gcr.io/distroless/static-debian13:nonroot
FROM --platform=$BUILDPLATFORM ${GO_IMAGE} AS builder
ARG TARGETOS
ARG TARGETARCH
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# A reviewed .dockerignore must exclude secrets and unnecessary local files.
# This example deliberately supports Linux amd64/arm64 only.
RUN test "$TARGETOS" = "linux" && (test "$TARGETARCH" = "amd64" || test "$TARGETARCH" = "arm64") && mkdir -p /out && CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath -ldflags="-s -w" -o /out/app ./cmd/server
FROM ${RUNTIME_IMAGE} AS runner
WORKDIR /app
COPY --from=builder --chown=65532:65532 /out/app /app/app
USER 65532:65532
EXPOSE 8080
ENTRYPOINT ["/app/app"]
# Verify certificates, DNS, assets, writable paths, signals and linking in your app.
לא נבנתה ולא נמדדה כאן תמונה. גדלי תמונת הבנייה והתמונה הסופית תלויים ב־digests שנבחרו, בארכיטקטורה, בקובץ הבינארי, בספריות ובנכסים. כשמודדים, תעדו בנפרד את גודל ההעברה הדחוס ואת הגודל המקומי לאחר פריסה. [מקור 1] [מקור 2]
היעדר shell פירושו שמטענים מוכרים המבוססים על מעטפת עשויים לא לעבוד בצורתם המקורית, אך תוקף עדיין יכול לנצל את סביבת ריצת היישום, לבצע קריאות מערכת או להשתמש ביכולות היישום והרשת. Distroless אינו מונע RCE או הוצאת מידע. הפעילו גם משתמש שאינו root, הרשאות מזעריות, מערכת קבצים לקריאה בלבד כשאפשר, תעבורה יוצאת מוגבלת, seccomp ועדכונים בזמן; בדקו את הבקרות עבור היישום. [מקור 1] [מקור 2]
ההשפעה על DevOps ולמה זה חשוב לצוות
אימוץ הגישה ״בנו רחב, הפיצו מצומצם״ אינו רק דרך להציג מספרים קטנים במסוף. יש לו יתרונות מצטברים בכל מחזור אספקת התוכנה:
- עבודת העברה ב־CI/CD: פחות בתים נחוצים יכולים לצמצם דחיפה ומשיכה מהמאגר, אך מטמון שכבות, מהירות הרשת ואתחול היישום קובעים את ההשפעה בפועל. מדדו במקום להבטיח שיפור מדקות לשניות. [מקור 1]
- ניהול חולשות: הסרת חבילות לא נחוצות יכולה לצמצם ממצאים והיקף תחזוקה, אך לא נובעת מכך הפחתה קבועה של 95%. סרקו את התמונה הסופית בפועל ואת תלויות היישום, הפיקו SBOM והעריכו אפשרות ניצול; פחות ממצאים אינם מוכיחים שהיישום שנותר מאובטח. [מקור 1] [מקור 2]
- עלות תשתית: פחות מידע מאוחסן ומועבר עשוי להפחית חלק מהחיובים, בהתאם לתמחור הספק, לשימוש חוזר במטמון ולשמירת תמונות. גם זמן בנייה וסריקה כרוכים בעלויות, ולכן השוו את הצינור המלא במקום להניח חיסכון.
סיכום
בנייה מרובת שלבים היא דרך מעשית להפריד כלי בנייה מתוצרי ריצה. נהלּו את שני השלבים בבקרה, ודאו גרסאות נתמכות ו־digests, הגנו על הקשר הבנייה ובדקו את התנהגות התמונה הסופית ואת תלויותיה. אריזה מצומצמת מסייעת לאבטחה ולתפעול; היא אינה מבטיחה אותם. [מקור 1] [מקור 2]
איך נראית תצורת ה־Docker של הצוות שלכם? האם עברתם ל־Distroless, או שאתם עדיין מסתמכים על תמונות בסיס מנופחות? כתבו בתגובות או שתפו את המאמר עם מוביל ה־DevOps בצוות!
עד הפעם הבאה, בנו רחב והפיצו מצומצם.