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

נתונים וקונפיג — Volumes, משתני סביבה, וסודות כמו שצריך

בפרק 3 בניתם image של האפליקציה שלכם — קטן, מאובטח בסיסית, ומוכן לרוץ. עכשיו מגיע החלק שבו האפליקציה הופכת למשהו אמיתי: שמירת מסד-נתונים שלא נעלם כשמוחקים container, העברת API keys בלי להדליף אותם ל-GitHub, והבנה מדוע הרבה אפליקציות AI ברשת חשופות בגלל טעות אחת קטנה שחוזרת על עצמה. בפרק הזה תלמדו שלושה דברים שמשנים את כל מה שתעשו מכאן והלאה: named volume למסד-נתונים, --env-file לסודות, והכלל שאסור לשבור — אף פעם לא לאפות סוד לתוך image.

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

אתם בלב הפרויקט המרכזי של הקורס. בפרק 1 הבנתם למה "רץ אצלי במחשב" לא מספיק. בפרק 2 התקנתם את Docker, הרצתם nginx ב-browser, ולמדתם את ששת הפעלים היומיומיים. בפרק 3 בניתם image משלכם עם docker init, סידרתם את ה-Dockerfile (dependencies לפני COPY . ., base slim, USER appuser), וקיבלתם image קטן ומאובטח בסיסית.

בפרק הזה אתם פותרים שתי בעיות שעדיין מפרידות את ה-image שלכם מאפליקציה אמיתית:

  1. נתונים שלא נעלמים — ה-image שלכם רץ, אבל אם תעצרו ותמחקו את ה-container, כל מה שהיה בתוכו — מסד הנתונים, קבצי המשתמש, הלוג — נמחק. תלמדו named volume ו-bind mount, ותבינו מתי להשתמש באיזה.
  2. סודות שלא דולפים — כל אפליקציה שמתחברת ל-API חיצוני צריכה key. הדרך הלא-נכונה היא לכתוב את ה-key ב-Dockerfile. הדרך הנכונה היא --env-file .env, ולוודא ש-.env מוחרג גם מ-Git וגם מ-.dockerignore.

מה הלאה: בפרק 5 נחבר את האפליקציה למסד-נתונים ב-compose.yaml אחד, ובפרק 6 נדחוף את ה-image ל-ghcr.io ונפרוס אותו על VPS או Coolify. ה-Dockerfile, ה-volume, וקובץ ה-.env שתבנו היום הם הבסיס לכל זה.

בינוני 6 דקות הבנה persistence

למה הנתונים שלכם נעלמים — ה-writable layer ה-ephemeral

זה הרגע שבו רוב ה-Vibe Coders נתקלים בפעם הראשונה ב"באג שאף אחד לא מדבר עליו." אתם מריצים את האפליקציה ב-Docker, יוצרים משתמש חדש, רושמים הערה, מוסיפים קובץ — הכל עובד נהדר. עוצרים את ה-container (docker stop), מריצים מחדש (docker start) — הכל עדיין שם. ואז מוחקים את ה-container (docker rm) ומריצים docker run שוב — והמסד ריק, המשתמשים נעלמו, והקבצים חזרו לנקודת האפס של ה-image.

הרגע הזה הוא ה-aha moment של הפרק. זו לא באג. זו ארכיטקטורה.

ה-writable layer: הסיבה הטכנית

