- ליצור
Dockerfile+compose.yaml+.dockerignoreלאפליקציה שלכם עםdocker init— ואז להריץdocker build -t myapp .עד שה-image בנוי ומריץdocker runבלי שגיאה. - לקרוא
Dockerfileשורה-שורה ולהסביר כל instruction (הוראה ב-Dockerfile):FROM,WORKDIR,COPY,RUN,EXPOSE,ENV,CMDמולENTRYPOINT— בלי להסתנוור מהסינטקס. - לסדר מחדש את ה-Dockerfile כך שהתקנת ה-dependencies קודמת ל-
COPY . ., ולמדוד את הקיצוץ בזמן ה-rebuild — מ-10 דקות ל-30 שניות על עריכת שורת קוד אחת. - לבחור base image (תמונת הבסיס שעליה הכל נבנה) —
fullמול-slimמול-alpineמולdistroless— לנמק את הבחירה לפי גודל מול תאימות, ולהחיל multi-stage build בסיסי לקיצוץ של כ-80% בגודל.
- פרקים קודמים: פרק 1 (למה Docker — ה-mental model) ופרק 2 (התקנה, hello-world, nginx, port mapping, 0.0.0.0). בלי הבחנה ברורה בין image ל-container, ובלי הבנה של
-p host:container, הפרק הזה ירגיש כמו קסם. - אפליקציה שה-AI בנה לכם בתיקייה מקומית — Next.js, Flask, FastAPI, Express, Streamlit, או כל שפה אחרת. צריך שתהיה רצה מקומית עם
npm run dev/python app.py/ פקודה דומה, ושיש להpackage.jsonאוrequirements.txt(או שתדעו להתקין dependencies ידנית ב-Dockerfile). - Docker Desktop רץ ועובד — אם עוד לא עברתם את הבדיקות של פרק 2 (
hello-world,nginxב-browser), עצרו וחיזרו לפרק 2. בלי סביבה יציבה, הפרק הזה יהיה מתסכל. - עורך טקסט לעריכת Dockerfile — VSCode, Notepad++, Sublime, או כל עורך שמראה שורות. תצטרכו לפתוח את ה-Dockerfile ולקרוא אותו שורה-שורה.
- זמן משוער: 90-120 דקות. זה הפרק הכי ארוך בקורס עד כה, ויש בו 4 תרגילים מעשיים.
אתם בעיצומו של הפרויקט המרכזי של הקורס: לקחת את האפליקציה שה-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 שניות.
מהרצת 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 עצמו:
Dockerfile— קובץ ההוראות לבניית ה-image. הקובץ המרכזי.compose.yaml— קובץ שמגדיר איך להריץ את ה-image ועם אילו שירותים נוספים (מסד-נתונים, cache). נשתמש בו בפרק 5..dockerignore— קובץ שאומר ל-Docker מה לא להעתיק לתוך ה-image. חובה, לא אופציה.README.Docker.md— תיעוד קצר של איך להריץ את ה-image. שימושי לכם ולמי שיקרא את הריפו.
הוא לא עושה שלושה דברים שאנשים מצפים ממנו: הוא לא בונה image, לא מריץ container, ולא מעלה את הקוד ל-registry. הוא רק יוצר את הקבצים. את השאר אתם עושים — ובשלב הזה תלמדו לעשות את זה נכון.
לפני שאתם מריצים docker init, פתחו terminal ובדקו שני דברים:
docker --version— מחזיר מספר גרסה. אם לא, Docker Desktop לא רץ.pwdו-ls— אתם באמת בתיקיית הפרויקט שלכם? אם לא,cd ~/projects/your-app(או הנתיב שלכם).
תוצאה צפויה: גרסת Docker מודפסת (למשל Docker version 27.x.x, build xxxxx), ורשימת קבצים של הפרויקט שלכם — package.json / requirements.txt / app.py / קבצי קוד אחרים. אם הרשימה ריקה או שאתם לא בתיקייה הנכונה — עצרו ותקנו. docker init יוצר קבצים בתיקייה הנוכחית, ואתם לא רוצים שהוא יזרוק Dockerfile בתיקיית הבית שלכם.
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.
אחרי docker run, פתחו terminal שני (אל תסגרו את הראשון — ה-container רץ שם) והריצו:
docker ps— תראו אתmyapp-containerברשימה. עמודת STATUS צריכה להיותUp X seconds.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 אומר?
לקרוא 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?
שלוש הוראות נפוצות שלא ראינו כאן, אבל תראו בפרויקטים רבים:
EXPOSE 3000— תיעוד בלבד. אומר "ה-container הזה מאזין על 3000" אבל לא פותח את הפורט. אתם צריכים עדיין-p 3000:3000ב-docker run. ראינו את זה בפרק 2.ENV VAR=value— קובע משתנה סביבה (environment variable — ערך שזמין לאפליקציה מבלי להיות בקוד). שימושי להגדרות כלליות, אבל לא לסודות (לעולם לא לאפות API key לתוך image).USER appuser— מחליף את המשתמש שרץ בתוך ה-container. ברירת המחדל היא root. נדבר על זה בסעיף 10.
בואו נראה את המלכודת של 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 הוא תיעוד בלבד. זכרו את זה — זו טעות שכל מתחיל עושה.
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:
DOCKER_BUILDKIT=1כבר לא נדרש — היה צריך להגדיר את זה ידנית לפני 2023. עכשיו זה default. אם אתם רואים מדריך ישן שאומר "תגדירDOCKER_BUILDKIT=1", תדעו שזה כבר לא נחוץ.- BuildKit v0.17.0 (Q1 2026) הוסיף אופטימיזציות של זיכרון וביצועים, ו-incremental builds מהירים יותר. אין לכם מה לעשות עם זה — רק לדעת שזה קיים.
מה קורה כשאתם בונים?
הריצה הראשונה ארוכה (45 שניות בדוגמה שלנו). הריצה השנייה — בלי שינויים — תיקח שנייה אחת, כי BuildKit מזהה שאין שינויים ומשתמש ב-cache של כל ה-layers. עריכת שורת קוד אחת תגרור — תלוי בסדר ה-Dockerfile — rebuild של layer אחד או כל ה-image. בסעיף הבא נלמד איך לסדר את ה-Dockerfile כך שעריכה של שורת קוד תגרור rebuild של שכבה אחת בלבד.
הריצו 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 מהריצה הקודמת."
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, ההבדל הוא:
- Dockerfile "רע": 3-5 דקות rebuild בכל שינוי קוד. אחרי 50 שינויים ביום, 4 שעות build.
- Dockerfile "נכון": 5-10 שניות rebuild בכל שינוי קוד. אחרי 50 שינויים ביום, 7 דקות build.
זה ההבדל בין "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 אחרים
העיקרון זהה תמיד:
- FROM — בסיס
- WORKDIR — תיקיית עבודה
- COPY <manifest> (requirements.txt / package.json / go.mod / pom.xml) — רק קובץ ה-dependencies
- RUN <install> — התקנה
- COPY . . — שאר הקוד
- CMD — פקודת הפעלה
את העיקרון הזה תראו בכל Dockerfile מקצועי, בכל שפה.
קחו את ה-Dockerfile הנוכחי שלכם (או צרו אחד חדש עם docker init בפרויקט גדול). אתם הולכים למדוד שני תרחישים.
- תרחיש א: Dockerfile "רע" — העתיקו את ה-Dockerfile לגרסה עם
COPY . .לפניRUN pip install. בנו:time docker build -t myapp-bad .. רשמו את הזמן. - תרחיש ב: Dockerfile "טוב" — החזירו את הסדר הנכון (dependencies לפני קוד). בנו שוב:
time docker build -t myapp-good .. רשמו. - עכשיו ערכו שורת קוד אחת (למשל, הוסיפו רווח ב-
app.py). בנו שוב את שניהם. רשמו.
תוצאה צפויה: הריצה הראשונה של שניהם לוקחת בערך אותו זמן (לא משנה הסדר בריצה הראשונה, כי אין cache). ההפרש האמיתי מופיע בריצה השנייה: ה-Dockerfile ה"רע" יבנה מחדש את הכל (כולל pip install). ה-Dockerfile ה"טוב" יבנה רק את ה-layer של הקוד. ההפרש: 3-5 דקות מול 5-10 שניות.
.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. הסודות שלכם בטוחים.
פתחו את .dockerignore ש-docker init יצר, והוסיפו לפחות 3 שורות שמתאימות לפרויקט שלכם. רעיונות:
- תיקיית data/datasets אם יש לכם (הרבה מגיבים AI שומרים שם קבצי JSON גדולים)
- תיקיית tests/ אם אתם לא רוצים אותה ב-production image
- קובץ README.md (לא צריך אותו ב-image, רק בריפו)
תוצאה צפויה: docker build יהיה מהיר יותר, ה-image יהיה קטן יותר, ו-transferring context יראה מספר קטן בהרבה. תרגישו את ההבדל גם ב-build הראשון, לא רק בשני.
בחירת 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
ב-Dockerfile שלכם, החליפו את ה-base לגרסה רזה יותר. למשל:
- אם יש לכם
python:3.11(full) — החליפו ל-python:3.11-slim - אם יש לכם
node:22(full) — החליפו ל-node:22-slim
בנו מחדש: docker build -t myapp-slim .. הריצו: docker images | grep myapp והשוו את ה-SIZE עם ה-image הקודם.
תוצאה צפויה: קיצוץ של 60-80% בגודל. מ-image של 1GB תרדו ל-200-300MB. אם האפליקציה רצה באותה צורה, סימן שלא היו לכם תלויות ב-binary חיצוני שחייב את ה-full base. אם היא קורסת — חזרו ל-full ותחקרו למה.
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 — טריק שתשתמשו בו המון.
מתי משתמשים בכל אחד?
- רוב הזמן —
CMD. האפליקציה שלכם תמיד רצה עם אותה פקודה, ואתם רוצים להשאיר פתח לדריסה לדיבוג (docker run myapp bash). זו ברירת המחדל שלdocker init. - כלי CLI או wrapper script —
ENTRYPOINT. נניח שה-Dockerfile שלכם בונהmytool(binary שלכם).ENTRYPOINT ["mytool"]אומר: תמיד הרץ אתmytool, והמשתמש מעביר רק arguments (docker run mytool --version← רץmytool --version). - השילוב המקצועי —
ENTRYPOINT+CMDיחד.ENTRYPOINT ["python", "app.py"]עםCMD ["--port", "8080"]נותן פקודה קבועה (python app.py) עם arguments ברירת-מחדל (--port 8080) שאפשר לדרוס:docker run myapp --port 9090.
בפרויקט 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. זה בעייתי כי:
- לא מקבלים signals נכון (כמו SIGTERM) — ה-container לא נעצר נקי.
- יכול להיות הפתעות עם environment variables.
תמיד השתמשו ב-exec form (מערך JSON):
CMD ["python", "app.py"]
זה הסיבה ש-docker init יוצר את הצורה הזו מלכתחילה.
הריצו:
docker run --rm myapp sh
זה אמור לפתוח shell בתוך ה-container (במקום להריץ את python app.py). הסיבה: sh בסוף דורס את ה-CMD. כתבו ls ותראו את הקבצים של האפליקציה שלכם. exit לצאת.
תוצאה צפויה: אתם בתוך shell של ה-container, רואים את הקבצים ב-/app. זו דרך מצוינת לאבחן "למה זה לא עובד" — אתם נכנסים פנימה, מסתכלים, מתקנים, יוצאים. תרגיל 4 ישתמש בזה.
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 פחות קריטי. אבל יש מקרים שבהם זה עוזר:
- אם ה-front-end שלכם כולל build step (Next.js עם webpack, React עם bundling) — תוכלו לבנות את ה-frontend בשלב אחד ולהעתיק רק את ה-
build/לשלב השני. - אם אתם מקמפלים C extensions — אותו עיקרון: לבנות בשלב אחד, להעתיק רק את ה-artifacts.
ה-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 קל.
ענו על שלוש שאלות על הפרויקט שלכם:
- האם יש שלב build שלא נדרש ב-runtime? (TypeScript compilation, bundling, minification)
- האם יש חבילה כבדה שמשמשת רק בזמן build? (dev dependencies ב-npm, test frameworks)
- האם יש לכם כלי חיצוני שצריך לבנות assets?
אם עניתם "כן" על אחת מהן, multi-stage יכול לחסוך לכם 50-200MB. אם "לא" על כולן, הישארו עם Dockerfile רגיל.
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. הוא לא יכול:
- להתקין חבילות (
apt installדורש 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 של חברות גדולות מכילים את זה. גם אם זה לא האבטחה הכי חזקה, זו הבסיסית.
אחרי שהוספתם את שלוש השורות, בנו מחדש והריצו:
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.
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
בסוף הפרק, אתם צריכים לבנות שלושה דברים:
- Dockerfile מקורי של מה ש-docker init יצר, שרץ ב-
docker run. - Dockerfile משופר עם
USER appuser, EXPOSE, והערות ברורות. - טבלת החלטה (3 שורות) שמתעדת: base image שבחרתם, למה, וכמה גודל ה-image הסופי.
אלה ה-deliverables של הפרק. הם חוזרים ב-checklist בסוף.
זו הטעות הראשונה שתעשו אחרי שתתחילו לכתוב 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, אין לי 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 קטן יותר, אז 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 נבנים נכון.
המטרה: לעבור את כל הפרק בפועל על האפליקציה שלכם. מ-Dockerfile שלא היה לכם, ל-image שרץ עם docker run.
מה תעשו:
- פתחו terminal בתיקיית הפרויקט שלכם. ודאו שאתם רואים את
package.json/requirements.txtעםls. - הריצו
docker init. ענו על השאלות. אשרו את ברירות המחדל. - הריצו
ls -la. אתם אמורים לראות:Dockerfile,compose.yaml,.dockerignore,README.Docker.md(פלוס הקבצים המקוריים של הפרויקט). - פתחו את ה-Dockerfile בעורך. קראו אותו שורה-שורה. אל תשנו כלום עדיין.
- הריצו
docker build -t myapp .. חכו לסיום. - הריצו
docker images | grep myapp. ראו את ה-image החדש. רשמו את הגודל. - הריצו
docker run -d -p 3000:3000 --name myapp-test myapp(או ה-port שלכם). - פתחו
http://localhost:3000ב-browser. אם האפליקציה עולה — סיימתם. - אם לא:
docker logs myapp-test. חפשו שגיאות. - עצרו:
docker stop myapp-test && docker rm myapp-test.
תוצאה צפויה: בתוך 25 דקות יש לכם image רץ. docker images מציג את myapp, וה-browser מציג את האפליקציה. docker logs נקי או עם הודעות startup רגילות. אם משהו נשבר — חיזרו לסעיף 3 ובדקו אם ה-Dockerfile תואם את ה-expected behavior של האפליקציה שלכם.
המטרה: לראות בעיניים את ההפרש בין Dockerfile "רע" (dependencies אחרי הקוד) לבין Dockerfile "נכון" (dependencies לפני הקוד).
מה תעשו:
- צרו עותק של ה-Dockerfile:
cp Dockerfile Dockerfile.bad. - ערכו את
Dockerfile.badכך ש-COPY . .יופיע לפניRUN pip install. - מדדו:
time docker build -f Dockerfile.bad -t myapp-bad .. רשמו את הזמן. - מדדו שוב:
time docker build -f Dockerfile.bad -t myapp-bad .. רשמו. (זה cache hit מלא — אמור להיות מהיר). - עכשיו ערכו שורת קוד אחת (למשל, הוסיפו רווח ב-
app.py). מדדו שוב:time docker build -f Dockerfile.bad -t myapp-bad .. רשמו. זה הזמן החשוב. - חזרו על אותו הדבר עם ה-Dockerfile המקורי (הנכון).
time docker build -t myapp .. רשמו. - ערכו שורה ב-
app.py. מדדו שוב. רשמו.
תוצאה צפויה: הריצה השנייה של Dockerfile.bad אחרי עריכת קוד — 30 שניות עד 3 דקות. הריצה השנייה של ה-Dockerfile הנכון אחרי אותה עריכה — 5-10 שניות. ההפרש הזה הוא ה-cache. אם ראיתם את ההפרש, אתם מבינים עכשיו למה הסדר חשוב.
המטרה: לראות את ההשפעה של בחירת base image על הגודל, ולהחליט בעצמכם.
מה תעשו:
- אם ה-Dockerfile שלכם מתחיל ב-
python:3.11-slim— בנו כרגע:docker build -t myapp-slim .. - צרו עותק:
cp Dockerfile Dockerfile.full. שנו ל-python:3.11(בלי slim). בנו:docker build -f Dockerfile.full -t myapp-full .. - צרו עותק נוסף:
cp Dockerfile Dockerfile.alpine. שנו ל-python:3.11-alpine. בנו:docker build -f Dockerfile.alpine -t myapp-alpine .. - הריצו:
docker images | grep myapp. ראו את שלושת הגדלים. רשמו. - עכשיו נסו להריץ את myapp-alpine:
docker run --rm myapp-alpine python app.py. אם זה עובד — מזל טוב, ה-dependencies שלכם pure-Python. אם זה קורס עם ImportError — חיזרו ל-slim. - נקו:
docker rmi myapp-full myapp-alpine Dockerfile.full Dockerfile.alpine.
תוצאה צפויה: myapp-full ~1GB, myapp-slim ~180MB, myapp-alpine ~120MB (אם הצליח). ההפרש ברור. ההחלטה שלכם: האם ה-100MB הנוספים של slim שווים את ההגנה מ-musl? ברוב המקרים — כן. השאירו את slim.
המטרה: להוסיף USER appuser ל-Dockerfile שלכם, לבנות מחדש, ולוודא שהאפליקציה רצה כ-user לא-root.
מה תעשו:
- פתחו את ה-Dockerfile. לפני
CMD, הוסיפו:RUN useradd --create-home --shell /bin/bash appuser \ && chown -R appuser:appuser /app USER appuser - אם יש לכם
WORKDIR /app, שנו ל-WORKDIR /home/appuser/app(או השאירו/appאם זה עובד). - בנו:
docker build -t myapp-secure .. - הריצו:
docker run -d -p 3000:3000 --name myapp-secure myapp-secure. - בדקו:
docker exec myapp-secure whoami. הפלט צריך להיותappuser. - בדקו:
docker exec myapp-secure apt-get update. זה אמור להיכשל (Permission denied) כיappuserלא יכול להתקין חבילות. - פתחו
http://localhost:3000. האפליקציה צריכה לעבוד. - נקו:
docker stop myapp-secure && docker rm myapp-secure.
תוצאה צפויה: whoami מחזיר appuser, האפליקציה רצה, ו-apt-get נכשל. אם whoami מחזיר root, ה- USER directive לא עבד — בדקו שהוא אחרי ה-useradd ולפני ה-CMD. אם האפליקציה לא רצה, ייתכן שהיא צריכה לכתוב לקבצים של root — תקנו את הרשאות.
החלטה על base image היא trade-off בין גודל, תאימות, ואבטחה. ה-framework הזה מוביל אתכם ב-3 שאלות:
- האם לאפליקציה שלכם יש dependencies שמבוססות על C extensions?
- כן (numpy, pandas, scipy, psycopg2, sharp, sqlite3-binary) →
-slim. הימנעו מ-alpine כי ה-binary wheels לא תואמים musl. - לא (pure Python, pure JS, Go, Rust) → שקלו
-alpineלחיסכון של עוד 50-100MB.
- כן (numpy, pandas, scipy, psycopg2, sharp, sqlite3-binary) →
- האם גודל ה-image קריטי?
- כן (VPS קטן, deploy ל-CDN, bandwidth מוגבל) →
-alpineאוdistroless. - לא (VPS רגיל, deploy פשוט) →
-slim. זה מספיק טוב.
- כן (VPS קטן, deploy ל-CDN, bandwidth מוגבל) →
- האם האבטחה היא priority עליון?
- כן (production אמיתי עם משתמשים אמיתיים) →
distroless. אבל תצטרכו לוותר עלdocker exec … sh(אין shell). - לא (side project, MVP) →
-slimעםUSER appuser. זה מספיק.
- כן (production אמיתי עם משתמשים אמיתיים) →
ברירת-המחדל המומלצת: -slim. תמיד. רק עברו ל-alpine אם אתם יודעים שאתם צריכים, ול-distroless אחרי שאתם כבר בפרודקשן.
לפני שאתם שולחים image ל-registry, עברו על 8 הבדיקות האלה. כל "לא" = תקנו לפני deploy.
- ה-base image מקובע גרסה? לא
latest, כןpython:3.11-slim. - ה-
WORKDIRמוגדר? לא/. - ה-dependencies נטענים לפני
COPY . .? - יש
EXPOSEעל הפורט הנכון? (לתיעוד; לא פותח פורט בפועל). - יש
USERלא-root? ברירת המחדל היא root; לא מתאים לפרודקשן. - יש
.dockerignoreעם.envו-node_modules? - ה-CMD ב-exec form? (
["python", "app.py"], לאpython app.py). - אין סודות ב-Dockerfile או בקוד? (תזכורת:
ENV API_KEY=...זה גרוע. השתמשו ב---env-fileב-runtime.)
איך להשתמש: תעתיקו את 8 הבדיקות ל-Notepad. אחרי שאתם כותבים Dockerfile, עברו אחת-אחת. אם אתם עוברים 8/8 — שלחו. אם פחות — תקנו.
הסדר קובע: (א) כמה מהר אתם בונים מחדש, ו-(ב) כמה גדול ה-image. הנוסחה:
FROM— תמיד ראשון. בסיס.WORKDIR— לפני כל הוראה שמשתמשת בנתיב יחסי.COPY <manifest>— קובץ ה-dependencies בלבד. חיוני לפני RUN install.RUN <install>— התקנת dependencies. משתמש ב-manifest.COPY . .— שאר הקוד. אחרי ההתקנה.RUN <build-steps>(אופציונלי) — קומפילציה, minification, וכו'.EXPOSE— תיעוד פורט.RUN <user-setup>(אופציונלי) — יצירת user לא-root.USER— החלפה ל-user החדש.CMD/ENTRYPOINT— בסוף. פקודת ההרצה.
הכלל: דברים שמשתנים לעתים רחוקות למעלה (base, dependencies). דברים שמשתנים לעתים קרובות למטה (קוד). ככל שמשהו קרוב יותר ל-FROM, כך הוא נשמר יותר ב-cache.
יומי (15 דקות, 14 ימים ראשונים):
- 5 דקות — Docker init בכל פרויקט חדש. כל פעם שאתם פותחים פרויקט חדש, תריצו
docker initמיד. הפכו את זה להרגל — כל פרויקט מקבל Dockerfile מהיום הראשון. - 5 דקות — לקרוא Dockerfile אחד של מישהו אחר. חפשו ב-GitHub Dockerfiles של פרויקטים מוכרים (למשל,
tiangolo/fastapi,nodejs/docker-node). ראו איך מקצוענים בונים. שימו לב לסדר, ל-user, ל-multi-stage. - 5 דקות — לבנות ולמדוד. בנו
time docker build -t myapp .. רשמו את הזמן. ערכו שורה. בנו שוב. רשמו. תרגישו את ה-cache בעצמכם.
שבועי (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 לפרויקטים שלכם.
- שאלה: יש לכם 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 עושה). - שאלה: בניתם 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 הסטנדרטיים. - שאלה: מה ההבדל בין
CMDל-ENTRYPOINT? באיזה מהם כדאי להשתמש לאפליקציה רגילה של Vibe Coder, ולמה?
תשובה:CMDקובע פקודת ברירת-מחדל שניתנת לדריסה.ENTRYPOINTקובע פקודה קבועה שמקבלת arguments נוספים. לאפליקציה רגילה (FastAPI, Flask, Next.js),CMDהוא הבחירה הנכונה כי אין סיבה לאפשר דריסה של פקודת ההפעלה.ENTRYPOINTמתאים לכלי CLI שמשתמשים בהם בצורה גמישה (למשל,mytool --flag value). - שאלה: אתם רואים Dockerfile עם
FROM node:latest. למה זו בעיה, ואיך תתקנו?
תשובה: הבעיה:latestהוא תג נע — מחר הוא יצביע על גרסה אחרת של node. הבנייה שלכם לא-משוחזרת. אם הקוד שלכם תלוי בהתנהגות של node 20, ומחר docker hub יעלה node 23 — ה-image ייבנה אחרת, ואולי יקרוס. התיקון: קבעו גרסה ספציפית, למשלFROM node:22-slim(או גרסה מדויקת יותר כמוnode:22.11.0-slimאם אתם רוצים ביטחון מלא). - שאלה: הוספתם ל-Dockerfile את השורה
USER appuser, בניתם מחדש, והרצתםdocker exec myapp-container whoami. הפלט הואroot. מה השתבש, ואיך תתקנו?
תשובה: כנראהUSER appuserמופיע לפני ה-useradd, או שלא הוספתם את ה-useradd בכלל. ה-USERdirective מחליף למשתמש שכבר קיים. אםappuserלא קיים, ה-USERdirective נכשל — ובמקרים מסוימים Docker מתעלם וממשיך עם root. התיקון: ודאו שהסדר הוא: (1)RUN useradd --create-home appuser, (2)USER appuser, (3)CMD. בנו מחדש.
- Dockerfile + .dockerignore שנוצרו ב-
docker initלאפליקציה האמיתית שלכם, ו-image בנוי (docker build -t myapp .) שרץ (docker run -p 3000:3000 myapp). האפליקציה נגישה ב-browser. - Dockerfile משופר: dependencies מותקנים לפני
COPY . ., base מוחלף ל--slim(או מוסבר למה לא), ו-USER appuserלא-root נוסף. עם השוואת גודל image וזמן rebuild לפני/אחרי (תרגיל 2 מספק את המספרים). - טבלת החלטה (3 שורות) שמתעדת: base image שבחרתם, למה בחרתם אותו (slim vs alpine vs distroless), וכמה גודל ה-image הסופי ב-
docker images. טבלה זו תעזור לכם בפרק 4 ובפרק 5 כשתוסיפו volumes ו-services.
docker initהוא ה-on-ramp, לא ביישן להשתמש בו. הוא יוצר Dockerfile מקצועי לפי best practices של Docker עצמו. תנו לו לכתוב את הראשון, ואז תלמדו לקרוא ולשפר. אין צורך להתחיל מאפס.- הסדר של ההוראות ב-Dockerfile קובע את מהירות ה-rebuild. Dependencies לפני
COPY . .= rebuild של 5-10 שניות.COPY . .לפני dependencies = rebuild של 3-5 דקות. ההפרש הוא ה-cache. .dockerignoreהוא לא אופציה — הוא חובה. הוא חוסך build time, מקטין את ה-image, ובעיקר — שומר על הסודות שלכם מחוץ ל-image.docker initיוצר אחד; אל תמחקו אותו.- Base image הוא ההחלטה הכי משמעותית לגודל.
-slimהוא הברירת-מחדל המומלצת.-alpineקטן יותר אבל שובר חבילות native (numpy/pandas).distrolessהוא לפרודקשן אמיתי, לא למתחילים. - קבעו גרסאות, אל תשתמשו ב-
:latest.latestהוא תג נע, לא גרסה.FROM python:3.11-slimהוא build משוחזר;FROM python:latestהוא הפתעה מחר בבוקר. - multi-stage build מקטין את ה-image בכ-80%. שלב builder עם כל הכלים, שלב runtime רק עם ה-artifact.
COPY --from=builderמעתיק רק את מה שצריך. - אל תריצו כ-root. הוסיפו
RUN useradd+USER appuserלפני ה-CMD. זה לא פתרון האבטחה היחיד, אבל זה הבסיסי ביותר, וזה מצופה מכל Dockerfile של פרודקשן.
אם תוציאו רק פעולה אחת מהפרק הזה השבוע — שתהיה זאת: לכו ל-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 (נתונים וקונפיג — Volumes, משתני סביבה, וסודות כמו שצריך) נתמודד עם שתי בעיות קריטיות שעדיין לא פתרנו: (1) נתונים שנעלמים — אם תעצרו ותמחקו את ה-container, הכל נמחק. נלמד על named volumes (תיקיות מנוהלות על-ידי Docker) ו-bind mounts (תיקיות מהמארח), ומתי להשתמש בכל אחד. (2) סודות שנחשפים — איך להעביר API keys ל-container בלי לאפות אותם ל-image. זה הפרק שבו תלמדו את הכלל הכי חשוב בקורס: אף פעם לא לאפות סודות לתוך image. הטעות הזו קיימת ב-48% מאפליקציות AI, ובפרק 4 נוודא שאתם לא חלק מהסטטיסטיקה הזו. ה-Dockerfile המשופר שיצרתם בפרק הזה הוא הבסיס; פרק 4 יוסיף לו את ה-data layer וה-config layer.
- ☐ הרצתי
docker initבתיקיית הפרויקט שלי, ועניתי על השאלות שלו (פלט:Dockerfile,compose.yaml,.dockerignore,README.Docker.mdנוצרו). - ☐ הצלחתי לבנות image:
docker build -t myapp .הסתיים בהצלחה והציג את כל ה-layers. - ☐ הצלחתי להריץ את ה-image:
docker run -p 3000:3000 myappהציג את האפליקציה ב-http://localhost:3000. - ☐ אני יכול להסביר בלי להציץ: מה עושה כל אחת מההוראות
FROM,WORKDIR,COPY,RUN,EXPOSE,CMD. - ☐ ה-Dockerfile שלי בנוי בסדר הנכון:
COPY requirements.txt(אוpackage.json) לפניRUN pip install(אוnpm ci), ורק אחר כךCOPY . .. - ☐ מדדתי את ההפרש בזמן rebuild בין Dockerfile "רע" (COPY . . לפני install) לבין Dockerfile "נכון" (dependencies לפני .) — וראיתי את ההפרש של פי 10 בערך.
- ☐ יש לי
.dockerignoreשכולל לפחות:.git,__pycache__אוnode_modules,.env,.venvאוvenv,*.log, ו-.DS_Store. - ☐ אימתי ש-
.envלא נכנס ל-image:docker run --rm myapp ls /app | grep .envלא מחזיר כלום. - ☐ בחרתי base image לפי ה-framework של "לאיזה base ללכת?" — והתוצאה היא
python:3.11-slim/node:22-slim/ דומה, לא:latestולא-alpine(אלא אם כן בדקתי שזה תואם). - ☐ החלפתי base image ומדדתי:
docker imagesמציג את הגודל החדש, ואני יודע להסביר את ההפרש. - ☐ ה-Dockerfile שלי כולל
USER appuser(אחריRUN useraddולפניCMD), ואני יודע לוודא עםdocker exec myapp-container whoami. - ☐ אני מבין את ההבדל בין
CMDל-ENTRYPOINT, ואני יודע שלרוב אני צריךCMD. - ☐ אני יודע לזהות הזדמנות multi-stage בפרויקט שלי: אם יש build step נפרד (TypeScript, bundling, קומפילציה), אני יכול להשתמש ב-multi-stage כדי להקטין את ה-image.
- ☐ אני מרגיש/ה שאני מוכן/ה לפרק 4 (נתונים וקונפיג). ה-image שלי רץ, הוא מאובטח בסיסית, ואני יודע לקרוא את ה-Dockerfile שלי שורה-שורה.