3 שלב המיומנות

לארוז את האפליקציה — Dockerfile, docker init, ו-images קטנים

בפרק 2 למדתם להריץ images של אחרים — nginx בשלושים שניות, hello-world בלחיצה אחת. היום אתם עוברים את הרף האמיתי: לארוז את האפליקציה שלכם לתוך image שאתם בניתם. בלי לכתוב Dockerfile מאפס. בלי להבין כל שורה לפני שמתחילים. קודם docker init נותן לכם Dockerfile ראשון, מוכן לפרודקשן, לפי best practices של Docker עצמו. אחר כך אתם לומדים לקרוא אותו שורה-שורה, לסדר את ה-layers נכון כדי שעריכה של שורת קוד אחת לא תאלץ התקנה מלאה, להחליף את ה-base image לגרסה רזה יותר, ולהריץ את האפליקציה כ-user לא-root. בסוף הפרק תהיה לכם תמונה אמיתית של האפליקציה שלכם — image שאפשר לדחוף ל-registry, להריץ על שרת, ולשתף עם חבר בלי "אצלי זה עובד" אף פעם יותר.

מה תוכלו לעשות אחרי הפרק הזה
לפני שמתחילים
הפרויקט שלכם

אתם בעיצומו של הפרויקט המרכזי של הקורס: לקחת את האפליקציה שה-AI בנה לכם (web app + מסד-נתונים בהמשך) ולהפוך אותה ל-container שרץ בכל מקום. בפרק 1 הבנתם למה "רץ אצלי במחשב" הוא לא מספיק. בפרק 2 התקנתם את Docker, הרצתם nginx ב-browser, והכרתם את 6 הפעלים היומיומיים. בפרק הזה אתם עוברים מ-"להריץ images של אחרים" ל-"לבנות image משלי" — קודם עם docker init שעושה את העבודה הקשה, ואז עם הבנה של כל שורה ב-Dockerfile ושלושה שיפורים שמייצרים image קטן, בנייה מהירה, ו-container מאובטח יותר.

מה הלאה: בפרק 4 (נתונים וקונפיג) נשמור את המידע של האפליקציה עם volumes, נעביר API keys ב-runtime בלי לאפות אותם ל-image, ונלמד את הכלל הכי קריטי של הקורס — אף פעם לא לאפות סודות לתוך image (הטעות שיש ב-48% מאפליקציות AI). בפרק 5 נחבר את ה-app למסד-נתונים ב-compose.yaml אחד, ובפרק 6 נדחוף את ה-image ל-ghcr.io ונפרוס אותו על VPS או Coolify.

הפרק הזה מחולק לשני חצאים. החצי הראשון (סעיפים 1-6) נותן לכם image רץ: docker init, docker build, docker run. החצי השני (סעיפים 7-10) הופך את ה-image הזה לקטן, מהיר, ובטוח: בחירת base, layer ordering, non-root USER, multi-stage. הקפיצה התפיסתית של הפרק היא: אתם לא צריכים לכתוב Dockerfile מאפס, אבל אתם חייבים להיות מסוגלים לקרוא ולתקן אחד. ה-AI כותב הכי טוב שהוא יכול — אבל רוב הזמן הוא משתמש ב-base image שמנמוך מדי, לא מקבע גרסאות, ושוכח USER. אתם תדעו לזהות את זה בעיניים תוך 30 שניות.

בינוני 5 דקות הקדמה מתודולוגיה

מהרצת images של אחרים לבניית image משלכם — הקפיצה

בפרק 2 עשיתם משהו מאוד חשוב: הרצתם docker run nginx, ובעולם ה-Docker זה היה המקבילה של "לפתוח ספר שמישהו אחר כתב." אתם נהניתם מה-image שמישהו אחר בנה, אתם הרצתם אותו, אבל לא שיניתם בו כלום. זה כמו להזמין אוכל ממסעדה — טעים, מהיר, אבל אתם לא יודעים מה במתכון.

הפרק הזה הוא הרגע שבו אתם נכנסים למטבח. אתם הולכים לבנות image משלכם, עם הקוד שלכם, ה-dependencies שלכם, והגדרות שלכם. זה הצעד שהופך "משתמש Docker" ל"מי שיכול לפרוס אפליקציה אמיתית."

הגישה: קודם תנו לכלי לכתוב, אחר כך תלמדו לקרוא

רוב המדריכים ל-Docker מתחילים בזה: "פתחו עורך, כתבו FROM python:3.11, הוסיפו RUN pip install …, סיימתם." זה הגיוני למי שמבין תכנות, אבל עבור Vibe Coder שלא כתב Dockerfile מעולם — זה מבלבל. אתם רואים שורה אחת ולא יודעים מאיפה להתחיל לשאול שאלות.

הגישה שלנו הפוכה: קודם נותנים ל-docker init לכתוב את ה-Dockerfile, ואז לומדים לקרוא מה הוא כתב. זה ה-on-ramp הכי טוב שיש: רואים דוגמה טובה לפני שמנסים לכתוב לבד, ועדיין לומדים את כל היסודות. זה כמו לקרוא ספר מתכונים מקצועי לפני שמבשלים סושי — לא חייבים להבין הכל בקריאה הראשונה, אבל כבר יודעים איך סושי אמור להיראות.

מה docker init עושה (ומה לא)