כל container שרץ בנוי משתי חלקים:

  1. Image layers (read-only): שכבות ה-image שיצרתם ב-docker build. אלה קבועות, חסרות-שינוי, ולא משתנות בריצה. כל הוראה ב-Dockerfile יוצרת layer (FROM, COPY, RUN, וכו').
  2. Writable layer (ephemeral — זמני): שכבה דקה מעל ה-image layers, שבה נכתבים כל השינויים שהאפליקציה עושה בזמן ריצה. יצירת קבצים, כתיבה למסד נתונים, שינוי הגדרות — הכל הולך לכאן.

הבעיה: ה-writable layer חי בתוך ה-container. כשאתם עושים docker rm <container>, אתם מוחקים את ה-container — ואיתו את ה-writable layer. ה-image layers נשארים (הם לא שייכים ל-container), אבל כל מה שהיה בתוך ה-container — נמחק. Container הוא disposable by design (חד-פעמי בעיצובו): הוא נועד לרוץ, למות, ולהיות מוחלף באחד חדש שזהה לו.

זה הפילוסופיה של Docker: containers הם כמו תהליכים — לא משאבים. אתם לא שומרים על תהליך, אתם שומרים על מה שהתהליך יצר. הפתרון: לאחסן את "מה שנוצר" מחוץ ל-container, במקום ש-Docker לא מוחק.

שתי דרכים לשמור נתונים מחוץ ל-container

Docker נותן שתי דרכים עיקריות להוציא נתונים מה-container:

ההבחנה הזו — volume מנוהל מול mount של מארח — היא אחת ההחלטות הכי מבלבלות למתחילים. בואו נפרק אותה לעומק.

בינוני 9 דקות core persistence

named volume — האחסון המנוהל ששורד גם אחרי docker rm

named volume (בקיצור — volume) הוא אזור אחסון ש-Docker יוצר ומנהל בעצמו. אתם נותנים לו שם, Docker יודע איפה לשמור את הנתונים במארח (ב-/var/lib/docker/volumes/<name>/_data), ואתם לא צריכים לדאוג לנתיבים. כשאתם מצמידים את ה-volume ל-container, כל מה שה-container כותב לנתיב המצוין — נשמר ב-volume, לא ב-writable layer.

התחביר בפועל

אתם משתמשים ב--v (או --volume, שניהם זהים) עם הפורמט:

docker run -v volume_name:/path/inside/container image_name

לדוגמה, להריץ PostgreSQL עם volume בשם pgdata שישמור את הנתונים ב-/var/lib/postgresql/data (הנתיב ש-PostgreSQL מצפה לו):

docker run -d --name my-postgres \
  -e POSTGRES_PASSWORD=secret123 \
  -v pgdata:/var/lib/postgresql/data \
  -p 5432:5432 \
  postgres:16

החלק החשוב: pgdata הוא השם של ה-volume, ו-/var/lib/postgresql/data הוא הנתיב בתוך ה-container שבו PostgreSQL שומר את הנתונים. שני החלקים נדרשים: בלי שם ה-volume, Docker יוצר anonymous volume עם hash אקראי; בלי הנתיב, Docker לא יודע לאן לחבר.

הוכחה — הנתונים שורדים את המחיקה

הנה הסצנה הקלאסית שתעשו היום, בשלוש דקות:

  1. מריצים את ה-container עם volume (הפקודה למעלה).
  2. מתחברים למסד, יוצרים טבלה, מכניסים שורה, יוצאים.
  3. עוצרים ומוחקים את ה-container: docker stop my-postgres && docker rm my-postgres.
  4. מריצים מחדש עם אותה פקודה (כולל -v pgdata:/var/lib/postgresql/data).
  5. מתחברים שוב — הטבלה והשורה שם.

הסיבה: ה-volume pgdata לא היה שייך ל-container שמחקתם. הוא חי בנפרד, במערכת הקבצים של המארח, ורק ה-container "ראה" אותו דרך ה-mount. כשמחקתם את ה-container, ה-volume נשאר. כשהרצתם container חדש והצמדתם את אותו volume — ה-container החדש ראה את אותם נתונים.

פקודות ניהול של volumes

כמה פקודות שימושיות שתחזרו עליהן:

named volume מול anonymous volume

כשאתם כותבים docker run -v /path/in/container image (בלי שם לפני הנקודתיים), Docker יוצר anonymous volume עם hash אקראי כמו a1b2c3d4e5f6. הוא עובד, אבל אין לכם שם ידידותי לזכור, וקשה לנהל אותו. תמיד תנו שם — זה עולה שתי אותיות וחוסך בלאגן אחר כך.

בינוני 7 דקות פיתוח hot reload

bind mount — הגשר בין המארח ל-container לפיתוח

bind mount עושה משהו שונה לגמרי: הוא ממפה תיקייה מהמארח שלכם (הלפטופ, המקום שבו אתם עורכים קוד עם VSCode) לתוך ה-container, בזמן אמת. כל שינוי בקבצים במארח — משתקף מיד ב-container. זה הבסיס של hot reload — עריכת קוד בלי לבנות מחדש.

תחביר ה-bind mount

הפורמט דומה ל-volume, אבל במקום שם — נתיב מוחלט במארח:

docker run -v /absolute/path/on/host:/path/in/container image_name

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

docker run -d --name myapp-dev \
  -v $(pwd):/app \
  -p 3000:3000 \
  myapp:latest

החלק $(pwd):/app אומר: "קח את התיקייה הנוכחית במארח (שבה רץ ה-Dockerfile), ומפה אותה ל-/app בתוך ה-container." אם אתם עורכים את app.py במארח עם VSCode, ה-container רואה את השינוי מיד — בלי docker build מחדש, בלי docker run מחדש. רוב ה-frameworks המודרניים (Next.js עם Fast Refresh, Flask עם debug mode, FastAPI עם --reload) רואים את השינוי וטוענים את הקוד מחדש אוטומטית.

מתי bind mount, מתי named volume

הכלל פשוט:

המלכודת הקלאסית: להריץ מסד-נתונים עם bind mount של תיקייה במארח. זה עובד בפיתוח, אבל ברגע שתעלו לשרת, התיקייה הזו לא קיימת — והמסד לא עולה. מסדי-נתונים תמיד על named volume.

הפקודה המעשית — dev מול prod על אותו Dockerfile

הנה ההבדל המעשי שתרגישו ממש בתרגיל 2:

# Production: image אפוי, אין קוד במארח בכלל
docker run -d --name myapp-prod \
  -v mydata:/var/lib/myapp \
  -p 3000:3000 \
  myapp:v1.0

# Development: bind mount לקוד, hot reload
docker run -d --name myapp-dev \
  -v $(pwd):/app \
  -p 3000:3000 \
  -e NODE_ENV=development \
  myapp:latest

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

הזהירות הקריטית: container הוא disposable

עכשיו, אחרי שאתם יודעים על volumes, הנה הכלל שיחסוך לכם שעות תסכול: אל תערכו קבצים בתוך container רץ עם docker exec -it <id> sh. גם אם זה עובד עכשיו, השינויים שלכם חיים ב-writable layer — וייעלמו ברגע שתעשו docker rm או docker pull של image חדש. אם אתם צריכים לשנות משהו — תקנו את ה-Dockerfile, תקנו את הקוד, או השתמשו ב-bind mount. לא ב-docker exec.

בינוני 8 דקות core configuration

משתני סביבה ב-runtime — -e, --env-file, ו-ENV ב-Dockerfile

עכשיו עוברים לחצי השני של הפרק: config. כל אפליקציה אמיתית צריכה הגדרות — כתובת מסד נתונים, API key, פורט, feature flag, סיסמה. השאלה היא לא אם צריך config, אלא איך מעבירים אותו ל-container בלי לדלוף אותו למקום הלא נכון.

שלוש דרכים להעביר config

יש שלוש דרכים עיקריות, וכל אחת מתאימה למקרה אחר:

  1. -e KEY=value — משתנה בודד. מועבר inline בפקודת ה-docker run. טוב לבדיקה מהירה, גרוע לפרודקשן כי הוא נראה בהיסטוריית ה-shell.

    docker run -e POSTGRES_PASSWORD=secret123 postgres
  2. --env-file .env — קובץ שלם של משתנים. Docker קורא את הקובץ ומעביר כל שורה KEY=value כמשתנה סביבה. זו הדרך המומלצת לפרודקשן.

    # .env
    POSTGRES_PASSWORD=secret123
    API_KEY=sk-abc123
    DATABASE_URL=postgres://user:pass@db:5432/mydb
    
    # הרצה
    docker run --env-file .env myapp
  3. ENV ב-Dockerfile — ערך שנקבע בבנייה. הערך נכנס ל-image ונשאר ב-layer. מתאים לערכים לא-רגישים שלא משתנים בין סביבות (למשל גרסת אפליקציה, תיקיית ברירת-מחדל).

    # Dockerfile
    ENV APP_VERSION=1.0.0
    ENV NODE_ENV=production
    ENV PORT=3000

הכלל שיעזור לכם לבחור:

ה-Application קורא את המשתנים

בתוך ה-container, האפליקציה קוראת את המשתנים עם המנגנון הסטנדרטי של השפה שלה. כמה דוגמאות מהירות:

# Python (Flask / FastAPI)
import os
db_url = os.environ.get("DATABASE_URL")
api_key = os.environ["API_KEY"]  # KeyError אם חסר

# Node.js
const dbUrl = process.env.DATABASE_URL;
const apiKey = process.env.API_KEY;

# Shell
echo "DB host is $DATABASE_HOST"

העיקרון: ה-container לא צריך לדעת איך המשתנים הגיעו. הוא רק רואה אותם בסביבה שלו. זה מה שהופך את --env-file לכל-כך גמיש: אותו image, קבצי .env שונים, סביבות שונות.

קובץ .env — המבנה הנכון

קובץ .env הוא פשוט רשימת שורות KEY=value, בלי רווחים סביב ה-=:

# .env — local development, NEVER commit
NODE_ENV=development
DATABASE_URL=postgres://myuser:mypass@localhost:5432/mydb
API_KEY=sk-test-abc123
JWT_SECRET=super-secret-token-rotation
PORT=3000

והקובץ המקבילה, .env.example, נשמר ב-Git בלי הסודות:

# .env.example — checked into git, no real values
NODE_ENV=development
DATABASE_URL=postgres://user:pass@localhost:5432/dbname
API_KEY=your-api-key-here
JWT_SECRET=generate-a-random-string
PORT=3000

כך כל מי שעובד על הפרויקט יודע אילו משתנים נדרשים, בלי לראות את הערכים האמיתיים.

בינוני 5 דקות הבחנה קריטי

build ARG מול runtime ENV — ההבחנה שמונעת אסון

זו ההבחנה שהכי קל לבלבל בה, והבלבול הזה הוא מקור מס' 1 לדליפת סודות. שני directives נראים דומים, אבל הם חיים במקומות שונים לגמרי:

ההבדל בפועל

תראו את שתי הדוגמאות האלה וההבדל יהפוך ברור:

# Dockerfile — שימוש לא נכון
ARG API_KEY
ENV API_KEY=$API_KEY     # ← הערך נכנס ל-image layer!

# Dockerfile — שימוש נכון
# אין ARG, אין ENV. הערך מועבר בריצה:
# docker run --env-file .env myapp

בגרסה הראשונה, מי שיריץ docker history myapp יראה את ה-ARG ואת ה-ENV, ויוכל לחלץ את ה-key מה-layers. ה-key "נאפה" לתוך ה-image. זו דליפה קבועה — גם אם תמחקו את השורה מה-Dockerfile אחר כך, ה-image שכבר נבנה מכיל את הסוד.

בגרסה השנייה, ה-image לא מכיר את ה-key. הוא מקבל אותו רק כשמריצים docker run --env-file .env. docker history לא יראה שום דבר רגיש.

טבלת ההבחנה המהירה

מאפיין ARG ENV / -e / --env-file
מתי זמין רק בזמן docker build רק בזמן docker run
איך מועבר --build-arg KEY=value -e KEY=value / --env-file .env
איפה נשמר image layer (קבוע!) רק בסביבת ה-container הרץ
גלוי ב-docker history כן לא
מתאים לסודות לעולם לא כן (עם --env-file)
מתאים לגרסת build כן (למשל --build-arg VERSION=1.2) לא

הכלל: ARG לדברים שמשפיעים על הבנייה (גרסה, target platform, flag של compiler). ENV/-e/--env-file לדברים שמשפיעים על הריצה (config, סודות, endpoints). אם אתם רואים ARG API_KEY ב-Dockerfile — זה אות אזהרה אדום.

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

הכלל הקרדינלי: אף פעם לא לאפות סודות ל-image

הגענו ללב הפרק. זה הכלל החשוב ביותר בקורס כולו — חשוב יותר מ-cache, יותר מ-multi-stage, יותר מ-non-root USER. אם תזכרו רק דבר אחד, שזה יהיה זה.

הבעיה: סודות ש"נאפים" ל-image

"אפיית סוד" (secret baking) זה המצב שבו משתנה רגיש — API key, סיסמת מסד, token — נכנס לתוך ה-image בזמן הבנייה, ונשאר שם לנצח. הסיבות הנפוצות:

  1. ENV API_KEY=sk-abc123 ב-Dockerfile. הערך נכתב ל-layer ונשאר ב-image.
  2. ARG API_KEY=sk-abc123 ואז ENV API_KEY=$API_KEY. אותה בעיה בשני שלבים.
  3. COPY .env /app/.env ב-Dockerfile בלי .dockerignore. הקובץ כולו נכנס.
  4. RUN curl -H "Authorization: Bearer $API_KEY" ... ב-Dockerfile — הערך מופיע בהיסטוריית ה-layer.
  5. Private key, certificate, או credentials file שמועתקים עם COPY.

בכל המקרים האלה, docker history myapp יראה את הערך, וכל מי שיכול למשוך את ה-image — יכול לחלץ אותו.

המספר שצריך לזכור: 48%

מחקרים על אפליקציות שפורסמו לציבור (כולל אלה שנבנו עם כלי AI) מראים שכ-48% מהן מכילות סוד שדלף לתוך ה-image — API key של OpenAI, token של GitHub, סיסמת מסד. זו לא בעיה נדירה; זו ברירת המחדל כשלא מקפידים. רוב המקרים האלה קרו למי שפשוט לא ידע ש-ENV ב-Dockerfile שומר את הערך ב-layer. הם חשבו שזה רק "הגדרה זמנית." לא. זה קבוע.

ההגנה — ארבע שכבות

ההגנה הנכונה היא בת ארבע שכבות, והן חייבות לעבוד יחד:

  1. אל תכתבו סודות ב-Dockerfile. בלי ENV, בלי ARG, בלי COPY .env בתוך ה-Dockerfile. אם צריך config בבנייה, תכתבו placeholder שמוחלף בריצה.

  2. החריגו .env מ-.gitignore. הקובץ לא יכול להגיע ל-Git. הוסיפו .env ל-.gitignore בראש הפרויקט, וצרו .env.example כתבנית ציבורית.

  3. החריגו .env מ-.dockerignore. גם אם הקובץ נשאר במארח (למשל במהלך פיתוח), docker build לא יעתיק אותו לתוך ה-build context. docker init יוצר .dockerignore עם .env כברירת-מחדל — אל תמחקו אותו.

  4. העבירו את הסוד בריצה. docker run --env-file .env myapp, או environment: ב-compose (פרק 5). הערך נשאר במארח, מועבר ל-container רק כשצריך, ולא נכנס ל-image.

כל ארבע השכבות נדרשות. אם תפספסו אחת — דליפה.

בינוני 5 דקות best practice אבטחה

file-based secrets — סודות ב-/run/secrets ו-best practices

העברת סודות דרך משתני סביבה היא הסטנדרט ברוב המקרים, אבל יש לה חיסרון: משתני סביבה יכולים להופיע בלוגים, ב-stack traces, וב-dumps של ה-process. cat /proc/1/environ מציג את כולם לכל מי שיש לו גישה ל-container. לפעמים — במיוחד בפרודקשן אמיתי — זה לא מספיק.

החלופה: secrets כקבצים זמניים

Docker תומך ב-mount של סודות כקבצים בתוך ה-container, בנתיב /run/secrets/<name>. הקבצים האלה:

התחביר עם Docker Compose (פרק 5):

services:
  myapp:
    image: myapp:latest
    secrets:
      - api_key
      - db_password

secrets:
  api_key:
    file: ./secrets/api_key.txt
  db_password:
    file: ./secrets/db_password.txt

בתוך ה-container, הקבצים מופיעים ב-/run/secrets/api_key ו-/run/secrets/db_password. האפליקציה קוראת אותם עם open('/run/secrets/api_key').read().strip() במקום os.environ['API_KEY'].

מתי זה הכרחי?

לרוב ה-Vibe Coders, --env-file .env הוא מספיק. file-based secrets מתאימים ל:

עבור הפרויקט הראשון שלכם, --env-file .env הוא הצעד הנכון. כשתעברו לפרודקשן אמיתי בעוד כמה חודשים, תוסיפו file-based secrets או Vault.

בינוני 4 דקות אימות קריטי

לאמת שאין דליפה — docker history ו-inspect

לפני שאתם דוחפים image ל-registry (פרק 6) או נותנים למישהו אחר למשוך אותו — תעצרו לבדיקה אמיתית. שתי פקודות חובה:

docker history — רואה את כל ה-layers

הפקודה docker history myapp:latest (או --no-trunc לראות את הפקודה המלאה) מציגה את כל ה-layers של ה-image, ולכל layer — את ההוראה שיצרה אותו. חפשו:

אם אתם רואים משהו רגיש — תבנו מחדש. אין דרך "למחוק" סוד מ-image קיים. כל מה שנכנס ל-layer — נשאר.

docker inspect — רואה env vars ו-mounts

הפקודה docker inspect myapp:latest (או docker inspect <running-container-id>) מחזירה JSON עם כל המטא-דאטה של ה-image או ה-container. חפשו את השדות:

בדיקה מהירה למצוא סודות שדלפו:

docker history myapp:latest --no-trunc | grep -iE "key|secret|token|password"
docker inspect myapp:latest | grep -iE "key|secret|token|password"

אם ה-grep מחזיר שורות — תבדקו כל אחת. שורה תמימה כמו ENV NODE_VERSION=20 היא תקינה; ENV STRIPE_KEY=sk_live_... — אסון.

רשת הביטחון האוטומטית — docker scout cves

בפרק 6 נרחיב על docker scout (כלי סריקת חולשות שמגיע עם Docker Desktop). הוא בונה SBOM (Software Bill of Materials — רשימת כל החבילות ב-image) ומסמן חולשות אבטחה ידועות. הוא לא יתפוס סוד שדלף — לזה אתם צריכים docker history — אבל הוא יתפוס base image ישן עם חולשות.

בינוני 12 דקות capstone הרצה מלאה

Capstone — האפליקציה רצה עם volume ו-env בפועל

עכשיו הכל מתחבר. בואו נריץ את התרחיש האמיתי: אפליקציה שמתחברת למסד-נתונים, סודות מועברים ב-runtime, הכל שורד restart. זו הבסיס לכל מה שיבוא בפרק 5 (compose) ובפרק 6 (deploy).

המסגרת: שני containers, שיתוף env ו-volume

התרחיש המלא: PostgreSQL עם named volume למסד, ואפליקציה שמתחברת אליו עם סיסמה מ---env-file. שני ה-containers עצמאיים; הקישור ביניהם הוא רק דרך הרשת וה-config.

# 1. הרצת PostgreSQL עם volume
docker run -d --name my-postgres \
  -e POSTGRES_PASSWORD=secret123 \
  -e POSTGRES_DB=mydb \
  -v pgdata:/var/lib/postgresql/data \
  -p 5432:5432 \
  postgres:16

# 2. הכנסת נתונים (psql מהמארח, או דרך container נוסף)
docker exec my-postgres psql -U postgres -d mydb \
  -c "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT);"
docker exec my-postgres psql -U postgres -d mydb \
  -c "INSERT INTO users (name) VALUES ('Alice'), ('Bob');"

# 3. הרצת האפליקציה עם env file
cat > .env << EOF
DATABASE_URL=postgres://postgres:secret123@host.docker.internal:5432/mydb
API_KEY=sk-test-abc123
EOF

docker run -d --name myapp \
  --env-file .env \
  -p 3000:3000 \
  myapp:latest

# 4. הוכחת השרידות
docker stop my-postgres && docker rm my-postgres
docker run -d --name my-postgres-v2 \
  -e POSTGRES_PASSWORD=secret123 \
  -e POSTGRES_DB=mydb \
  -v pgdata:/var/lib/postgresql/data \
  -p 5432:5432 \
  postgres:16

docker exec my-postgres-v2 psql -U postgres -d mydb \
  -c "SELECT * FROM users;"
# Alice, Bob — שם.

הרגע הזה — ראיתם את הנתונים נשרדים אחרי מחיקת ה-container — הוא ה-aha moment של volumes. זה לא קסם; זה פשוט נתיב במערכת הקבצים של המארח ש-Docker שומר בין ריצות.

הקיצור לעתיד — bind mount בפיתוח

אותו Dockerfile, אבל בסביבת פיתוח, אתם מחליפים את ה-volume ב-bind mount של קוד המקור:

# Development — קוד מהמארח, hot reload
docker run -d --name myapp-dev \
  -v $(pwd):/app \
  -p 3000:3000 \
  --env-file .env \
  myapp:latest

# עריכה ב-VSCode → שינוי מיידי ב-container
# (בהנחה שהאפליקציה רצה עם --reload / nodemon / debug mode)

ההבדל היחיד: pgdata:/var/lib/postgresql/data (named volume) הפך ל-$(pwd):/app (bind mount). ה-image אותו image, הקונפיג אותו קונפיג, הסביבה שונה.

אימות סופי — האם הסודות באמת בחוץ?

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

# 1. האם .env מוחרג מ-Git?
cat .gitignore | grep "^\.env$"
# צריך להציג שורה אחת: .env

# 2. האם .env מוחרג מ-Docker build?
cat .dockerignore | grep "^\.env"
# צריך להציג שורה אחת: .env

# 3. האם ה-image נקי מסודות?
docker history myapp:latest --no-trunc | grep -iE "key|secret|token|password"
# צריך להציג רק ENV בטוחים (PORT, NODE_ENV) או רשימה ריקה

אם שלוש הבדיקות עוברות — אתם במקום הנכון. ה-volume ישמור, ה-config יזרום, הסודות יישארו במארח. אתם מוכנים לפרק 5.

תרגיל 1 — volume ששורד rm (15 דקות)

המטרה: להוכיח בעצמכם ש-named volume שומר על נתונים גם אחרי מחיקת ה-container, ולהבין את ההבדל מול containers בלי volume.

מה תעשו:

  1. הריצו: docker run -d --name test-ephemeral -e POSTGRES_PASSWORD=pass postgres:16 (בלי volume — שימו לב!).
  2. הכניסו נתונים: docker exec test-ephemeral psql -U postgres -c "CREATE TABLE t (x INT); INSERT INTO t VALUES (1);"
  3. מחקו: docker stop test-ephemeral && docker rm test-ephemeral.
  4. הריצו שוב בלי volume: docker run -d --name test-ephemeral-v2 -e POSTGRES_PASSWORD=pass postgres:16.
  5. נסו: docker exec test-ephemeral-v2 psql -U postgres -c "SELECT * FROM t;". צפו לשגיאה: relation "t" does not exist — הנתונים אבדו.
  6. עכשיו הריצו עם volume: docker run -d --name test-volume -e POSTGRES_PASSWORD=pass -v pgdata:/var/lib/postgresql/data postgres:16.
  7. הכניסו נתונים: docker exec test-volume psql -U postgres -c "CREATE TABLE t (x INT); INSERT INTO t VALUES (42);"
  8. מחקו: docker stop test-volume && docker rm test-volume.
  9. הריצו שוב עם אותו volume: docker run -d --name test-volume-v2 -e POSTGRES_PASSWORD=pass -v pgdata:/var/lib/postgresql/data postgres:16.
  10. בדקו: docker exec test-volume-v2 psql -U postgres -c "SELECT * FROM t;". צפו לראות את השורה: x = 42.
  11. נקו: docker stop test-volume-v2 && docker rm test-volume-v2 && docker volume rm pgdata.

תוצאה צפויה: בלי volume — הנתונים אבדו, הטבלה לא קיימת. עם volume — הנתונים שרדו גם אחרי מחיקת ה-container. ראיתם את ההבדל בעיניים, לא רק קראתם עליו.

תרגיל 2 — bind mount ל-hot reload בפיתוח (15 דקות)

המטרה: לחוות את ה-hot reload — לערוך קוד במארח ולראות את השינוי ב-container בלי rebuild.

מה תעשו:

  1. צרו תיקייה ריקה: mkdir -p /tmp/hotreload && cd /tmp/hotreload.
  2. צרו קובץ app.py פשוט (לדוגמה, Flask):
    from flask import Flask
    app = Flask(__name__)
    
    @app.route("/")
    def hello():
        return "Hello v1 — first version"
    
    if __name__ == "__main__":
        app.run(host="0.0.0.0", port=3000, debug=True)
  3. צרו requirements.txt: flask==3.0.0.
  4. צרו Dockerfile פשוט (או השתמשו ב-זה ש-docker init יצר).
  5. בנו: docker build -t hotreload:test ..
  6. הריצו עם bind mount: docker run -d --name hot -v $(pwd):/app -p 3000:3000 hotreload:test.
  7. בדקו בטרמינל אחר: curl http://localhost:3000. צפו: "Hello v1".
  8. עכשיו, ב-VSCode (או כל עורך), פתחו את app.py ושנו את "Hello v1" ל-"Hello v2 — live reload works". שמרו.
  9. בלי לעצור את ה-container, הריצו curl http://localhost:3000 שוב. צפו: "Hello v2".
  10. שנו שוב ל-"Hello v3" ובדקו. אם Flask עם debug mode רץ — השינוי אמור להיות מיידי.
  11. נקו: docker stop hot && docker rm hot && docker rmi hotreload:test.

תוצאה צפויה: שינוי ב-VSCode משתקף ב-container תוך שנייה, בלי rebuild. זה ה-hot reload שמייצר cycle מהיר של "כתבתי → ראיתי → תיקנתי." בלי bind mount, הייתם צריכים docker build + docker run אחרי כל שינוי.

תרגיל 3 — סודות ב-runtime בלי לדלוף (15 דקות)

המטרה: להעביר API key ל-container דרך --env-file ולאמת שהוא לא נכנס ל-image.

מה תעשו:

  1. צרו תיקייה חדשה: mkdir -p /tmp/secrets-test && cd /tmp/secrets-test.
  2. צרו .env:
    API_KEY=sk-test-abc123
    DB_PASSWORD=hunter2
    PORT=3000
  3. ודאו: grep "^\.env$" ../your-project/.gitignore (או צרו .gitignore חדש עם .env בשורה הראשונה).
  4. ודאו: grep "^\.env$" .dockerignore (אם אין — צרו עם .env ו-.git).
  5. צרו app.py שמדפיס את המשתנים:
    import os
    print("API_KEY =", os.environ.get("API_KEY"))
    print("DB_PASSWORD =", os.environ.get("DB_PASSWORD"))
    print("PORT =", os.environ.get("PORT"))
  6. צרו Dockerfile מינימלי שמעתיק את app.py ומריץ אותו עם Python.
  7. בנו: docker build -t secret-test:1.0 ..
  8. הריצו: docker run --rm --env-file .env secret-test:1.0. צפו לראות את שלושת הערכים.
  9. בדקו את ה-image: docker history secret-test:1.0 --no-trunc. חפשו "API_KEY" או "sk-test" — צריך להיות ריק. הסוד לא ב-image.
  10. בדקו גם: docker inspect secret-test:1.0 | grep -i "api_key\|sk-test". צריך להיות ריק.
  11. בדקו גם: docker run --rm secret-test:1.0 (בלי --env-file). צפו לראות None במקום הערכים — ה-image לא מכיר את הסודות.
  12. נקו: docker rmi secret-test:1.0 && rm .env.

תוצאה צפויה: --env-file .env מעביר את הסודות בריצה, והם זמינים ב-container. docker history ו-docker inspect לא רואים אותם. בלי --env-file, הערכים הם None. ארבע השכבות של ההגנה עובדות.

תרגיל 4 — אינטגרציה: volume + env בפועל (20 דקות)

המטרה: להריץ תרחיש מלא — מסד-נתונים עם volume, אפליקציה שמתחברת אליו עם סיסמה מ---env-file, ולהוכיח שהכל שורד restart.

מה תעשו:

  1. הריצו PostgreSQL עם volume: docker run -d --name pg -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql/data postgres:16.
  2. צרו .env:
    DATABASE_URL=postgres://postgres:secret@host.docker.internal:5432/postgres
    API_KEY=sk-test-xyz789
  3. צרו app.py שמתחברת למסד ויוצרת טבלה:
    import os
    import psycopg2
    conn = psycopg2.connect(os.environ["DATABASE_URL"])
    cur = conn.cursor()
    cur.execute("CREATE TABLE IF NOT EXISTS messages (id SERIAL PRIMARY KEY, text TEXT)")
    cur.execute("INSERT INTO messages (text) VALUES ('hello from container')")
    conn.commit()
    cur.execute("SELECT * FROM messages")
    for row in cur.fetchall():
        print(row)
    conn.close()
  4. צרו requirements.txt: psycopg2-binary==2.9.9.
  5. צרו Dockerfile שמתקין את psycopg2-binary ומריץ את app.py.
  6. ודאו .gitignore כולל .env, ו-.dockerignore כולל .env.
  7. בנו: docker build -t app-with-db:1.0 ..
  8. הריצו: docker run --rm --env-file .env --add-host=host.docker.internal:host-gateway app-with-db:1.0. צפו לראות את השורה (1, 'hello from container') בפלט.
  9. הריצו שוב. הפעם הטבלה כבר קיימת, אז ה-CREATE IF NOT EXISTS ידלג, ה-INSERT יוסיף שורה שנייה, וה-SELECT יחזיר שתי שורות.
  10. עכשיו מחקו רק את ה-container של ה-DB (ה-volume חי!): docker stop pg && docker rm pg. הריצו שוב: docker run -d --name pg -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql/data postgres:16.
  11. הריצו את האפליקציה: docker run --rm --env-file .env --add-host=host.docker.internal:host-gateway app-with-db:1.0. צפו לראות את שתי השורות שכבר היו שם, ועוד אחת חדשה. ה-volume שמר.
  12. אמתו: docker history app-with-db:1.0 --no-trunc | grep -iE "secret|sk-test|password". צריך להיות ריק.
  13. נקו: docker stop pg && docker rm pg && docker volume rm pgdata && docker rmi app-with-db:1.0.

תוצאה צפויה: ה-DB רץ עם volume, הסיסמה מועברת ב---env-file, הנתונים שורדים מחיקה של ה-container, ו-docker history נקי. אתם יודעים עכשיו להריץ אפליקציה אמיתית שמתחברת למסד-נתונים אמיתי, עם סודות אמיתיים, בלי לדלוף כלום.

Framework — "איזה mount מתאים למה?"

ההחלטה בין named volume, bind mount, או בלי mount בכלל — תלויה בשלוש שאלות:

  1. האם הנתונים צריכים לשרוד את ה-container?

    • כן (מסד, uploads, cache, state) → named volume בהכרח. בלי זה, הכל ייעלם ב-docker rm.
    • לא (חישוב זמני, build artifacts, scratch space) → בלי mount. ה-container הוא disposable; זה בסדר.
  2. האם אתם צריכים לערוך את הנתונים מהמארח בזמן אמת?

    • כן (פיתוח, עריכת קוד, hot reload) → bind mount לתיקיית הקוד.
    • לא (production, staging) → named volume.
  3. האם ה-container ירוץ במכונה אחרת (VPS, CI, חבר)?

    • כןnamed volume. לעולם לא bind mount בפרודקשן — הנתיב /Users/yourname/project לא קיים ב-VPS.
    • לא (רק על הלפטופ) → bind mount בסדר לפיתוח.

הכלל המעשי: קוד בפיתוח → bind mount. נתונים בכל מקום → named volume. אם אתם זוכרים רק את המשפט הזה — אתם מסודרים.

Framework — "איך להעביר config נכון?"

ה-flow הנכון להעברת config ל-container, בארבעה צעדים:

  1. זהה מה רגיש ומה לא.

    • רגיש: API keys, סיסמאות, tokens, private keys, OAuth secrets.
    • לא רגיש: ports, hostnames, feature flags, log levels, NODE_ENV.
  2. בחר מנגנון העברה לפי רגישות.

    • רגיש--env-file .env (או file-based secrets לפרודקשן מחמיר).
    • לא רגיש אך משתנה בין סביבות--env-file .env עם ערך שונה לכל סביבה.
    • לא רגיש וקבועENV ב-Dockerfile.
  3. ודא שלוש שכבות הגנה.

    • הסוד לא ב-Dockerfile (אין ENV KEY=value רגיש).
    • הסוד לא ב-Git (.env ב-.gitignore; .env.example במקומו).
    • הסוד לא ב-image (.env ב-.dockerignore; docker history נקי).
  4. אמת לפני push.

    לפני docker push (פרק 6), הריצו:

    docker history myapp:latest --no-trunc | grep -iE "key|secret|token|password"

    אם יש שורות שלא צריכות להיות שם — תבנו מחדש.

הכלל: כל סוד שראה אור בתוך image — דלף. אין דרך למחוק. תפסו את זה לפני ה-push.

Do Now — 4 דקות (רשימת volumes קיימים)

פתחו terminal והריצו docker volume ls. תראו את כל ה-volumes ש-Docker יצר אוטומטית בפרויקטים קודמים. עכשיו הריצו docker volume prune (אחרי שווידאתם שאתם לא צריכים אותם) כדי לנקות. תוצאה צפויה: רשימה הרבה יותר קצרה, והבנה ברורה ש-named volumes שורדים docker system prune עד שאתם מוחקים אותם ספציפית. זו הסיבה שהם בטוחים לפרודקשן — אתם צריכים להחליט למחוק, לא Docker.

Do Now — 6 דקות (לבדוק את .gitignore של הפרויקט שלכם)

פתחו את .gitignore בפרויקט שלכם. חפשו את .env — אם הוא לא שם, הוסיפו אותו עכשיו. בנוסף, חפשו .env.example — אם הוא קיים, פתחו אותו ובדקו שהוא באמת ריק מסודות (רק placeholders). אם אין, צרו אחד. תוצאה צפויה: אתם יודעים ב-100% שהסודות שלכם לא ב-Git. הצעד הזה לבדו חוסך 90% מדליפות הסודות בעולם.

Do Now — 4 דקות (לבדוק את .dockerignore)

פתחו את .dockerignore בפרויקט. חפשו .env, .git, node_modules (או __pycache__). אם משהו חסר — הוסיפו. תוצאה צפויה: docker build שלכם מהיר יותר (פחות קבצים ב-build context), ה-image קטן יותר, והסודות לא נכנסים. זו הגנה שלוקחת 30 שניות להגדיר וחוסכת אסון.

Do Now — 5 דקות (לבדוק את ה-image הנוכחי שלכם)

הריצו docker images ובחרו את ה-image הראשי של הפרויקט. הריצו docker history myapp:latest --no-trunc. עברו על כל שורה וחפשו: key, secret, token, password, credential. אם יש — תאתרו את המקור (Dockerfile? COPY? ENV?) ותקנו. תוצאה צפויה: רשימה נקייה, או רשימה ארוכה של פעולות לתיקון לפני ה-push ל-registry בפרק 6.

Do Now — 7 דקות (להריץ container עם --env-file ולראות את ההבדל)

צרו קובץ .env עם שני משתנים: MY_NAME=YourName ו-MY_GREETING=Shalom. צרו app.py שמדפיס את שניהם. בנו image שלא מכיל את הערכים (רק את הקוד). הריצו פעמיים: פעם אחת בלי --env-file (צפו ל-None), פעם שנייה עם --env-file .env (צפו לערכים). תוצאה צפויה: אתם רואים בעיניים שה-image לא יודע את הערכים — הם מגיעים רק מהקובץ בריצה. זו ההוכחה הקונקרטית להפרדה.

Do Now — 5 דקות (לראות את ה-Writable Layer בעיניים)

הריצו container פשוט: docker run -d --name demo -e POSTGRES_PASSWORD=pass postgres:16. התחברו: docker exec -it demo sh. צרו קובץ: echo "this is ephemeral" > /tmp/test.txt. צאו (exit). מחקו: docker rm -f demo. הריצו אותו container שוב. התחברו: docker exec -it $(docker ps -aq | head -1) sh. בדקו: ls /tmp/test.txt — לא קיים. תוצאה צפויה: ראיתם בעיניים שהקובץ נעלם עם ה-container. עכשיו תבינו למה volumes קיימים.

Do Now — 8 דקות (להריץ volume + container ולראות שרידות)

הריצו: docker run -d --name pg1 -e POSTGRES_PASSWORD=pass -v mydata:/var/lib/postgresql/data postgres:16. צרו טבלה והכניסו שורה דרך docker exec. מחקו: docker rm -f pg1. הריצו שוב: docker run -d --name pg2 -e POSTGRES_PASSWORD=pass -v mydata:/var/lib/postgresql/data postgres:16. בדקו: docker exec pg2 psql -U postgres -c "SELECT * FROM your_table;". תוצאה צפויה: השורה שם. ה-volume שרד את המחיקה. אתם יכולים לחזור על התרגיל הזה עוד 10 פעמים — המסקנה תהיה זהה. הבנה דרך חזרה היא הבנה אמיתית.

Do Now — 6 דקות (לבדוק את host.docker.internal)

הריצו: docker run --rm alpine sh -c "ping -c 1 host.docker.internal" (או nslookup host.docker.internal). תוצאה צפויה: הכתובת תהיה 192.168.65.2 ב-Docker Desktop או 10.0.2.2 ב-Linux. זו הכתובת שבה container רואה את המארח שלכם. תצטרכו אותה בתרגיל 4 כשאתם מחברים את האפליקציה למסד שרץ במארח.

טעות נפוצה: להריץ מסד-נתונים בלי named volume

"אני אשמור הכל בקבצים רגילים בתוך ה-container" — לא. ה-writable layer הוא ephemeral, והוא נמחק עם ה-container. אחרי docker rm, docker system prune, או אפילו docker pull של image חדש — המסד ריק. זו הסיבה המס' 1 ל"איבדתי את כל המידע של המשתמשים שלי" בעולם ה-Docker.

הסימפטום המדויק: אתם מריצים PostgreSQL בלי volume, יוצרים טבלאות ומכניסים נתונים, עושים restart — הכל בסדר. אחרי docker rm + docker run חדש — קסם, המסד ריק.

התיקון: תמיד -v pgdata:/var/lib/postgresql/data (או הנתיב של ה-DB הספציפי שלכם). ה-volume חי במארח, לא ב-container, ושורד כל מחיקה. תוך 10 שניות אתם מגינים על כל המידע של המשתמשים שלכם.

טעות נפוצה: לקבע API key ב-Dockerfile

"זה רק לפיתוח, אני אשנה את זה אחר כך" — לא. ENV API_KEY=sk-abc123 ב-Dockerfile אופה את ה-key ל-layer. גם אם תמחקו את השורה מה-Dockerfile בעוד שבוע, ה-image שכבר נבנה מכיל את הסוד. וכל מי שמושך את ה-image לומד את ה-key שלכם. זו הסיבה המס' 1 לדליפת API keys ב-GitHub Container Registry.

הסימפטום המדויק: אתם מוצאים sk-... ב-docker history myapp, או גרוע — ב-docker inspect. ה-key נחשף. החלפת ה-key ב-OpenAI היא הצעד הראשון; הבנה איך זה קרה היא הצעד השני.

התיקון: אל תכתבו סודות ב-Dockerfile. בכלל. לא ENV, לא ARG, לא RUN echo $KEY. העבירו את הסוד בריצה עם --env-file .env או -e. הוסיפו .env ל-.gitignore ול-.dockerignore. זה ה-baseline. הסטטיסטיקה: כ-48% מאפליקציות ה-AI שפורסמו לציבור מכילות סוד שדלף בדרך הזו. אל תהיו חלק מהסטטיסטיקה.

טעות נפוצה: לערוך קבצים בתוך container רץ ולחשוב שהם נשמרים

אתם docker exec -it <id> sh, פותחים קובץ, משנים שורה, שומרים, בודקים — עובד. עושים restart — השינוי נעלם. עושים rebuild — השינוי נעלם. כל שינוי שעשיתם בתוך ה-container חי ב-writable layer, וזה אומר שהוא ימות עם ה-container.

הסימפטום המדויק: אתם רואים את השינוי ב-docker exec אבל לא ב-docker run חדש. או שהשינוי נעלם אחרי docker pull של image חדש. זה לא באג; זו התנהגות נכונה של containers — הם חד-פעמיים.

התיקון: אם אתם רוצים לשנות קוד — תקנו את הקוד במארח, תבנו image חדש. אם אתם רוצים לשנות config — העבירו אותו ב--e או --env-file. אם אתם רוצים לשנות state שלא קשור לקוד — שמרו אותו ב-volume. אל תערכו בתוך container רץ ותצפו שזה יישמר. זו אנטי-דפוס ב-Docker.

טעות נפוצה: להשתמש ב-bind mount בפרודקשן

זה עובד נהדר בפיתוח: -v $(pwd):/app, עורכים ב-VSCode, רואים את השינוי ב-container. אז למה לא להעלות את זה לפרודקשן? כי $(pwd) במארח שלכם זה /Users/yourname/projects/coolapp, וב-VPS שלכם זה נתיב שלא קיים. ה-container פשוט לא יעלה.

הסימפטום המדויק: עובד בלפטופ, קורס ב-VPS עם Error: stat /Users/yourname/... או דומה. הפתרון המהיר של חבריכם: "תשנה את הנתיב ב-VPS." לא. זה לא פתרון — זה פתרון ad-hoc שישבור בכל deploy הבא.

התיקון: בפרודקשן, ה-image עומד בפני עצמו. אין תלות במבנה המארח. קוד נכנס ב-COPY . . בזמן build; state נשמר ב-named volume. bind mount הוא כלי פיתוח, לא כלי פרודקשן. אם אתם רואים -v /home/... ב-compose.yaml של production — תקנו.

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

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

שבועי (30 דקות, יום ראשון): סקירת הגדרות. עברו על .gitignore, .dockerignore, Dockerfile, וקובץ .env.example. האם .env בשני ה-ignore-ים? האם יש ENV KEY=value רגיש ב-Dockerfile? האם .env.example עדיין ריק מסודות? אם יש בעיה — תקנו השבוע.

לפני פרק 5 (סוף שבוע שני): ודאו שהאפליקציה שלכם רצה עם named volume למסד-נתונים, ועם --env-file .env לכל ה-config. docker history נקי. docker inspect לא מראה סודות. אם משהו נשבר — תקנו עכשיו. בפרק 5 נעבור ל-compose.yaml ונחבר הכל בקובץ אחד, ואתם צריכים image יציב שעובד עם volume ו-env.

חודשי (45 דקות, סוף חודש): audit מסודר. docker volume ls, docker ps -a, docker images. נקו מה שלא צריך. בדקו שאתם זוכרים איפה הסודות של כל פרויקט — ב-.env מקומי, לא ב-Git, לא ב-image. אם יש ספק — grep -r "API_KEY\|SECRET\|TOKEN" . (לא בתוך .env, בקבצים אחרים).

Check Yourself — 5 שאלות הבנה
  1. שאלה: אתם מריצים docker run --name db postgres:16 (בלי volume), יוצרים טבלה עם נתונים, ואז עושים docker rm db. מה קרה לנתונים, ולמה?

    תשובה: הנתונים אבדו. ה-writable layer של ה-container הכיל את הקבצים של PostgreSQL, וכשה-container נמחק — ה-layer נמחק איתו. ה-image layers נשארו (הם לא שייכים ל-container), אבל בלי volume — שום דבר מה-state של הריצה לא שרד. התיקון: הוסיפו -v pgdata:/var/lib/postgresql/data בפקודת ה-docker run. ה-volume חי במארח, נפרד מה-container, ושורד את המחיקה.

  2. שאלה: ב-Dockerfile שלכם יש ENV API_KEY=sk-abc123. אתם מוחקים את השורה הזו ועושים docker build מחדש עם תג myapp:v2. האם myapp:v1 עדיין חושף את ה-API key?

    תשובה: כן. ENV API_KEY=sk-abc123 אופה את הערך לתוך layer בזמן build. ה-layer הזה הוא חלק מ-myapp:v1, ומי שיש לו את ה-image יכול לחלץ את הערך עם docker history. העובדה שמחקתם את השורה מה-Dockerfile לא משנה את v1 — רק v2 יהיה נקי. הלקח: אין דרך "למחוק" סוד מ-image. צריך לבנות מחדש, וגם אז — כל מי שמשך את v1 עדיין יכול לראות. זו הסיבה שאסור לאפות סודות מלכתחילה.

  3. שאלה: אתם רוצים לערוך קוד ב-VSCode ולראות את השינוי ב-container מיד, בלי rebuild. עם איזה mount תשתמשו, ולמה?

    תשובה: bind mount. docker run -v $(pwd):/app myapp:latest ממפה את תיקיית הפרויקט במארח לתוך /app ב-container. כל שינוי ב-VSCode משתקף מיד ב-container, ואם האפליקציה רצה במצב debug/--reload, היא טוענת את הקוד מחדש אוטומטית. named volume לא יעבוד כי הוא לא קשור למארח — רק ל-Docker.

  4. שאלה: איזה מהבאים נכון: (א) ARG DB_PASSWORD=secret ב-Dockerfile מאובטח כי ARG זמין רק בזמן build. (ב) ENV DB_PASSWORD=secret ב-Dockerfile בטוח כי זה רק בתוך ה-container. (ג) docker run -e DB_PASSWORD=secret בטוח כי זה לא נכנס ל-image. (ד) --env-file .env בטוח בתנאי ש-.env ב-.gitignore וב-.dockerignore.

    תשובה: (א) לא נכון — ARG זמין בזמן build, אבל הערך נשמר ב-layer של ה-image. docker history רואה אותו. (ב) לא נכון — ENV נכנס ל-image גם כן, וגם זמין ב-container. (ג) נכון חלקית-e כן מעביר בריצה בלי להיכנס ל-image, אבל הסוד גלוי בהיסטוריית ה-shell וב-docker inspect. (ד) נכון — בתנאי שכל התנאים מתקיימים: .env ב-.gitignore (לא ב-Git), ב-.dockerignore (לא ב-image), ומועבר רק ב---env-file בריצה. זו הדרך הנכונה.

  5. שאלה: למה named volume עדיף על bind mount למסדי-נתונים, גם בפיתוח?

    תשובה: שלוש סיבות. (1) ניידות — bind mount תלוי בנתיב ספציפי במארח (/Users/yourname/...); named volume לא תלוי בכלום ועובד בכל מכונה. (2) ניהולdocker volume ls, docker volume inspect, docker volume rm נותנים לכם שליטה ברורה; bind mount הוא סתם תיקייה במארח שאף אחד לא יודע עליה. (3) ביצועים — ב-Mac וב-Windows, bind mount יכול להיות איטי בגלל ה-translation של ה-filesystem; named volume משתמש ב-native filesystem של המארח ומהיר יותר לקריאה/כתיבה של הרבה קבצים קטנים (מה שמסדי-נתונים עושים). חריג יחיד: אם אתם רוצים לערוך את קוד המקור מהמארח — שם bind mount הוא הכלי הנכון. אבל state של האפליקציה (מסד, uploads, cache) → תמיד named volume.

מה תפיקו בסוף הפרק
סיכום הפרק — 7 לקחים שייקחו אתכם הלאה
  1. ה-writable layer הוא ephemeral. כל שינוי שה-container עושה בלי volume — נעלם עם ה-container. Containers הם disposable by design. מי שרוצה persistence — צריך volume.
  2. named volume מיועד למסדי-נתונים ו-state, bind mount לקוד בפיתוח. הכלל: volume לנתונים, mount לקוד. בלי להתבלבל — תיקנו 90% מהבעיות של מתחילים.
  3. משתני סביבה ב---env-file .env, לא ב-ENV ב-Dockerfile. הסודות לעולם לא נכנסים ל-image. --env-file הוא הגשר בין המארח ל-container; ה-image נשאר נקי.
  4. ARG נשמר ב-layer, ENV נשמר ב-layer. שניהם לא מתאימים לסודות. ARG לפרמטרים של build (גרסה, target); ENV לערכים קבועים לא-רגישים (port, NODE_ENV). סודות → --env-file תמיד.
  5. ארבע שכבות הגנה על סודות. (1) אין סוד ב-Dockerfile; (2) .env ב-.gitignore; (3) .env ב-.dockerignore; (4) --env-file בריצה. לפספס אחת — דליפה.
  6. docker history הוא המבחן הסופי. docker history myapp --no-trunc | grep -iE "key|secret|token|password" — אם יש שורות חשודות, תבנו מחדש. זו הבדיקה שלוקחת 10 שניות וחוסכת חודשים של נזק.
  7. container הוא disposable; תקנו את ה-Dockerfile או השתמשו ב-bind mount, לא ב-docker exec. שינויים בתוך container רץ הם אנטי-דפוס. הם נעלמים ב-rebuild, ב-pull, או ב-docker rm. המקור האמיתי של הקוד הוא המארח; המקור האמיתי של ה-state הוא volume.
Just One Thing — אם תזכרו רק דבר אחד מהפרק הזה

אם תוציאו רק פעולה אחת מהפרק הזה השבוע — שתהיה זאת: ודאו שה-image שלכם לא מכיל שום סוד. הריצו docker history myapp:latest --no-trunc עכשיו, ועברו על כל שורה. חפשו API_KEY, SECRET, TOKEN, PASSWORD, credential. אם אתם רואים משהו — תקנו את ה-Dockerfile, תבנו מחדש, ותעבירו את הסוד ל---env-file. ואז הוסיפו .env ל-.gitignore ול-.dockerignore אם הוא לא שם.

הסיבה שזו הפעולה הכי חשובה: 48% מאפליקציות ה-AI שפורסמו לציבור מכילות סוד שדלף ל-image. רובן לא במתכוון; המפתח פשוט לא ידע ש-ENV ב-Dockerfile נשמר ב-layer. אתם יודעים עכשיו. הבדיקה הזו לוקחת 30 שניות ומגינה עליכם מלהיות חלק מהסטטיסטיקה הזו. אם תזכרו רק את זה — הצלתם את המשתמשים שלכם מדליפת מידע, ואת עצמכם מבילוי חודשי ב-OpenAI על מישהו אחר.

מה הלאה — פרק 5

בפרק 5 (אפליקציות מרובות-Containers — Docker Compose ל-Web + מסד-נתונים) נעבור מפקודות docker run בודדות לקובץ compose.yaml אחד שמגדיר את כל הסביבה: web + database + cache, עם service discovery לפי שם, healthcheck שמחכה למסד-נתונים מוכן לפני שהאפליקציה עולה, ו-depends_on עם תנאים. ה-volume וקובץ ה-.env שבניתם בפרק הזה הם הבסיס; בפרק 5 פשוט נעטוף אותם בקובץ אחד שמריץ את הכל ביחד. בפרק 6 נדחוף את ה-image ל-ghcr.io ונפרוס אותו על VPS או Coolify, וההגנה על הסודות שבניתם היום היא מה שתגן עליכם גם שם.

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