docker init הוא פקודה אינטראקטיבית (כלומר — היא שואלת אתכם שאלות) שמזהה את השפה של הפרויקט שלכם (Python, Node.js, Go, Java, ASP.NET/C#, Rust, PHP+Apache), יוצרת ארבעה קבצים לפי best practices של Docker עצמו:

הוא לא עושה שלושה דברים שאנשים מצפים ממנו: הוא לא בונה image, לא מריץ container, ולא מעלה את הקוד ל-registry. הוא רק יוצר את הקבצים. את השאר אתם עושים — ובשלב הזה תלמדו לעשות את זה נכון.

Do Now — 2 דקות (הכנה לפני שנתחיל)

לפני שאתם מריצים docker init, פתחו terminal ובדקו שני דברים:

  1. docker --version — מחזיר מספר גרסה. אם לא, Docker Desktop לא רץ.
  2. pwd ו-ls — אתם באמת בתיקיית הפרויקט שלכם? אם לא, cd ~/projects/your-app (או הנתיב שלכם).

תוצאה צפויה: גרסת Docker מודפסת (למשל Docker version 27.x.x, build xxxxx), ורשימת קבצים של הפרויקט שלכם — package.json / requirements.txt / app.py / קבצי קוד אחרים. אם הרשימה ריקה או שאתם לא בתיקייה הנכונה — עצרו ותקנו. docker init יוצר קבצים בתיקייה הנוכחית, ואתם לא רוצים שהוא יזרוק Dockerfile בתיקיית הבית שלכם.

בינוני 12 דקות build docker init

docker init בפעולה — ה-Dockerfile הראשון שלכם

בסעיף הזה אתם הולכים להריץ docker init בפעם הראשונה, לענות על כמה שאלות שלו, ולראות איך הוא יוצר Dockerfile מוכן לפרודקשן בלי שכתבתם שורה. אחר כך תריצו docker build, תראו את ה-image נבנה, ותריצו אותו.

שלב 1: הרצת docker init

בתיקיית הפרויקט, הריצו:

docker init

הפלט ייראה בערך כך (השאלות תלויות בשפה שזיהה; להלן דוגמה לפרויקט Python):

? Welcome to the Docker Init CLI!
  Your project contains:
  - Python (3.11)
  - pip requirements

  ? Which application platform does your project use?  [Use arrows to move]
  > Python
    Node
    Go
    Java
    ...
? What version of Python do you want to use?  > 3.11
? What port does your server listen on?  > 3000
? What command do you want to use to start the app?  > python app.py
? Would you like to use a .dockerignore?  > Yes

בחרו את הברירות-מחדל בכל שאלה. אל תנסו לחכם יותר מדי בסבב הראשון — אנחנו נשפר הכל אחר כך.

בסוף הפלט תראו:

✔ Created → Dockerfile
✔ Created → compose.yaml
✔ Created → .dockerignore
✔ Created → README.Docker.md

→ To start your application, run: docker compose up --build

זה הכל. יש לכם Dockerfile חדש בתיקייה, והוא לא דרס שום קובץ קיים (אלא אם כן היו Dockerfile/compose קודמים — ובמקרה כזה הוא ישאל אתכם לפני דריסה). הוא גם זיהה אוטומטית את requirements.txt שלכם ויצר Dockerfile שמתקין אותו.

שלב 2: להציץ ב-Dockerfile שנוצר

לפני שאתם בונים, פתחו את ה-Dockerfile בעורך הטקסט. הוא ייראה בערך כך (גרסה לפרויקט Python):

# syntax=docker/dockerfile:1
FROM python:3.11-slim

WORKDIR /app

# Install dependencies separately for better layer caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy the rest of the application
COPY . .

# Run the application
CMD ["python", "app.py"]

קצר. ברור. כל שורה עושה דבר אחד. עכשיו, אל תנסו להבין הכל — את זה נעשה בסעיף הבא. רק ראו שיש 4-5 שורות, שהן מתחילות ב-FROM ומסתיימות ב-CMD, ושהמבנה הגיוני: בוחרים בסיס, מתקינים dependencies, מעתיקים קוד, מריצים.

שלב 3: לבנות את ה-image

עכשיו בונים. זו הפעם הראשונה, אז זה ייקח זמן (Docker צריך להוריד את ה-base image, להריץ את כל ה-RUN, להעתיק קבצים). הריצו:

docker build -t myapp .

המשמעות: "תבנה image, תן לו תג (tag — שם שאתם בוחרים) בשם myapp, תיקיית ההקשר (build context) היא הנוכחית (.)."

הפלט יראה כך:

[+] Building 45.2s (9/9) FINISHED
 => [internal] load build definition from Dockerfile
 => => transferring dockerfile: 132B
 => [base 1/2] FROM docker.io/library/python:3.11-slim
 => => transferring dockerfile: 132B
 => => resolving metadata for docker.io/library/python:3.11-slim@sha256:...
 => [base 2/2] WORKDIR /app
 => [deps] COPY requirements.txt .
 => [deps] RUN pip install --no-cache-dir -r requirements.txt
 => [stage-1 2/3] COPY . .
 => [stage-1 3/3] CMD ["python", "app.py"]
 => naming to docker.io/library/myapp

ה-45.2s זה הזמן שלוקח להוריד את ה-base + להתקין dependencies בפעם הראשונה. ריצות הבאות יהיו הרבה יותר מהירות (נדבר על זה בסעיף 5).

שלב 4: להריץ את ה-image כ-container

עכשיו ה-image שלכם יושב ב-Docker. תראו אותו ברשימה:

docker images

הפלט יראה משהו כמו:

REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
myapp        latest    a1b2c3d4e5f6   2 minutes ago   180MB
python       3.11-slim x9y8z7w6v5u4   3 days ago      130MB

ה-image myapp שלכם (180MB) קיים. עכשיו תריצו אותו. נניח שהאפליקציה שלכם מאזינה על פורט 3000:

docker run -p 3000:3000 --name myapp-container myapp

המשמעות: "תריץ את myapp, תקרא ל-container בשם myapp-container, ותחבר את פורט 3000 של המארח לפורט 3000 של ה-container."

אם הכל הלך חלק, תראו את הפלט של האפליקציה שלכם (למשל, "Uvicorn running on http://0.0.0.0:3000") ותוכלו לפתוח את http://localhost:3000 ב-browser.

Do Now — 3 דקות (אימות הצלחה)

אחרי docker run, פתחו terminal שני (אל תסגרו את הראשון — ה-container רץ שם) והריצו:

  1. docker ps — תראו את myapp-container ברשימה. עמודת STATUS צריכה להיות Up X seconds.
  2. curl http://localhost:3000 (או פתחו ב-browser) — אם האפליקציה עונה, אתם ב-90% מהדרך.

תוצאה צפויה: docker ps מציג את ה-container רץ, וה-browser מציג את האפליקציה. אם לא — עצרו ובדקו: (1) האם האפליקציה מאזינה על 0.0.0.0 ולא 127.0.0.1? (2) האם -p 3000:3000 תואם לפורט שהאפליקציה מאזינה עליו? (3) מה docker logs myapp-container אומר?

בינוני 10 דקות קריאה תחביר

לקרוא Dockerfile שורה-שורה

עכשיו כשיש לכם Dockerfile אמיתי מול העיניים, בואו נעבור עליו שורה-שורה. כל שורה היא instruction (הוראה), ויש 13 הוראות סטנדרטיות ב-Docker. בפועל תשתמשו ב-6-7 מהן. הנה ה-Dockerfile המלא שוב, והפעם עם הסבר לכל שורה:

שורה 1: # syntax=docker/dockerfile:1

זו comment מיוחדת שמופיעה בראש ה-Dockerfile, והיא אומרת ל-Docker להשתמש בגרסה 1 של תקן ה-Dockerfile. היא מופיעה אוטומטית בכל Dockerfile שנוצר על ידי docker init מאז 2023, והיא מאפשרת תכונות מודרניות (multi-stage מתקדם, build secrets, mounts). אל תמחקו אותה.

שורה 2: FROM python:3.11-slim

ההוראה: FROM קובעת את base image — תמונת הבסיס שעליה הכל נבנה. ה-base הוא מערכת הפעלה מינימלית + שפת תכנות + כלי בסיס. python:3.11-slim אומר: "קח את ה-image הרשמי של Python גרסה 3.11, גרסת slim (רזה), מ-Docker Hub."

חשוב: כל Dockerfile חייב להתחיל ב-FROM. אין יוצאים מן הכלל.

שורה 3: WORKDIR /app

ההוראה: WORKDIR קובעת את תיקיית העבודה בתוך ה-container. מכאן והלאה, כל פקודה שלא תציין נתיב מלא תרוץ ב-/app. זה כמו cd /app בלינוקס, אבל נשמר בכל layer. בלי WORKDIR, ברירת המחדל היא / (שורש המערכת), וזה לא נוח.

שורה 5: COPY requirements.txt .

ההוראה: COPY מעתיקה קובץ מהמארח (המחשב שלכם) לתוך ה-container. המבנה: COPY <source> <destination>. . ביעד = "לתיקיית העבודה" (כלומר /app, מה שהגדרנו ב-WORKDIR).

למה רק requirements.txt ולא הכל? זה החלק החשוב. זה הקסם שמאיץ את ה-rebuild פי 10. נדבר על זה בסעיף 5. בקצרה: requirements.txt משתנה לעתים רחוקות, הקוד שלכם משתנה כל הזמן. אם נעתיק קודם את ה-requirements, ה-layer של התקנת ה-dependencies יישמר ב-cache גם כשהקוד משתנה.

שורה 6: RUN pip install --no-cache-dir -r requirements.txt

ההוראה: RUN מריצה פקודה בזמן הבנייה (build time). הפלט שלה (קבצים שהותקנו) נשמר ב-layer. pip install --no-cache-dir -r requirements.txt מתקין את כל ה-dependencies מ-requirements.txt. --no-cache-dir אומר ל-pip לא לשמור cache של הורדות (חוסך מקום ב-image).

חשוב: RUN שונה מ-CMD ו-ENTRYPOINT. RUN רץ פעם אחת בזמן הבנייה ויוצר קבצים ב-image. CMD/ENTRYPOINT רצים בכל פעם שה-container עולה.

שורה 9: COPY . .

ההוראה: מעתיקה את כל השאר מהמארח ל-container. ה-. הראשון = "מהתיקייה הנוכחית במארח", ה-. השני = "לתיקיית העבודה ב-container" (/app).

זו השורה שמעתיקה את הקוד שלכם. רק עכשיו, אחרי שה-dependencies כבר מותקנים. זה הסדר הנכון.

שורה 12: CMD ["python", "app.py"]

ההוראה: CMD קובעת את ברירת המחדל לפקודה שרצה כשה-container עולה. התחביר הזה (רשימה של מחרוזות ב-JSON-style) הוא ה-exec form. הוא מעביר את הפקודה ישירות למערכת ההפעלה, בלי shell wrapper.

CMD ["python", "app.py"] אומר: כשה-container רץ, הרץ python app.py.

מה לא ב-Dockerfile?

שלוש הוראות נפוצות שלא ראינו כאן, אבל תראו בפרויקטים רבים:

Do Now — 5 דקות (להוסיף EXPOSE ולראות שאין הבדל)

בואו נראה את המלכודת של EXPOSE בעיניים. הוסיפו את השורה הזאת ל-Dockerfile אחרי FROM (או בכל מקום):

EXPOSE 3000

בנו מחדש: docker build -t myapp .. הריצו: docker run --name myapp-no-port myapp (בלי -p!). נסו curl http://localhost:3000 — לא עובד. עכשיו עצרו (docker stop myapp-no-port) והריצו שוב: docker run -p 3000:3000 --name myapp-with-port myapp — עובד.

תוצאה צפויה: שתי הריצות יצליחו לבנות ולהריץ, אבל רק הריצה השנייה (עם -p) תהיה נגישה מהדפדפן. EXPOSE הוא תיעוד בלבד. זכרו את זה — זו טעות שכל מתחיל עושה.

בינוני 8 דקות build BuildKit

docker build -t myapp . — תג, build context, ו-BuildKit

הפקודה docker build היא הפעולה הכי נפוצה שלכם אחרי docker run. בואו נפענח את שלושת החלקים שלה: התג, הנקודה, וה-builder שמאחור.

התג (-t myapp)

-t מגיע מ-tag — שם שאתם נותנים ל-image. בלי -t, Docker ייתן ל-image שם אקראי כמו sha256:a1b2c3... ותצטרכו לכתוב את כל ה-hash כדי להריץ אותו. תיוג נכון הופך את החיים להרבה יותר קלים.

תג יכול לכלול גם גרסה:

docker build -t myapp:v1.0 .
docker build -t myapp:latest .
docker build -t myapp:2026-06-23 .

אם לא ציינתם תג, Docker מוסיף :latest אוטומטית. :latest הוא לא "הגרסה הכי חדשה" — הוא רק "התג ברירת-מחדל." זה מלכודת שנדבר עליה בסעיף 7.

ה-build context (.)

הנקודה . היא ה-build context — תיקיית הבסיס ש-Docker מעתיק ממנה קבצים לתוך ה-image. ה- COPY . . ב-Dockerfile מעתיק מהתיקייה הזו.

זה המקום שבו .dockerignore הופך קריטי. אם התיקייה שלכם מכילה 5GB של node_modules, datasets, .git, או סרטונים — Docker ינסה להעתיק את הכל לפני שהוא בכלל מתחיל לבנות. ה-build יהיה איטי, ה-image יהיה ענק, ואם יש לכם .env בתיקייה — הוא ייכנס ל-image. נדבר על .dockerignore בסעיף 6.

BuildKit — ה-builder המודרני

מאז 2023, BuildKit הוא ה-builder הדיפולטי ב-Docker Desktop וב-Docker Engine. אתם לא צריכים להגדיר שום דבר — docker build פשוט משתמש בו. BuildKit מאיץ בניות עם cache חכם יותר, תומך ב-multi-platform builds (נדבר על זה בפרק 6), ומציג פלט מסודר יותר מה-builder הישן.

יש שני דברים שכדאי לדעת על BuildKit:

מה קורה כשאתם בונים?

הריצה הראשונה ארוכה (45 שניות בדוגמה שלנו). הריצה השנייה — בלי שינויים — תיקח שנייה אחת, כי BuildKit מזהה שאין שינויים ומשתמש ב-cache של כל ה-layers. עריכת שורת קוד אחת תגרור — תלוי בסדר ה-Dockerfile — rebuild של layer אחד או כל ה-image. בסעיף הבא נלמד איך לסדר את ה-Dockerfile כך שעריכה של שורת קוד תגרור rebuild של שכבה אחת בלבד.

Do Now — 2 דקות (לראות cache בפעולה)

הריצו docker build -t myapp . פעמיים ברצף. הריצה הראשונה לוקחת 45 שניות. הריצה השנייה צריכה להסתיים תוך 1-2 שניות, עם הודעות כמו => CACHED ליד כל שלב.

תוצאה צפויה: שתי ההרצות מצליחות, אבל השנייה היא כמעט מיידית. הפלט של הריצה השנייה יראה בערך:

[+] Building 0.8s (9/9) FINISHED
 => [internal] load build definition from Dockerfile    0.0s
 => => transferring dockerfile: 132B                    0.0s
 => [base] FROM docker.io/library/python:3.11-slim       0.0s
 => [base] WORKDIR /app                                 0.0s
 => [deps] COPY requirements.txt .                      0.0s
 => [deps] RUN pip install --no-cache-dir -r ...        0.0s
 => CACHED                                              0.0s
 ...

המילה CACHED היא הסימן שלכם: "השכבה הזו לא השתנתה, השתמשתי ב-cache מהריצה הקודמת."

בינוני 10 דקות אופטימיזציה cache

Image layers ו-build cache — הכלל שמאיץ rebuild פי עשרות

זה הסעיף הכי חשוב בפרק. אם תזכרו רק דבר אחד מהפרק — זה. הסדר של השורות ב-Dockerfile קובע כמה מהר תוכלו לבנות מחדש אחרי כל שינוי בקוד.

מה זה layer?

כל instruction ב-Dockerfile יוצרת layer (שכבה). layer הוא snapshot של מערכת הקבצים אחרי שההוראה רצה. כל ה-layers מונחים זה על זה, וביחד הם יוצרים את ה-image. כשמריצים container מ-image, Docker מרכיב את כל ה-layers למערכת קבצים אחת עם union file system — שכבות לקריאה-בלבד שמתאחדות למערכת אחת.

למה זה חשוב? כי Docker שומר cache (זיכרון-ביניים) של כל layer. אם הוראה לא השתנתה, Docker לא מריץ אותה שוב — הוא משתמש ב-layer הקיים. הבעיה: אם הוראה אחת השתנתה, Docker צריך לבנות מחדש את כל ההוראות שאחריה.

הכלל שמאיץ rebuild פי 10: dependencies לפני הקוד

הנה ההגיון. הקוד שלכם משתנה כל הזמן — אתם עורכים קובץ, בודקים, מתקנים, חוזרים. ה-dependencies (packages) משתנים לעתים רחוקות — אולי פעם ביום, פעם בשבוע, פעם בחודש. אם תסדרו את ה-Dockerfile כך שהקוד ייכנס לפני ה-dependencies, כל עריכה תבטל את ה-cache של ה-dependencies. אם תסדרו כך שה-dependencies ייכנסו לפני הקוד, רק ה-layer של הקוד ייבנה מחדש.

הדוגמה הרעה (הסדר שרוב ה-AI-ים כותבים):

FROM python:3.11-slim
WORKDIR /app
COPY . .                    # ← הכל נכנס קודם
RUN pip install --no-cache-dir -r requirements.txt  # ← dependencies רק אחר כך

תוצאה: כל עריכה של app.py מבטלת את ה-layer של pip install, כי הוא תלוי ב-COPY . . שהשתנה. pip install רץ מחדש = 3-5 דקות. כל פעם.

הדוגמה הנכונה (הסדר ש-docker init יוצר):

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .     # ← רק requirements נכנס
RUN pip install --no-cache-dir -r requirements.txt  # ← dependencies מותקנים
COPY . .                    # ← הקוד נכנס רק עכשיו

תוצאה: עריכה של app.py מבטלת רק את ה-layer של COPY . .. pip install נשאר ב-cache. ריצה מחדש = 5-10 שניות.

המספרים בפועל

בפרויקט Python עם 50 packages ב-requirements.txt, ההבדל הוא:

זה ההבדל בין "Docker איטי, אני שונא את זה" לבין "Docker מהיר, אני אוהב את זה."

בעבור Node.js: package.json לפני הכל

אותו עיקרון, רק עם npm:

FROM node:22-slim
WORKDIR /app
COPY package*.json ./       # ← רק package.json/package-lock.json
RUN npm ci --omit=dev       # ← dependencies מותקנים
COPY . .                    # ← הקוד נכנס

שימו לב ל-npm ci במקום npm install. ci מתקין בדיוק לפי ה-lock file, מהיר יותר, ועקבי יותר בין סביבות. בפרודקשן תמיד npm ci, לא npm install.

בעבור frameworks אחרים

העיקרון זהה תמיד:

  1. FROM — בסיס
  2. WORKDIR — תיקיית עבודה
  3. COPY <manifest> (requirements.txt / package.json / go.mod / pom.xml) — רק קובץ ה-dependencies
  4. RUN <install> — התקנה
  5. COPY . . — שאר הקוד
  6. CMD — פקודת הפעלה

את העיקרון הזה תראו בכל Dockerfile מקצועי, בכל שפה.

Do Now — 10 דקות (למדוד את ההפרש בעצמכם)

קחו את ה-Dockerfile הנוכחי שלכם (או צרו אחד חדש עם docker init בפרויקט גדול). אתם הולכים למדוד שני תרחישים.

  1. תרחיש א: Dockerfile "רע" — העתיקו את ה-Dockerfile לגרסה עם COPY . . לפני RUN pip install. בנו: time docker build -t myapp-bad .. רשמו את הזמן.
  2. תרחיש ב: Dockerfile "טוב" — החזירו את הסדר הנכון (dependencies לפני קוד). בנו שוב: time docker build -t myapp-good .. רשמו.
  3. עכשיו ערכו שורת קוד אחת (למשל, הוסיפו רווח ב-app.py). בנו שוב את שניהם. רשמו.

תוצאה צפויה: הריצה הראשונה של שניהם לוקחת בערך אותו זמן (לא משנה הסדר בריצה הראשונה, כי אין cache). ההפרש האמיתי מופיע בריצה השנייה: ה-Dockerfile ה"רע" יבנה מחדש את הכל (כולל pip install). ה-Dockerfile ה"טוב" יבנה רק את ה-layer של הקוד. ההפרש: 3-5 דקות מול 5-10 שניות.

בינוני 6 דקות אבטחה ביצועים

.dockerignore — רשת הביטחון שאסור לדלג עליה

קובץ .dockerignore הוא .gitignore לבנייה — הוא אומר ל-Docker מה לא להעתיק לתוך ה-image. docker init יוצר אחד אוטומטית, אבל רוב המתחילים מוחקים אותו, מתעלמים ממנו, או לא יודעים שהוא קיים. זו טעות.

מה הבעיה בלי .dockerignore?

כשאתם מריצים docker build ., Docker שולח את כל תוכן התיקייה ל-daemon שלו לפני שהוא מתחיל לבנות. זה נקרא build context. אם התיקייה שלכם מכילה 2GB של node_modules, 500MB של .git, 200MB של venv, וקובץ .env עם API keys — הכל נשלח. אחר כך Docker בודק מה ב-Dockerfile צריך להעתיק לתוך ה-image, אבל הזמן שלוקח לשלוח הוא כבר אבוד.

ובעיה גרועה יותר: אם .env נמצא בתיקייה ולא ב-.dockerignore, הוא עלול להיכנס ל-image אם ה-Dockerfile שלכם עושה COPY . .. הסודות שלכם — בתוך ה-image, גלויים לכל מי שמושך את ה-image. זה אסון אבטחה.

מה docker init שם ב-.dockerignore

הנה דוגמה טיפוסית ל-.dockerignore ש-docker init יוצר:

# Git
.git
.gitignore

# Python
__pycache__
*.pyc
*.pyo
*.pyd
.Python
venv
.venv
env
*.egg-info/

# Node
node_modules
npm-debug.log

# IDE / Editor
.vscode
.idea
*.swp
.DS_Store

# Build artifacts
dist
build
*.log

# Secrets
.env
.env.*
secrets/

# Docker
Dockerfile
docker-compose*.yml

זה סבבה להתחלה. אם יש לכם תיקיות גדולות אחרות (datasets, model weights, סרטונים) — הוסיפו אותן בעצמכם.

הבדיקה הפשוטה: כמה גדול ה-build context שלכם?

תריצו:

docker build -t myapp . 2>&1 | head -5

השורה הראשונה תראה משהו כמו:

[+] Building 12.4s (5/9)
 => [internal] load build context
 ...
 => transferring context: 2.3GB                            4.2s

השורה transferring context: 2.3GB אומרת לכם ש-Docker העתיק 2.3GB לפני שהתחיל. אם זה גדול מ-100MB, סימן שמשהו צריך להיכנס ל-.dockerignore.

הוכחה: .env לא נכנס ל-image

אחרי שיש לכם .dockerignore עם .env, תוכלו לאמת:

docker run --rm myapp ls -la /app | grep -i env

הפלט לא יכלול .env. הקובץ לא נכנס ל-image. הסודות שלכם בטוחים.

Do Now — 4 דקות (להפוך את ה-.dockerignore לקובץ משלכם)

פתחו את .dockerignore ש-docker init יצר, והוסיפו לפחות 3 שורות שמתאימות לפרויקט שלכם. רעיונות:

תוצאה צפויה: docker build יהיה מהיר יותר, ה-image יהיה קטן יותר, ו-transferring context יראה מספר קטן בהרבה. תרגישו את ההבדל גם ב-build הראשון, לא רק בשני.

בינוני 10 דקות אופטימיזציה החלטה

בחירת base image — full / slim / alpine / distroless

השורה הראשונה ב-Dockerfile, FROM, היא ההחלטה הכי משמעותית לגודל ה-image. בואו נעבור על ארבע האפשרויות העיקריות, ואז נראה איך לבחור.

ארבעת ה-base images העיקריים

1. :full (או :latest ללא סיומת) — הדביאן המלא

זה ה-base הדיפולטי של רוב ה-images הרשמיים. הוא כולל את מערכת ההפעלה Debian (או Ubuntu) המלאה, עם כל הכלים, הספריות, וה-utils. גודל משוער: python:3.11 ~1.0GB, node:22 ~1.0GB. זה התנאי הכי בטוח לתאימות — כל חבילה native (numpy, pandas, scipy) תעבוד.

2. :-slim — דביאן מצומצם

אותה מערכת הפעלה, אבל בלי חבילות מיותרות. רוב הדברים שאתם צריכים נשארים, אבל docs, man pages, ו-caches מוסרים. גודל משוער: python:3.11-slim ~130MB, node:22-slim ~220MB. הברירת-מחדל המומלצת לרוב ה-Vibe Coders — תאימות גבוהה, גודל סביר.

3. :-alpine — Alpine Linux (מינימליסטי)

Alpine Linux הוא הפצת לינוקס שתוכננה להיות קטנה. הוא משתמש ב-musl במקום glibc (שתי ספריות C שונות) וב-BusyBox במקום כלי GNU. גודל משוער: python:3.11-alpine ~50MB, node:22-alpine ~150MB. הקטן ביותר, אבל יש לו מלכודת.

4. distroless — בלי shell, בלי package manager

base image שמגיע מ-Google, שכולל רק את ה-runtime הנדרש (למשל, רק את Python עצמה) — בלי shell, בלי apt, בלי כלי עזר. גודל משוער: gcr.io/distroless/python3 ~50MB. הכי בטוח כי אין דרך להיכנס פנימה ולהריץ פקודות, אבל גם אי אפשר להיכנס פנימה (אין shell). לא מתאים למתחילים — שמורו את זה לפרויקטים שכבר בפרודקשן.

טבלת ההחלטה המעשית

Base גודל משוער (Python 3.11) תאימות מתי לבחור
python:3.11 (full) ~1.0GB גבוהה מאוד פרויקט legacy, תלוי ב-binary חיצוני, או C extension ישן
python:3.11-slim ~130MB גבוהה ברירת-מחדל מומלצת — רוב הפרויקטים, כולל אלה עם numpy/pandas
python:3.11-alpine ~50MB בינונית (musl) אפליקציות pure-Python, Go, Rust — בלי native dependencies
distroless/python3 ~50MB גבוהה (glibc) פרודקשן אמיתי שצריך אבטחה מקסימלית; לא למתחילים

הערה: הגדלים הם משוערים ומשתנים עם כל גרסה של ה-base. בדקו את הגודל המדויק עם docker images אחרי הבנייה.

מלכודת alpine: musl מול glibc

Alpine לא משתמש ב-glibc, הספרייה הסטנדרטית של C שרוב החבילות בנויות נגדה. הוא משתמש ב-musl, שהיא תואמת אבל לא זהה. ההבדל מתגלה כשאתם מתקינים חבילה שכוללת C code שנבנה מראש (binary wheel) — למשל numpy, pandas, scipy, psycopg2.

התסמין: pip install נופל עם שגיאות כמו undefined symbol: __libc_start_main או Incompatible architecture. הפתרון: חזרה ל--slim.

כלל אצבע: אם הפרויקט שלכם משתמש ב-numpy/pandas/scipy, או בכל חבילה שמושכת C extension — התחילו מ--slim. אל תנסו להיות גיבורים עם alpine בלי סיבה.

מלכודת :latest — builds לא-משוחזרים

כשאתם כותבים:

FROM node:latest

אתם אומרים: "תמיד תשתמש בגרסה הכי חדשה של node." מחר, כש-Docker Hub יעלה גרסה חדשה של node (למשל 23), ה-image שלכם ייבנה אחרת. התנהגות שונה. אולי קוד שעבד אתמול יישבר היום. "Works on my machine" חוזר, רק שעכשיו זה תלוי ביום.

הפתרון: תמיד לקבע גרסה. גם ל-base:

FROM node:22-slim     # לא node:latest
FROM python:3.11-slim # לא python:latest
FROM postgres:16-alpine  # לא postgres:latest

וגם ל-image שלכם:

docker build -t myapp:v1.0.3 .  # לא רק myapp
Do Now — 5 דקות (להחליף base ולמדוד)

ב-Dockerfile שלכם, החליפו את ה-base לגרסה רזה יותר. למשל:

בנו מחדש: docker build -t myapp-slim .. הריצו: docker images | grep myapp והשוו את ה-SIZE עם ה-image הקודם.

תוצאה צפויה: קיצוץ של 60-80% בגודל. מ-image של 1GB תרדו ל-200-300MB. אם האפליקציה רצה באותה צורה, סימן שלא היו לכם תלויות ב-binary חיצוני שחייב את ה-full base. אם היא קורסת — חזרו ל-full ותחקרו למה.

בינוני 7 דקות תחביר הוראות

CMD מול ENTRYPOINT — מי מגדיר את הפקודה

שתי ההוראות האלה קובעות מה רץ כשה-container עולה, ולמתחילים זה הכי מבלבל. ההבדל פשוט כשמבינים אותו.

CMD — ברירת המחדל

CMD קובע את הפקודה אם לא העברתם אחת. אם תריצו docker run myapp (בלי פקודה בסוף), Docker ישתמש ב-CMD. אם תריצו docker run myapp bash, Docker יתעלם מ-CMD ויריץ bash.

CMD ["python", "app.py"]

הצורה הזו (מערך JSON) נקראת exec form. היא המומלצת.

ENTRYPOINT — הפקודה הקבועה

ENTRYPOINT קובע את הפקודה תמיד. גם אם תעבירו פקודה אחרת, היא תתווסף לאחר ה-ENTRYPOINT בתור arguments.

ENTRYPOINT ["python", "app.py"]

עכשיו docker run myapp יריץ python app.py, ו-docker run myapp --help יריץ python app.py --help (ה-arguments מהפקודה מתווספים).

אותה פקודה, שתי התנהגויות — דוגמה צד-בצד

הדרך הכי מהירה להפנים את ההבדל היא לראות מה קורה כשמריצים בדיוק את אותן פקודות docker run מול שני ה-Dockerfiles. נניח ששני ה-images בנויים מאותו קוד, רק שורת הסיום שונה:

הפקודה שהרצתם עם CMD ["python", "app.py"] עם ENTRYPOINT ["python", "app.py"]
docker run myapp רץ python app.py רץ python app.py
docker run myapp --help מתעלם מ-CMD, מנסה להריץ את התוכנית --helpקורס (אין כזו תוכנית) רץ python app.py --help ← ה---help מתווסף כ-argument
docker run myapp bash מתעלם מ-CMD, נותן לכם shell של bash בתוך ה-container (שימושי לדיבוג) רץ python app.py bash ← מעביר את המילה bash כ-argument לתוכנית

שימו לב לשורה האמצעית: עם CMD אתם דורסים את כל הפקודה; עם ENTRYPOINT אתם מוסיפים arguments. זו בדיוק הסיבה ש-docker run myapp bash נותן לכם shell לדיבוג רק כשמשתמשים ב-CMD — טריק שתשתמשו בו המון.

מתי משתמשים בכל אחד?

בפרויקט vibe-coded רגיל, תשתמשו ב-CMD. זה מה ש-docker init יוצר. ENTRYPOINT הוא לכלים מורכבים יותר.

מלכודת: CMD python app.py (בלי מערך)

הצורה הזו:

CMD python app.py

נקראת shell form. היא לא שווה-ערך ל-CMD ["python", "app.py"]. ב-shell form, Docker עוטף את הפקודה ב-/bin/sh -c, מה שאומר שהיא רצה דרך shell. זה בעייתי כי:

תמיד השתמשו ב-exec form (מערך JSON):

CMD ["python", "app.py"]

זה הסיבה ש-docker init יוצר את הצורה הזו מלכתחילה.

Do Now — 3 דקות (לראות override של CMD)

הריצו:

docker run --rm myapp sh

זה אמור לפתוח shell בתוך ה-container (במקום להריץ את python app.py). הסיבה: sh בסוף דורס את ה-CMD. כתבו ls ותראו את הקבצים של האפליקציה שלכם. exit לצאת.

תוצאה צפויה: אתם בתוך shell של ה-container, רואים את הקבצים ב-/app. זו דרך מצוינת לאבחן "למה זה לא עובד" — אתם נכנסים פנימה, מסתכלים, מתקנים, יוצאים. תרגיל 4 ישתמש בזה.

בינוני 8 דקות אופטימיזציה מתקדם

multi-stage build בעדינות — ה-80% קיצוץ

multi-stage build הוא טכניקה שמאפשרת לכם להשתמש בכמה FROM באותו Dockerfile. stage (שלב) אחד בונה את הקוד, stage שני רץ בפרודקשן — ורק הקבצים הסופיים עוברים הלאה. התוצאה: image שמכיל רק מה שצריך לרוץ, בלי הכלים שהיו נדרשים לבנייה.

למה זה חשוב?

בפרויקט Go, למשל, צריך compiler שמתקפל ב-200MB. אחרי הקומפילציה, ה-binary שלכם הוא 15MB. בלי multi-stage, ה-image שלכם יכיל גם את ה-compiler וגם את ה-binary. עם multi-stage, ה-image מכיל רק את ה-binary.

בפרויקט Node.js שמקמפל TypeScript, צריך את כל ה-dev dependencies. בפרודקשן אתם צריכים רק את ה-runtime ואת ה-build artifacts.

הדוגמה הקלאסית: Go

# שלב 1: builder — כבד, מקמפל
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o myapp

# שלב 2: runtime — רזה, רק הבינארי
FROM alpine:3.20
WORKDIR /app
COPY --from=builder /app/myapp .
EXPOSE 8080
CMD ["./myapp"]

ה-AS builder נותן שם לשלב. ה-COPY --from=builder בשלב השני מעתיק את myapp מהשלב הראשון. השלב הראשון (200MB) לא נכלל ב-image הסופי — רק השלב השני (15MB).

הדוגמה לפייתון — מתי זה עוזר?

פייתון לא מקמפלת (פחות או יותר), אז multi-stage פחות קריטי. אבל יש מקרים שבהם זה עוזר:

ה-django + node המשולב

תרחיש נפוץ ל-Vibe Coder: backend Python + frontend Node. ה-Dockerfile יכול להיראות כך:

# שלב 1: בניית ה-frontend
FROM node:22-slim AS frontend-builder
WORKDIR /frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ .
RUN npm run build

# שלב 2: ה-Python backend
FROM python:3.11-slim
WORKDIR /app
COPY backend/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY backend/ .
# העתקת ה-frontend הבנוי
COPY --from=frontend-builder /frontend/dist /app/static/
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

image סופי מכיל: Python + Django + ה-frontend הבנוי. בלי node_modules (עשרות MB), בלי כלי build של Node.

מתי לא להשתמש ב-multi-stage

אם הפרויקט שלכם pure-Python או pure-Node, בלי build step חיצוני — multi-stage הוא overkill. ה-Dockerfile הרגיל עם base רזה מספיק טוב. multi-stage הוא שימושי כשיש שני עולמות שצריך להפריד: כלי build כבד, ו-runtime קל.

Do Now — 3 דקות (לזהות הזדמנות multi-stage בפרויקט שלכם)

ענו על שלוש שאלות על הפרויקט שלכם:

  1. האם יש שלב build שלא נדרש ב-runtime? (TypeScript compilation, bundling, minification)
  2. האם יש חבילה כבדה שמשמשת רק בזמן build? (dev dependencies ב-npm, test frameworks)
  3. האם יש לכם כלי חיצוני שצריך לבנות assets?

אם עניתם "כן" על אחת מהן, multi-stage יכול לחסוך לכם 50-200MB. אם "לא" על כולן, הישארו עם Dockerfile רגיל.

בינוני 6 דקות אבטחה hardening

non-root USER — קו ההגנה הראשון

ברירת המחדל של images היא לרוץ כ-root. זה אומר שאם תוקף מוצא חור אבטחה באפליקציה שלכם, הוא מקבל root בתוך ה-container. ואם הוא מצליח לברוח מה-container (container escape), הוא מקבל root על המארח.

התיקון: להוסיף user לא-root ולהריץ את האפליקציה תחתיו.

איך מוסיפים USER?

שתי שורות ב-Dockerfile. הראשונה יוצרת את ה-user, השנייה מחליפה אליו:

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

# יצירת user לא-root
RUN useradd --create-home --shell /bin/bash appuser
USER appuser

CMD ["python", "app.py"]

useradd --create-home --shell /bin/bash appuser יוצר user חדש בשם appuser עם תיקיית בית ו-shell. USER appuser מחליף את המשתמש הפעיל ל-user החדש. מכאן והלאה, כל הפקודות רצות תחת appuser.

למה זה עובד נגד חלק מהמתקפות

אם תוקף מנצל חור ב-Python או ב-Node ומקבל גישה ל-shell בתוך ה-container, הוא מקבל user רגיל — לא root. הוא לא יכול:

זה לא פתרון מושלם (אם הוא כבר בתוך ה-container, הוא יכול לקרוא את הקוד שלכם, לראות env vars, וכו'), אבל זה קו הגנה נוסף שמצמצם נזק.

בעיה נפוצה: קבצים בבעלות root

כשאתם מריצים container עם -v $(pwd):/app (bind mount לפיתוח), הקבצים במארח ייכתבו על-ידי ה-container. אם ה-container רץ כ-root, הקבצים במארח יהיו בבעלות root, ולא תוכלו לערוך אותם בלי sudo.

פתרון: החליפו ל-user לא-root לפני ה-bind mount, או השתמשו ב-user namespace.

ה-3 שורות שחובה להוסיף

בכל Dockerfile של פרודקשן, הוסיפו את השלוש השורות האלה (לפני ה-CMD):

RUN useradd --create-home --shell /bin/bash appuser
USER appuser
WORKDIR /home/appuser/app

או ב-Node:

RUN useradd --create-home --shell /bin/bash appuser
USER appuser
WORKDIR /home/appuser/app

זה הסטנדרט. רוב ה-Dockerfiles של חברות גדולות מכילים את זה. גם אם זה לא האבטחה הכי חזקה, זו הבסיסית.

Do Now — 3 דקות (לוודא שהאפליקציה רצה כ-user לא-root)

אחרי שהוספתם את שלוש השורות, בנו מחדש והריצו:

docker build -t myapp-secure .
docker run -d --name myapp-secure-container myapp-secure
docker exec myapp-secure-container whoami

תוצאה צפויה: הפלט של whoami הוא appuser, לא root. אם זה root, ה-USER directive לא עבד — בדקו שהוא מופיע לפני ה-CMD.

בינוני 15 דקות פרויקט Capstone

Capstone — לקחת את ה-Dockerfile של docker init ולשדרג אותו

עכשיו אתם יודעים את כל היסודות. בואו נשלב אותם ל-Dockerfile מקצועי אחד. ה-Dockerfile ש-docker init יצר הוא טוב — אבל הוא לא אופטימלי. הנה המסלול מ-"התחלה" ל-"מקצועי":

שלב 1: התחלה (מה ש-docker init יוצר)

# syntax=docker/dockerfile:1
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]

זה עובד. זה רץ. אבל זה לא מאובטח (root), לא מתועד (אין EXPOSE), ואין healthcheck.

שלב 2: שיפור ראשון — הוספת EXPOSE, USER, והערות

# syntax=docker/dockerfile:1

# Base image: רזה, מקובע גרסה
FROM python:3.11-slim

# תיקיית עבודה
WORKDIR /app

# העתקת dependencies קודם (cache)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# העתקת הקוד
COPY . .

# תיעוד הפורט
EXPOSE 3000

# יצירת user לא-root
RUN useradd --create-home --shell /bin/bash appuser \
    && chown -R appuser:appuser /app
USER appuser

# פקודת הרצה
CMD ["python", "app.py"]

הוספנו: EXPOSE 3000 (תיעוד), useradd + chown + USER appuser (אבטחה בסיסית), והערות שמסבירות כל קטע. האפליקציה רצה עדיין, אבל עכשיו גם מאובטחת יותר.

שלב 3: גרסה סופית — multi-stage, healthcheck, ועוד

לפרויקטים מורכבים יותר, זו הגרסה שכדאי לכם לכתוב:

# syntax=docker/dockerfile:1

# ===== שלב 1: builder (לא נכלל ב-image הסופי) =====
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt

# ===== שלב 2: runtime =====
FROM python:3.11-slim

# העתקת ה-dependencies מה-builder
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH

WORKDIR /app
COPY --chown=appuser:appuser . .

# יצירת user לא-root
RUN useradd --create-home --shell /bin/bash appuser \
    && chown -R appuser:appuser /app
USER appuser

EXPOSE 3000

# בדיקת בריאות
HEALTHCHECK --interval=30s --timeout=3s \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:3000/health')" || exit 1

CMD ["python", "app.py"]

השיפורים: --chown=appuser:appuser ב-COPY (מעתיק ישר עם הבעלות הנכונה), HEALTHCHECK ש-Docker יכול להשתמש בו כדי לדעת אם ה-container בריא, ו-multi-stage מופרד (אם הייתם צריכים build step נפרד).

הערה: HEALTHCHECK דורש שתהיה לכם endpoint של /health באפליקציה. אם אין — הוסיפו אחת, או הסירו את ה-HEALTHCHECK.

טבלת ההשוואה — מה השתנה?

מאפיין התחלה שיפור 1 גרסה סופית
גודל משוער 180MB 180MB 130MB
זמן rebuild (שינוי קוד) 5-10s 5-10s 5-10s
Root user כן לא לא
EXPOSE לא כן כן
HEALTHCHECK לא לא כן
Multi-stage לא לא כן

הגודל השתפר רק עם multi-stage, אבל השיפור המשמעותי הוא באבטחה (USER) ובמה שהקוד עושה בכל סביבה (HEALTHCHECK).

מה אתם בונים ב-Capstone

בסוף הפרק, אתם צריכים לבנות שלושה דברים:

  1. Dockerfile מקורי של מה ש-docker init יצר, שרץ ב-docker run.
  2. Dockerfile משופר עם USER appuser, EXPOSE, והערות ברורות.
  3. טבלת החלטה (3 שורות) שמתעדת: base image שבחרתם, למה, וכמה גודל ה-image הסופי.

אלה ה-deliverables של הפרק. הם חוזרים ב-checklist בסוף.

טעות נפוצה: לכתוב COPY . . לפני התקנת ה-dependencies

זו הטעות הראשונה שתעשו אחרי שתתחילו לכתוב Dockerfile בעצמכם. תכתבו COPY . . לפני RUN pip install כי זה "הגיוני" — קודם מעתיקים הכל, אחר כך מתקינים. אבל הסדר הזה הופך כל שינוי בקוד להתקנה מלאה מחדש של dependencies. ה-build שלכם יקח 3-5 דקות במקום 10 שניות.

הסימפטום המדויק: אתם עורכים app.py, שומרים, בונים מחדש, והפלט מראה => [deps] RUN pip install … רץ מחדש (לא CACHED). אתם רואים את כל ה-pip output — הורדה של חבילות, קומפילציה, התקנה — למרות שרק ערכתם שורה אחת.

התיקון: העתיקו את קובץ ה-dependencies בלבד (requirements.txt / package.json / go.mod) לפני שאתם מעתיקים את שאר הקוד. pip install תלוי רק ב-requirements.txt; אם קובץ ה-dependencies לא השתנה, ה-layer של ההתקנה נשאר ב-cache. עריכה של app.py מבטלת רק את ה-layer של COPY . ..

טעות נפוצה: לדלג על .dockerignore

"אני לא צריך .dockerignore, אין לי node_modules בפרויקט" — אתם תגידו את זה. אחר כך, אחרי שתריצו docker build פעמיים, תראו transferring context: 1.8GB ותבינו שיש לכם גם venv/, גם .git/, גם __pycache__/, וגם קובץ .env שדחפתם לריפו בלי לשים לב.

הסימפטום המדויק: ה-build לוקח 30 שניות לפני שהוא בכלל מתחיל. ה-image גדול בהרבה ממה שאתם מצפים. ובעיקר: docker run myapp cat .env מחזיר את הסודות שלכם. ה-API key שלכם בתוך ה-image, גלוי לכל מי שמושך אותו.

התיקון: docker init יוצר .dockerignore אוטומטית. אל תמחקו אותו. הוסיפו לו דברים ייחודיים לפרויקט שלכם (datasets, model weights, סרטונים). אם כבר מחקתם, צרו אחד חדש עם touch .dockerignore ותדביקו את התוכן מסעיף 6.

טעות נפוצה: לבחור alpine בעיוורון לאפליקציית Python/data

"Alpine קטן יותר, אז alpine עדיף" — לא בפייתון/data. Alpine משתמש ב-musl במקום glibc, וזה שובר חבילות native. ה-build יעבוד (אולי), אבל בריצה הראשונה תקבלו ImportError: libstdc++.so.6 או undefined symbol: __libc_start_main.

הסימפטום המדויק: ה-image נבנה בהצלחה. docker run myapp עובד 5 שניות ואז קורס עם ImportError. השגיאה מוזרה כי numpy הותקן "בהצלחה" ב-pip. הסיבה: ה-binary של numpy נבנה נגד glibc; ב-alpine אין glibc, יש musl. ה-binary לא יכול לרוץ.

התיקון: התחילו מ--slim. תמיד. python:3.11-slim, node:22-slim. רק אחרי שהאפליקציה רצה ואתם באמת צריכים עוד 100MB פחות — נסו alpine. ורק אחרי שבדקתם שכל ה-dependencies נבנים נכון.

תרגיל 1 — מקוד שרץ על הלפטופ ל-image שרץ ב-container (25 דקות)

המטרה: לעבור את כל הפרק בפועל על האפליקציה שלכם. מ-Dockerfile שלא היה לכם, ל-image שרץ עם docker run.

מה תעשו:

  1. פתחו terminal בתיקיית הפרויקט שלכם. ודאו שאתם רואים את package.json / requirements.txt עם ls.
  2. הריצו docker init. ענו על השאלות. אשרו את ברירות המחדל.
  3. הריצו ls -la. אתם אמורים לראות: Dockerfile, compose.yaml, .dockerignore, README.Docker.md (פלוס הקבצים המקוריים של הפרויקט).
  4. פתחו את ה-Dockerfile בעורך. קראו אותו שורה-שורה. אל תשנו כלום עדיין.
  5. הריצו docker build -t myapp .. חכו לסיום.
  6. הריצו docker images | grep myapp. ראו את ה-image החדש. רשמו את הגודל.
  7. הריצו docker run -d -p 3000:3000 --name myapp-test myapp (או ה-port שלכם).
  8. פתחו http://localhost:3000 ב-browser. אם האפליקציה עולה — סיימתם.
  9. אם לא: docker logs myapp-test. חפשו שגיאות.
  10. עצרו: docker stop myapp-test && docker rm myapp-test.

תוצאה צפויה: בתוך 25 דקות יש לכם image רץ. docker images מציג את myapp, וה-browser מציג את האפליקציה. docker logs נקי או עם הודעות startup רגילות. אם משהו נשבר — חיזרו לסעיף 3 ובדקו אם ה-Dockerfile תואם את ה-expected behavior של האפליקציה שלכם.

תרגיל 2 — להוכיח את ה-cache (15 דקות)

המטרה: לראות בעיניים את ההפרש בין Dockerfile "רע" (dependencies אחרי הקוד) לבין Dockerfile "נכון" (dependencies לפני הקוד).

מה תעשו:

  1. צרו עותק של ה-Dockerfile: cp Dockerfile Dockerfile.bad.
  2. ערכו את Dockerfile.bad כך ש-COPY . . יופיע לפני RUN pip install.
  3. מדדו: time docker build -f Dockerfile.bad -t myapp-bad .. רשמו את הזמן.
  4. מדדו שוב: time docker build -f Dockerfile.bad -t myapp-bad .. רשמו. (זה cache hit מלא — אמור להיות מהיר).
  5. עכשיו ערכו שורת קוד אחת (למשל, הוסיפו רווח ב-app.py). מדדו שוב: time docker build -f Dockerfile.bad -t myapp-bad .. רשמו. זה הזמן החשוב.
  6. חזרו על אותו הדבר עם ה-Dockerfile המקורי (הנכון). time docker build -t myapp .. רשמו.
  7. ערכו שורה ב-app.py. מדדו שוב. רשמו.

תוצאה צפויה: הריצה השנייה של Dockerfile.bad אחרי עריכת קוד — 30 שניות עד 3 דקות. הריצה השנייה של ה-Dockerfile הנכון אחרי אותה עריכה — 5-10 שניות. ההפרש הזה הוא ה-cache. אם ראיתם את ההפרש, אתם מבינים עכשיו למה הסדר חשוב.

תרגיל 3 — להחליף base ולמדוד (15 דקות)

המטרה: לראות את ההשפעה של בחירת base image על הגודל, ולהחליט בעצמכם.

מה תעשו:

  1. אם ה-Dockerfile שלכם מתחיל ב-python:3.11-slim — בנו כרגע: docker build -t myapp-slim ..
  2. צרו עותק: cp Dockerfile Dockerfile.full. שנו ל-python:3.11 (בלי slim). בנו: docker build -f Dockerfile.full -t myapp-full ..
  3. צרו עותק נוסף: cp Dockerfile Dockerfile.alpine. שנו ל-python:3.11-alpine. בנו: docker build -f Dockerfile.alpine -t myapp-alpine ..
  4. הריצו: docker images | grep myapp. ראו את שלושת הגדלים. רשמו.
  5. עכשיו נסו להריץ את myapp-alpine: docker run --rm myapp-alpine python app.py. אם זה עובד — מזל טוב, ה-dependencies שלכם pure-Python. אם זה קורס עם ImportError — חיזרו ל-slim.
  6. נקו: docker rmi myapp-full myapp-alpine Dockerfile.full Dockerfile.alpine.

תוצאה צפויה: myapp-full ~1GB, myapp-slim ~180MB, myapp-alpine ~120MB (אם הצליח). ההפרש ברור. ההחלטה שלכם: האם ה-100MB הנוספים של slim שווים את ההגנה מ-musl? ברוב המקרים — כן. השאירו את slim.

תרגיל 4 — לאבטח את ה-container (10 דקות)

המטרה: להוסיף USER appuser ל-Dockerfile שלכם, לבנות מחדש, ולוודא שהאפליקציה רצה כ-user לא-root.

מה תעשו:

  1. פתחו את ה-Dockerfile. לפני CMD, הוסיפו:
    RUN useradd --create-home --shell /bin/bash appuser \
        && chown -R appuser:appuser /app
    USER appuser
  2. אם יש לכם WORKDIR /app, שנו ל-WORKDIR /home/appuser/app (או השאירו /app אם זה עובד).
  3. בנו: docker build -t myapp-secure ..
  4. הריצו: docker run -d -p 3000:3000 --name myapp-secure myapp-secure.
  5. בדקו: docker exec myapp-secure whoami. הפלט צריך להיות appuser.
  6. בדקו: docker exec myapp-secure apt-get update. זה אמור להיכשל (Permission denied) כי appuser לא יכול להתקין חבילות.
  7. פתחו http://localhost:3000. האפליקציה צריכה לעבוד.
  8. נקו: docker stop myapp-secure && docker rm myapp-secure.

תוצאה צפויה: whoami מחזיר appuser, האפליקציה רצה, ו-apt-get נכשל. אם whoami מחזיר root, ה- USER directive לא עבד — בדקו שהוא אחרי ה-useradd ולפני ה-CMD. אם האפליקציה לא רצה, ייתכן שהיא צריכה לכתוב לקבצים של root — תקנו את הרשאות.

Framework — "לאיזה base image ללכת?"

החלטה על base image היא trade-off בין גודל, תאימות, ואבטחה. ה-framework הזה מוביל אתכם ב-3 שאלות:

  1. האם לאפליקציה שלכם יש dependencies שמבוססות על C extensions?
    • כן (numpy, pandas, scipy, psycopg2, sharp, sqlite3-binary) → -slim. הימנעו מ-alpine כי ה-binary wheels לא תואמים musl.
    • לא (pure Python, pure JS, Go, Rust) → שקלו -alpine לחיסכון של עוד 50-100MB.
  2. האם גודל ה-image קריטי?
    • כן (VPS קטן, deploy ל-CDN, bandwidth מוגבל) → -alpine או distroless.
    • לא (VPS רגיל, deploy פשוט) → -slim. זה מספיק טוב.
  3. האם האבטחה היא priority עליון?
    • כן (production אמיתי עם משתמשים אמיתיים) → distroless. אבל תצטרכו לוותר על docker exec … sh (אין shell).
    • לא (side project, MVP) → -slim עם USER appuser. זה מספיק.

ברירת-המחדל המומלצת: -slim. תמיד. רק עברו ל-alpine אם אתם יודעים שאתם צריכים, ול-distroless אחרי שאתם כבר בפרודקשן.

Framework — "האם ה-Dockerfile שלי מוכן ל-prod?"

לפני שאתם שולחים image ל-registry, עברו על 8 הבדיקות האלה. כל "לא" = תקנו לפני deploy.

  1. ה-base image מקובע גרסה? לא latest, כן python:3.11-slim.
  2. ה-WORKDIR מוגדר? לא /.
  3. ה-dependencies נטענים לפני COPY . .?
  4. יש EXPOSE על הפורט הנכון? (לתיעוד; לא פותח פורט בפועל).
  5. יש USER לא-root? ברירת המחדל היא root; לא מתאים לפרודקשן.
  6. יש .dockerignore עם .env ו-node_modules?
  7. ה-CMD ב-exec form? (["python", "app.py"], לא python app.py).
  8. אין סודות ב-Dockerfile או בקוד? (תזכורת: ENV API_KEY=... זה גרוע. השתמשו ב---env-file ב-runtime.)

איך להשתמש: תעתיקו את 8 הבדיקות ל-Notepad. אחרי שאתם כותבים Dockerfile, עברו אחת-אחת. אם אתם עוברים 8/8 — שלחו. אם פחות — תקנו.

Framework — "מה הסדר הנכון של הוראות ב-Dockerfile?"

הסדר קובע: (א) כמה מהר אתם בונים מחדש, ו-(ב) כמה גדול ה-image. הנוסחה:

  1. FROM — תמיד ראשון. בסיס.
  2. WORKDIR — לפני כל הוראה שמשתמשת בנתיב יחסי.
  3. COPY <manifest> — קובץ ה-dependencies בלבד. חיוני לפני RUN install.
  4. RUN <install> — התקנת dependencies. משתמש ב-manifest.
  5. COPY . . — שאר הקוד. אחרי ההתקנה.
  6. RUN <build-steps> (אופציונלי) — קומפילציה, minification, וכו'.
  7. EXPOSE — תיעוד פורט.
  8. RUN <user-setup> (אופציונלי) — יצירת user לא-root.
  9. USER — החלפה ל-user החדש.
  10. CMD / ENTRYPOINT — בסוף. פקודת ההרצה.

הכלל: דברים שמשתנים לעתים רחוקות למעלה (base, dependencies). דברים שמשתנים לעתים קרובות למטה (קוד). ככל שמשהו קרוב יותר ל-FROM, כך הוא נשמר יותר ב-cache.

Work Routine — שגרת Skill-Building Phase (פרק 3)

יומי (15 דקות, 14 ימים ראשונים):

שבועי (30 דקות, יום ראשון): סקירת Dockerfile. פתחו את ה-Dockerfile של הפרויקט הראשי שלכם. עברו על 8 הבדיקות של "Framework — מוכן ל-prod?". אם יש "לא" — תקנו השבוע.

לפני פרק 4 (סוף שבוע שני): ודאו שיש לכם image רץ של האפליקציה. docker run -p 3000:3000 myapp צריך לעבוד, ו-docker exec myapp-container whoami צריך להחזיר appuser (לא root). אם משהו נשבר — תקנו עכשיו. בפרק 4 נוסיף volumes ו-env vars, ואתם צריכים image יציב.

חודשי (60 דקות, סוף חודש): audit. הריצו docker images ובדקו כמה images יש לכם. docker image prune -a מנקה images שלא בשימוש. docker system df מראה כמה מקום Docker תופס. מטרה: תחת 10GB total לפרויקטים שלכם.

Check Yourself — 5 שאלות הבנה
  1. שאלה: יש לכם Dockerfile שבו COPY . . מופיע לפני RUN pip install -r requirements.txt. אתם עורכים שורה אחת ב-app.py. מה יקרה ב-docker build הבא, ולמה?
    תשובה: pip install ירוץ מחדש, למרות שרק ערכתם שורת קוד. הסיבה: pip install תלוי ב-COPY . ., והקובץ שלכם השתנה, אז ה-layer שלו מתעדכן, וכל מה שתלוי בו — כולל ההתקנה — נבנה מאפס. ה-build ייקח 1-3 דקות במקום 5 שניות. התיקון: העתיקו רק את requirements.txt לפני pip install (כמו ש-docker init עושה).
  2. שאלה: בניתם image עם FROM python:3.11-alpine. ה-image נבנה בהצלחה, אבל בריצה הראשונה אתם מקבלים ImportError: libstdc++.so.6: cannot open shared object file. מה הסיבה, ואיך תתקנו?
    תשובה: הסיבה: alpine משתמש ב-musl במקום glibc, וה-binary wheel של numpy (או כל חבילה native אחרת) נבנה נגד glibc. ה-binary לא יכול לרוץ. התיקון: החליפו את ה-base ל-python:3.11-slim. slim מבוסס על Debian (glibc), ותואם את ה-binary wheels הסטנדרטיים.
  3. שאלה: מה ההבדל בין CMD ל-ENTRYPOINT? באיזה מהם כדאי להשתמש לאפליקציה רגילה של Vibe Coder, ולמה?
    תשובה: CMD קובע פקודת ברירת-מחדל שניתנת לדריסה. ENTRYPOINT קובע פקודה קבועה שמקבלת arguments נוספים. לאפליקציה רגילה (FastAPI, Flask, Next.js), CMD הוא הבחירה הנכונה כי אין סיבה לאפשר דריסה של פקודת ההפעלה. ENTRYPOINT מתאים לכלי CLI שמשתמשים בהם בצורה גמישה (למשל, mytool --flag value).
  4. שאלה: אתם רואים Dockerfile עם FROM node:latest. למה זו בעיה, ואיך תתקנו?
    תשובה: הבעיה: latest הוא תג נע — מחר הוא יצביע על גרסה אחרת של node. הבנייה שלכם לא-משוחזרת. אם הקוד שלכם תלוי בהתנהגות של node 20, ומחר docker hub יעלה node 23 — ה-image ייבנה אחרת, ואולי יקרוס. התיקון: קבעו גרסה ספציפית, למשל FROM node:22-slim (או גרסה מדויקת יותר כמו node:22.11.0-slim אם אתם רוצים ביטחון מלא).
  5. שאלה: הוספתם ל-Dockerfile את השורה USER appuser, בניתם מחדש, והרצתם docker exec myapp-container whoami. הפלט הוא root. מה השתבש, ואיך תתקנו?
    תשובה: כנראה USER appuser מופיע לפני ה- useradd, או שלא הוספתם את ה-useradd בכלל. ה- USER directive מחליף למשתמש שכבר קיים. אם appuser לא קיים, ה- USER directive נכשל — ובמקרים מסוימים Docker מתעלם וממשיך עם root. התיקון: ודאו שהסדר הוא: (1) RUN useradd --create-home appuser, (2) USER appuser, (3) CMD. בנו מחדש.
מה תפיקו בסוף הפרק
סיכום הפרק — 7 לקחים שייקחו אתכם הלאה
  1. docker init הוא ה-on-ramp, לא ביישן להשתמש בו. הוא יוצר Dockerfile מקצועי לפי best practices של Docker עצמו. תנו לו לכתוב את הראשון, ואז תלמדו לקרוא ולשפר. אין צורך להתחיל מאפס.
  2. הסדר של ההוראות ב-Dockerfile קובע את מהירות ה-rebuild. Dependencies לפני COPY . . = rebuild של 5-10 שניות. COPY . . לפני dependencies = rebuild של 3-5 דקות. ההפרש הוא ה-cache.
  3. .dockerignore הוא לא אופציה — הוא חובה. הוא חוסך build time, מקטין את ה-image, ובעיקר — שומר על הסודות שלכם מחוץ ל-image. docker init יוצר אחד; אל תמחקו אותו.
  4. Base image הוא ההחלטה הכי משמעותית לגודל. -slim הוא הברירת-מחדל המומלצת. -alpine קטן יותר אבל שובר חבילות native (numpy/pandas). distroless הוא לפרודקשן אמיתי, לא למתחילים.
  5. קבעו גרסאות, אל תשתמשו ב-:latest. latest הוא תג נע, לא גרסה. FROM python:3.11-slim הוא build משוחזר; FROM python:latest הוא הפתעה מחר בבוקר.
  6. multi-stage build מקטין את ה-image בכ-80%. שלב builder עם כל הכלים, שלב runtime רק עם ה-artifact. COPY --from=builder מעתיק רק את מה שצריך.
  7. אל תריצו כ-root. הוסיפו RUN useradd + USER appuser לפני ה-CMD. זה לא פתרון האבטחה היחיד, אבל זה הבסיסי ביותר, וזה מצופה מכל Dockerfile של פרודקשן.
Just One Thing — אם תזכרו רק דבר אחד מהפרק הזה

אם תוציאו רק פעולה אחת מהפרק הזה השבוע — שתהיה זאת: לכו ל-Dockerfile של הפרויקט שלכם עכשיו, וודאו שה-dependencies נטענים לפני COPY . .. אם COPY . . מופיע לפני RUN pip install (או RUN npm install) — תקנו את הסדר. תעתיקו את requirements.txt / package.json לפני, תריצו install, ורק אחר כך את COPY . .. תבנו מחדש, תערכו שורת קוד, תבנו שוב, ותרגישו את ההפרש בזמן. זה ה-cache בפעולה.

הסיבה שזו הפעולה הכי חשובה: זה השיפור היחיד שמשפיע עליכם בכל build, יום-יום, לאורך כל הקורס וכל הפרויקטים שלכם. כל שאר השיפורים (USER, multi-stage, base image) הם חד-פעמיים. סדר נכון של הוראות משפיע על כל build, לעד. אם תזכרו רק את זה — תחסכו שעות של build time בחודש הקרוב.

מה הלאה — פרק 4

בפרק 4 (נתונים וקונפיג — Volumes, משתני סביבה, וסודות כמו שצריך) נתמודד עם שתי בעיות קריטיות שעדיין לא פתרנו: (1) נתונים שנעלמים — אם תעצרו ותמחקו את ה-container, הכל נמחק. נלמד על named volumes (תיקיות מנוהלות על-ידי Docker) ו-bind mounts (תיקיות מהמארח), ומתי להשתמש בכל אחד. (2) סודות שנחשפים — איך להעביר API keys ל-container בלי לאפות אותם ל-image. זה הפרק שבו תלמדו את הכלל הכי חשוב בקורס: אף פעם לא לאפות סודות לתוך image. הטעות הזו קיימת ב-48% מאפליקציות AI, ובפרק 4 נוודא שאתם לא חלק מהסטטיסטיקה הזו. ה-Dockerfile המשופר שיצרתם בפרק הזה הוא הבסיס; פרק 4 יוסיף לו את ה-data layer וה-config layer.

Checklist — 14 פריטים לסיום הפרק