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

אפליקציות מרובות-Containers — Docker Compose ל-Web + מסד-נתונים

בפרקים 3 ו-4 בניתם image של האפליקציה, שמרתם את המסד עם named volume, והעברתם סודות דרך --env-file. אבל עד עכשיו הרצתם הכל בשתי פקודות נפרדות — אחת למסד, אחת לאפליקציה — והאפליקציה התחברה למסד דרך host.docker.internal באמצעות "קסם" שלא תעבוד בשרת אמיתי. בפרק הזה אתם סוגרים את הפער: Docker Compose — קובץ אחד (compose.yaml) שמגדיר את כל הסביבה, מריץ אותה בפקודה אחת (docker compose up), ומחבר בין השירותים ברשת פנימית שבה הם מוצאים אחד את השני לפי שם. זה לב הקורס: בסוף הפרק תוכלו לקחת כל אפליקציה ש-AI בנה לכם (web + db, או web + db + cache), לעטוף אותה ב-Compose, ולהריץ את כל ה-stack בלוקלי ובשרת — אותו קובץ, אותה פקודה, אפס הפתעות.

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

אתם בלב הפרויקט המרכזי של הקורס. בפרק 1 הבנתם למה "רץ אצלי במחשב" לא מספיק. בפרק 2 התקנתם את Docker והרצתם nginx. בפרק 3 בניתם image של האפליקציה שלכם. בפרק 4 הוספתם named volume למסד, --env-file לסודות, והוכחתם שהנתונים שורדים docker rm.

בפרק הזה אתם פותרים את הבעיה האחרונה שמפרידה אתכם מ-stack שעובד בכל מקום:

  1. שתי פקודות במקום אחת — היום אתם מריצים docker run ל-db, docker run לאפליקציה, ומקווים שהסדר נכון. אחרי הפרק — פקודה אחת docker compose up מרימה הכל, בסדר הנכון, עם המתנה למסד מוכן.
  2. service discovery בלי קסם — היום האפליקציה מתחברת למסד דרך host.docker.internal (קסם שעובד רק בלוקל). אחרי הפרק — האפליקציה מתחברת ל-db (פשוט המילה db) כ-hostname, וזה עובד בלוקל וגם ב-VPS.
  3. הכל בקובץ אחד — היום הגדרות ה-stack פזורות בין פקודות docker run שאתם זוכרים בראש. אחרי הפרק — קובץ compose.yaml אחד מתעד את כל ה-stack, נשמר ב-Git, ומאפשר לחבר להריץ git clone + docker compose up ולראות את האפליקציה רצה.

מה הלאה: בפרק 6 נדחוף את ה-image של ה-web ל-ghcr.io, נבנה multi-arch (למקרה שה-VPS שלכם amd64 והלפטופ arm64), ונפרוס את כל ה-stack על VPS או Coolify. ה-compose.yaml שתבנו היום הוא הקובץ שיחיה גם בפרודקשן — בלי שינוי.

בינוני 6 דקות core stack

למה צריך את זה — הבעיה שפקודות docker run לא פותרות

תעצרו לרגע ותסתכלו על מה שאתם עושים היום כדי להריץ את האפליקציה המלאה. זה משהו בסגנון:

# טרמינל 1: מסד-נתונים
docker run -d --name myapp-db \
  -e POSTGRES_PASSWORD=secret \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16

# טרמינל 2: האפליקציה
docker run -d --name myapp-web \
  -e DATABASE_URL=postgres://postgres:secret@host.docker.internal:5432/mydb \
  -p 3000:3000 \
  myapp:latest

# ואם האפליקציה לא התחברה — לבדוק, לעצור, להפעיל מחדש
docker logs myapp-web
docker restart myapp-web

זה עובד, אבל יש בזה ארבע בעיות:

  1. שתי פקודות, שני טרמינלים, וסדר לא מובטח. הרצתם את ה-web לפני ה-db? הוא יקרוס. הפעלתם את שניהם ביחד? תקבלו connection refused באקראי.
  2. host.docker.internal הוא קסם לוקלי. זה עובד רק ב-Docker Desktop על Mac/Windows, ולא ב-Linux ולא ב-VPS. אם תעתיקו את הפקודה לשרת — היא תישבר.
  3. הגדרות ה-stack חיות רק בראש שלכם. אין קובץ שמתעד איזה port, איזה volume, איזה env var. חבר שירצה להריץ את הפרויקט? תצטרכו לשלוח לו את הפקודות ב-DM.
  4. אין דרך פשוטה להפעיל חלק מה-stack. רוצים רק את ה-db בלי ה-web לבדיקה? תצטרכו לזכור את הפקודה הראשונה.

Docker Compose פותר את כל ארבע. קובץ compose.yaml אחד מגדיר את כל ה-stack (web + db, או web + db + cache, או כל שילוב שתרצו). פקודה אחת (docker compose up) מרימה הכל בסדר הנכון, מחברת את השירותים ברשת פנימית, ויוצרת קובץ שאפשר לשמור ב-Git. זה הפורמט הסטנדרטי לשיתוף פרויקטים שמורכבים מכמה שירותים — בלי שום קסם, בלי כתובות IP קשיחות, בלי סדר ידני.

רוב האפליקציות ש-AI בונה לכם הן לפחות web + db. הרבה מהן web + db + Redis (cache) או web + db + worker. בלי Compose, אתם מנהלים N פקודות docker run במקביל. עם Compose — N שורות YAML בקובץ אחד.

Do Now — 3 דקות (רשימת הרצות נוכחיות)

פתחו terminal והריצו docker ps -a. תראו את כל ה-containers שרצים או נשארו מהפרקים הקודמים. עכשיו תנסחו בעצמכם, בשורה אחת: "כדי להריץ את ה-stack המלא שלי, אני צריך ____ פקודות docker run נפרדות." המספר שכתבתם הוא המספר ש-compose.yaml יחליף. תוצאה צפויה: הבנה אינטואיטיבית ש-Compose לא "עוד כלי" — הוא תחליף לפקודות docker run שאתם כבר מריצים היום.

בינוני 10 דקות core YAML

מבנה compose.yaml — services, image, build, ports, volumes, environment

קובץ compose.yaml הוא קובץ טקסט בפורמט YAML (YAML Ain't Markup Language) — שפת סידור נתונים שמשתמשת בהזחה (indentation) במקום סוגריים. כל שורה בקובץ היא key: value, וההזחה קובעת מה שייך למה. זה לא קוד — זה הגדרת תצורה (configuration).

המבנה הבסיסי

הנה ה-compose.yaml הקטן ביותר שעדיין עושה משהו אמיתי — שירות אחד שמריץ nginx:

services:
  web:
    image: nginx:latest
    ports:
      - "8080:80"

ארבע שורות, ואתם מקבלים את nginx רץ על http://localhost:8080. אבל הכוח של Compose הוא כמה services, אז בואו נראה stack אמיתי — web + database:

services:
  web:
    build: .                              # בנה image מה-Dockerfile בתיקייה הנוכחית
    ports:
      - "3000:3000"                       # host:container
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/mydb
    depends_on:
      db:
        condition: service_healthy       # חכה שה-db בריא

  db:
    image: postgres:16                    # משוך image מוכן מ-Docker Hub
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=mydb
    volumes:
      - pgdata:/var/lib/postgresql/data   # named volume למסד
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  pgdata:                                 # הצהרה על named volume ברמת ה-stack

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

המפתחות שתכירו הכי טוב

בכל service יש כמה שדות עיקריים. הנה מה כל אחד עושה:

שתי רמות: services ו-volumes

שימו לב שיש שתי רמות בקובץ: services: (רשימת הקונטיינרים) ו-volumes: (רשימת ה-volumes המנוהלים). כשאתם כותבים - pgdata:/var/lib/postgresql/data בתוך service, אתם משתמשים ב-volume; כשאתם כותבים pgdata: ברמה העליונה, אתם מצהירים עליו. בלי ההצהרה, Compose יוצר anonymous volume — שעובד, אבל קשה לנהל. עם ההצהרה — יש לכם שם ידידותי.

זה ה-contract הבסיסי עם Compose: קובץ אחד, תיאור שלם של ה-stack. אם תראו קובץ compose.yaml ב-Git repo של פרויקט — אתם יודעים בדיוק איך הוא בנוי, בלי לקרוא שורה של docker run.

Do Now — 4 דקות (לקרוא compose.yaml ש-docker init יצר)

אם הרצתם docker init בפרק 3 — בדקו אם יש compose.yaml בתיקייה. אם לא — צרו אחד ריק ותריצו docker init עכשיו (זה לא הופך כלום). פתחו את compose.yaml ותראו מה יש שם. תוצאה צפויה: קובץ עם services: ובתוכו שירות אחד (שם הפרויקט שלכם) עם build, ports, ועוד כמה שדות. הבנה ש-docker init כבר נתן לכם בסיס — אתם רק צריכים להוסיף את ה-db ולחבר.

בינוני 6 דקות קריטי v1 vs v2

docker compose (רווח) מול docker-compose (מקף) — המעבר שכולם מפספסים

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

ההבדל בפועל:

# ✅ הצורה הנכונה (v2)
docker compose up
docker compose down
docker compose ps
docker compose logs

# ❌ הצורה הישנה (v1) — ב-Docker Desktop חדש תקבלו:
docker-compose up
# docker: 'docker-compose' is not a docker command
# או command not found

אז למה זה עדיין רלוונטי? כי מדריכים ישנים, פלט AI, Stack Overflow, ואפילו תיעוד רשמי שלא עודכן ממשיכים לפלוט docker-compose עם מקף. אם תעתיקו פקודה כזאת — היא תישבר.

הסימנים שהמדריך ישן

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

ההגירה בפועל

אם יש לכם docker-compose.yml ישן — אין צורך למחוק אותו. Docker Compose v2 תומך גם בשם הישן (עם מקף) וגם בחדש. אבל אם אתם כותבים חדש — תכתבו compose.yaml בלי מקף, ותשתמשו בפקודות עם רווח.

הרגל פשוט שיחסוך לכם שעות תסכול: תמיד חפשו את הרווח. docker compose = נכון. docker-compose = ישן.

טעות נפוצה: להשתמש ב-docker-compose (מקף) כי זה מה שה-AI כתב

זה קורה בכל פעם שמשתמשים ב-ChatGPT, Claude, או Cursor על פרויקט Docker ישן: המודל מאומן על המון תיעוד מ-2018-2022, ופולט docker-compose up כברירת מחדל. הפקודה תיכשל ב-Docker Desktop הנוכחי עם command not found או 'docker-compose' is not a docker command. התיקון: תמיד תחליפו את המקף ברווח בפלט של AI לפני שאתם מריצים. או, עוד יותר טוב — תוסיפו לפרומפט שלכם: "השתמש ב-Docker Compose v2 עם רווח (docker compose, לא docker-compose)". ב-Gold Standard של הקורס הזה, כל הפקודות בפרק הן docker compose עם רווח.

בינוני 7 דקות core hybrid

image מול build באותו קובץ — לערבב image מוכן ו-image מקומי

אחד הדברים היפים ב-Compose הוא שאתם לא צריכים לבחור: או build או image. אתם יכולים לערבב באותו קובץ — שירות אחד שאתם בונים מקוד שלכם, ושירותים אחרים שאתם פשוט מושכים מ-registry.

הסיטואציה הקלאסית:

ההפרדה הזו חשובה: אתם לא בונים PostgreSQL. אתם רק משתמשים בו. הקוד של האפליקציה שלכם הוא מה שאתם בונים. הכל השאר — כלים, ספריות, services סטנדרטיים — נמשך מ-registry.

build עם context

ברירת המחדל של build: . היא "תיקייה נוכחית" (התיקייה שבה נמצא ה-compose.yaml, או ה-relative path ממנה). אבל לפעמים ה-Dockerfile במקום אחר:

services:
  web:
    build:
      context: ./web              # תיקייה עם ה-Dockerfile
      dockerfile: Dockerfile.dev   # קובץ Dockerfile חלופי
    ports:
      - "3000:3000"

זה שימושי כשיש כמה סביבות (dev Dockerfile עם dev dependencies, prod Dockerfile רזה) או כשהקוד בתת-תיקייה.

image עם tag מותאם

כשאתם כותבים image: postgres:16, Compose מושך postgres:16 מ-Docker Hub. אבל image יכול לקבל גם שם של image פרטי שאתם בניתם:

services:
  web:
    build: .
    image: myapp:1.0              # בנה, וגם תייג ב-myapp:1.0
    ports:
      - "3000:3000"

בלי image אחרי build, Compose נותן שם זמני כמו project-web-1. עם image — הוא משתמש בשם שאתם נותנים, וגם docker images יראה אותו. שימושי כשרוצים לעקוב אחרי גרסאות.

Framework — "מתי image מוכן, מתי build מקומי?"

שאלה אחת פשוטה קובעת:

הכלל המעשי: אם אתם יכולים להחליף את ה-service בלי לכתוב Dockerfile — זה image. אם אתם חייבים לכתוב Dockerfile כדי שהוא יעבוד — זה build. ב-stack טיפוסי של vibe coder: web = build, הכל השאר = image.

Do Now — 5 דקות (לרשום את ה-services של הפרויקט שלכם)

פתחו רשימה ב-Notepad או בקובץ זמני. לכל שירות בפרויקט שלכם, כתבו: (א) שם ה-service (כמו web, db), (ב) האם זה image (מוכן) או build (מקומי), (ג) על איזה port הוא רץ, (ד) איזה volume הוא צריך. תוצאה צפויה: רשימה של 2-4 שירותים. זה ה-backlog של ה-compose.yaml שלכם. אם אתם לא יודעים איזה services יש — תריצו את האפליקציה ותראו לאן היא מתחברת (מסד, cache, API חיצוני).

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

service discovery — איך ה-web מוצא את ה-db בלי IP

זה אחד הקסמים של Compose. כשאתם מגדירים כמה services באותו קובץ, Compose יוצר רשת פנימית אוטומטית ומחבר את כולם אליה. ברשת הזו, כל שירות מקבל שם-מארח (hostname) שהוא שם ה-service שלו.

הנה הקסם בפועל. ה-service של ה-db נקרא db. ה-web רוצה להתחבר אליו. במקום לכתוב IP או host.docker.internal, פשוט:

# בתוך ה-web, החיבור למסד:
DATABASE_URL=postgres://user:pass@db:5432/mydb
#                                          ^^^
#                                          זה לא localhost, לא IP, לא host.docker.internal
#                                          זה פשוט "db" — שם ה-service

וזה עובד. תמיד. בלוקל, ב-VPS, ב-CI, בכל מקום. כי db הוא לא מחרוזת — הוא שם מארח ש-Compose רושם ב-DNS הפנימי שלו. כשה-container של ה-web פותח חיבור ל-db:5432, ה-DNS הפנימי אומר לו "ה-IP של db הוא 172.18.0.2" (או משהו דומה), והחיבור מתבצע.

למה זה עדיף על host.docker.internal

בפרק 4 השתמשנו ב-host.docker.internal בתור "קסם" שעובד רק ב-Mac/Windows Docker Desktop — זה שם מארח שמצביע על המארח מתוך ה-container. עם Compose, אנחנו לא צריכים את זה: ה-db רץ בתוך container משלו, באותה רשת כמו ה-web, ויש לו hostname משלו. אנחנו מדברים איתו container-to-container, לא container-to-host.

היתרון המעשי: אותו compose.yaml עובד על הלפטופ שלכם, על ה-VPS שלכם, ועל הלפטופ של חבר. בלי שינוי. בלי קסם תלוי-OS.

בדיקה מהירה — hostname resolution בפועל

אחרי שתריצו docker compose up, תוכלו להיכנס ל-web ולוודא שה-DNS עובד:

# היכנסו ל-container של ה-web
docker compose exec web sh

# בתוך ה-container, בדקו:
nslookup db
# Server:    127.0.0.11
# Address:   127.0.0.11#53
# Non-authoritative answer:
# Name:      db
# Address:   172.18.0.2

# או בלי nslookup, פשוט:
ping -c 1 db
# PING db (172.18.0.2): 56 data bytes
# 64 bytes from 172.18.0.2: ...

ה-127.0.0.11 הוא ה-DNS resolver הפנימי של Docker. הוא מכיר את כל ה-services ברשת. אתם לא צריכים לדעת את ה-IP האמיתי של ה-db — רק את השם.

מגבלה: רק בתוך אותה רשת

service discovery עובד רק בתוך הרשת ש-Compose יוצרת. אם תריצו docker run חיצוני ל-Compose (container "זר"), הוא לא יראה את db כ-hostname. בקובץ compose.yaml תוכלו להגדיר רשתות מותאמות (networks:) ולחבר רק services מסוימים — אבל זה מתקדם יותר, ולרוב לא תצטרכו את זה.

Do Now — 4 דקות (לראות את הרשת של Compose יוצר)

אחרי שתריצו docker compose up (גם אם זה רק web + db מהדוגמה הקודמת), הריצו בטרמינל נפרד: docker network ls. תראו רשת עם שם שמתחיל ב-<project>_default (למשל myapp_default). עכשיו: docker network inspect myapp_default (או השם שראיתם). תוצאה צפויה: רשת bridge עם שני containers (web, db) מחוברים אליה, כל אחד עם IP משלו. זו הרשת שבה db הוא hostname תקף.

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

ports, volumes, ו-environment בקובץ אחד

הסעיף הזה הוא ה"תרגום" הישיר של מה שאתם כבר יודעים מפרק 4 (volumes, env vars) לעולם של Compose. הכלל פשוט: כל flag של docker run שאתם מכירים, יש לו מקבילה ב-Compose.

ports — מ-(-p) ל-(ports:)

ב-docker run הייתם כותבים -p 3000:3000. ב-Compose:

services:
  web:
    image: myapp:latest
    ports:
      - "3000:3000"   # host:container

אותו פורמט, אותו כלל host:container, אותה מלכודת של כיוון. 3000:3000 = "ב-host, port 3000 יוציא ל-container port 3000". אם תכתבו הפוך — החיבור יישבר.

יש גם וריאציות:

ports:
  - "3000:3000"          # host:container
  - "127.0.0.1:3000:3000" # רק מ-localhost, לא מהרשת
  - "8080:80"             # host port אחר

הצורה הראשונה היא מה שתשתמשו ב-99% מהמקרים. 127.0.0.1:3000:3000 שימושי כשאתם רוצים שרק המכונה המקומית תוכל לגשת ל-container (לא חושפים לרשת).

volumes — שלוש צורות

ב-Compose, volumes: יכול לקבל שלוש צורות:

services:
  db:
    image: postgres:16
    volumes:
      # 1. named volume (למסד-נתונים, production-ready)
      - pgdata:/var/lib/postgresql/data

      # 2. bind mount (לקוד בפיתוח)
      - ./src:/app

      # 3. anonymous volume (בלי שם, נדיר)
      - /var/lib/postgresql/data

volumes:
  pgdata:  # הצהרה על ה-named volume ברמת הקובץ

הצורה הראשונה (named volume) — בדיוק כמו -v pgdata:/var/lib/postgresql/data ב-docker run. הצורה השנייה (bind mount) — בדיוק כמו -v $(pwd)/src:/app. השלישית (anonymous) — נדירה, ועדיף להימנע.

environment — שתי דרכים

העברת config דרך הסביבה ב-Compose נראית ככה:

services:
  web:
    image: myapp:latest
    environment:
      # צורה 1: inline, בלי מרכאות (פשוט)
      NODE_ENV: production
      DEBUG: "false"

      # צורה 2: inline, עם רשימה (כמו CLI)
      - DATABASE_URL=postgres://user:pass@db:5432/mydb

      # צורה 3: הפניה למשתנה מהמארח
      - API_KEY=${API_KEY}

    # צורה 4: קובץ .env שלם (עדיף לסודות)
    env_file:
      - .env

ארבע צורות, אותה תוצאה. הכלל מפרק 4: ערכים לא-רגישים (NODE_ENV, ports, hostnames) יכולים להיות environment: inline. ערכים רגישים (סיסמאות, API keys) הולכים ל-env_file: שמצביע על .env, ו-.env ב-.gitignore וב-.dockerignore.

טבלת תרגום: docker run ↔ compose.yaml

הנה התרגום הישיר, לטיסה:

docker run compose.yaml
-d docker compose up -d (לא בקובץ עצמו)
--name myapp service name (למשל web: ב-services)
-p 3000:3000 ports: ["3000:3000"]
-v pgdata:/data volumes: [pgdata:/data] + volumes: pgdata: ברמה העליונה
-e KEY=value environment: {KEY: value}
--env-file .env env_file: [.env]
--restart unless-stopped restart: unless-stopped
--network mynet networks: [mynet] (עם הגדרה ברמה העליונה)

אם אתם זוכרים את הפקודה ב-docker run, אתם יודעים את המקבילה ב-Compose. זה אותו Docker, רק עם תחביר שונה.

Do Now — 5 דקות (להמיר פקודת docker run ל-service ב-Compose)

קחו את פקודת ה-docker run שהרצתם בפרק 4 למסד-נתונים (אתם אמורים לזכור — הייתה שם -v pgdata:... ו--e POSTGRES_PASSWORD=...). תרשמו אותה. עכשיו תתרגמו אותה ל-services.db בקובץ compose.yaml — אותם volumes, אותם environment, אותו image. תוצאה צפויה: הבנה ש-Compose לא "עושה משהו אחר" — הוא רק מתעד את מה שאתם עושים עם docker run, בצורה שאפשר לשמור ב-Git ולשתף.

בינוני 7 דקות core timing

הבעיה ש-depends_on לבדו לא פותר — connection refused

עד עכשיו דיברנו על איך לחבר services. עכשיו נדבר על מתי. וזה המקום שבו הרבה מתחילים נתקלים ב-flakiness שמטריד אותם שבועות.

התסריט הקלאסי

אתם מריצים docker compose up. Compose רואה שיש שני services: web ו-db. הוא רואה שיש depends_on: db ב-web. אז הוא עושה את מה שאתם מצפים: מריץ את ה-db לפני ה-web. נכון?

לא בדיוק. Compose מתחיל את ה-db לפני ה-web. זה נכון. אבל "מתחיל" לא אומר "מוכן לקבל חיבורים". PostgreSQL לוקח 2-5 שניות מהרגע שה-container עלה עד שהוא באמת מאזין ל-5432 ומוכן ל-authentication. באותו זמן, ה-web כבר מתחיל, מנסה להתחבר למסד, ומקבל connection refused.

web-1  | Error: connect ECONNREFUSED 172.18.0.2:5432
web-1  |   at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1661:16)
db-1   | PostgreSQL init process complete; ready for start up.
# ↑ ה-db סיים לעלות רק אחרי שה-web כבר ניסה להתחבר

אם תריצו שוב (docker compose restart web) — הפעם זה יעבוד, כי ה-db כבר רץ. אבל זה flakiness — בפעם הראשונה זה נשבר, בפעם השנייה זה עובד, ואתם לא יודעים למה.

הסיבה הטכנית

depends_on בלי תנאי אומר: "התחל את ה-dependency לפני שאתה מתחיל אותי". לא יותר. Compose לא בודק שה-process בתוך ה-container באמת פעיל, שהוא מאזין לפורט, או שהוא מוכן לקבל חיבורים. הוא רק מתחיל את ה-container.

הבעיה הזו ייחודית ל-environments שבהם ה-service צריך זמן "להתחמם" — מסדי-נתונים, caches, services שעושים migration, וכל דבר שמאזין לפורט. depends_on לבדו הוא סדר הפעלה, לא המתנה למוכנות.

הפתרון: healthcheck + condition: service_healthy

הסעיף הבא מראה איך healthcheck פותר את הבעיה הזו. אבל הנה spoiler — אתם צריכים שני דברים:

  1. healthcheck: על ה-db, שמגדיר איך לבדוק אם ה-db באמת מוכן.
  2. depends_on: db: { condition: service_healthy } על ה-web, שאומר "אל תתחיל אותי עד שה-db בריא".

בלי שני אלה יחד — ה-flakiness נשאר.

טעות נפוצה: depends_on לבדו פותר את הסדר

הרבה מתחילים רואים depends_on ומניחים שזה מספיק. "ה-db תלוי-ב-web? לא, ה-web תלוי-ב-db. הוספתי, שמרתי, עובד." עובד? ב-80% מהפעמים. ב-20% הנותרים — connection refused, ואתם מבלים שעה להבין למה. הכלל: depends_on בלי condition: service_healthy הוא לא מספיק לכל service שצריך זמן לעלות. כל מסד, כל cache, כל broker — דורש healthcheck. הסעיף הבא מראה איך.

Do Now — 4 דקות (לשחזר את ה-flakiness)

אם יש לכם כבר compose.yaml עם web ו-db בלי healthcheck, הריצו: docker compose down && docker compose up (כיבוי מוחלט והפעלה מאפס). הסתכלו ב-logs: docker compose logs. תוצאה צפויה: לפעמים תראו connection refused ב-web לפני שה-db מסיים לעלות. זה ה-flakiness שעלינו לפתור. אחרי שתוסיפו healthcheck + condition — הוא ייעלם.

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

healthcheck — ה-probe שאומר "אני מוכן באמת"

healthcheck הוא probe (בדיקה) ש-Docker מריץ בתוך ה-container כל כמה שניות. אם ה-probe מחזיר 0 (הצליח) — ה-container מסומן healthy (בריא). אם הוא מחזיר ערך אחר או נכשל — unhealthy (לא בריא). אם הוא עדיין לא סיים probe ראשון — starting (מתחיל).

המבנה של healthcheck

הנה healthcheck ל-PostgreSQL:

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
  interval: 5s
  timeout: 5s
  retries: 5
  start_period: 10s

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

probes נפוצים ל-services שתכירו

הנה רשימה קצרה של probes שתזדקקו להן. העתיקו והתאימו:

איך לראות את הסטטוס

אחרי docker compose up, תוכלו לראות את ה-health של כל service:

docker compose ps
# NAME        SERVICE   STATUS              PORTS
# myapp-db-1  db        Up (healthy)        5432/tcp
# myapp-web-1 web       Up                  0.0.0.0:3000->3000/tcp

# או עם docker ps:
docker ps --format "table {{.Names}}\t{{.Status}}"
# myapp-db-1   Up 2 minutes (healthy)
# myapp-web-1  Up 2 minutes

כשה-db עולה לראשונה, תראו Up (health: starting) במשך כמה שניות, ואז Up (healthy). הרגע שבו הוא עובר ל-healthy — זה הרגע שבו depends_on: condition: service_healthy מתחיל את ה-web.

למה CMD-SHELL ולא סתם CMD?

שימו לב לצורה ["CMD-SHELL", "..."] — זו הצורה הנכונה לרוב ה-probes. CMD-SHELL אומר "הרץ את זה דרך sh -c". זה מאפשר שימוש במשתני סביבה ($POSTGRES_USER), ב-pipes (||), וב-constructs של shell. הצורה ["CMD", "..."] (בלי SHELL) מריצה את הפקודה ישירות, בלי shell — טוב לפקודות פשוטות שלא צריכות expansion.

Do Now — 6 דקות (להוסיף healthcheck ל-db)

קחו את ה-compose.yaml שלכם, ול-service של ה-db, הוסיפו בלוק healthcheck עם pg_isready. אל תשנו את ה-web עדיין — רק את ה-db. הריצו: docker compose up -d, ואז docker compose ps. תוצאה צפויה: ה-db יראה Up (healthy) תוך 5-10 שניות. ה-web יראה Up בלי (healthy) עדיין (כי לא הגדרנו לו). זו ההוכחה שה-probe עובד.

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

condition: service_healthy — החיבור שמסיר את ה-flakiness

עכשיו, אחרי שה-db יודע להגיד "אני בריא", אפשר לגרום ל-web להקשיב. ב-depends_on של ה-web, במקום הצורה הפשוטה:

# ❌ הצורה הפשוטה — רק "התחל לפני"
depends_on:
  - db

נכתוב את הצורה המפורשת:

# ✅ הצורה הנכונה — "חכה שיהיה בריא"
depends_on:
  db:
    condition: service_healthy

זה ההבדל בין "התחל את ה-db לפני שאתה מתחיל אותי" לבין "אל תתחיל אותי בכלל עד שה-db בריא".

מה קורה עכשיו בפועל

תרחיש מלא עם השניים ביחד:

  1. אתם מריצים docker compose up.
  2. Compose רואה שיש שני services. רואה שה-web תלוי ב-db עם תנאי service_healthy.
  3. Compose מתחיל את ה-db.
  4. ה-db מתחיל לעלות. ה-healthcheck probe רץ כל 5 שניות. עד שהוא מחזיר 0 (בריא) — הסטטוס הוא starting.
  5. ברגע שה-probe מצליח (בדרך כלל תוך 3-8 שניות ל-PostgreSQL), ה-db מסומן healthy.
  6. עכשיו, ורק עכשיו, Compose מתחיל את ה-web.
  7. ה-web עולה, מתחבר ל-db ב-db:5432 דרך הרשת הפנימית, והחיבור מצליח מהפעם הראשונה.

אפס connection refused. אפס flakiness. אפס restart-ים ידניים.

condition: service_started לעומת service_healthy

יש עוד אפשרות: condition: service_started. זה אומר "חכה שה-container של ה-dependency התחיל" — שזה בערך כמו depends_on הפשוט (רק סדר). הצורה המועילה באמת היא service_healthy, והיא זו שתשתמשו בה לכל service שצריך זמן לעלות.

מתי כן מספיק service_started? כשה-dependency עולה מיידית ומוכן מיד — למשל, קובץ config server, או service פנימי שלכם שלא צריך warmup. ברוב המקרים — service_healthy.

מה קורה אם ה-db נופל?

healthcheck לא רק עוזר באתחול — הוא גם מנטר את ה-service במהלך הריצה. אם ה-db יקרוס באמצע הריצה, ה-probe יחזיר 1, הסטטוס יהפוך ל-unhealthy, ו-Compose יודיע על זה בפלט של docker compose ps (תראו Up (unhealthy)). עם restart: on-failure בקונפיג, ה-container יופעל מחדש אוטומטית.

אבל שימו לב: depends_on: condition: service_healthy בודק את התנאי רק בהפעלה. הוא לא יעצור את ה-web אם ה-db נופל תוך כדי ריצה. לזה צריך כלים אחרים (health monitoring, restart policies, observability).

Framework — "איזה תנאי לאיזה service?"

לכל depends_on ב-Compose, שאלו את השאלה:

  1. האם ה-dependency צריך זמן "להתחמם"?

    • כן (PostgreSQL, Redis, MongoDB, RabbitMQ, כל מסד) → condition: service_healthy + healthcheck: על ה-dependency.
    • לא (service פנימי שלכם שעולה מיד, static config) → depends_on: [db] פשוט (רק סדר, בלי תנאי).
  2. האם ה-dependency חיוני ל-web?

    • כן (בלי ה-db, האפליקציה לא יכולה לעבוד) → depends_on עם service_healthy (כדי שה-web לא יעלה בלי ה-db).
    • לא (cache, observability, sidecar) → בלי depends_on; ה-web עולה גם בלי, ומתמודד עם היעדרות בזמן ריצה.

הכלל המעשי: כל מסד-נתונים → service_healthy. כל cache → service_healthy. כל service שהוא "חובה-לפני-העלייה" → service_healthy. השאר → פשוט או בלי.

טעות נפוצה: לשכוח named volume ל-db ב-compose ולאבד את כל הנתונים ב-docker compose down

זו הטעות השנייה הכי נפוצה אחרי ה-connection refused: מישהו כותב compose.yaml יפהפה, מריץ docker compose up, יוצר נתונים, עושה docker compose down כדי "לסדר משהו", ואז עושה up שוב — והמסד ריק. למה? כי docker compose down מוחק את ה-containers וגם את הרשתות, אבל לא את ה-volumes. אם ה-db היה בתוך ה-container (בלי volume) — הנתונים אבדו. אם היה named volume (מוצהר ברמה העליונה של הקובץ) — הנתונים שרדו. הכלל: תמיד להצהיר על named volume ברמה העליונה של compose.yaml עבור מסד-נתונים, ולמפות אותו לתיקיית ה-data של ה-service. אל תסמכו על volumes אנונימיים.

Do Now — 6 דקות (לחבר את ה-web ל-db עם condition: service_healthy)

עכשיו, באותו compose.yaml שבו הוספתם healthcheck ל-db, ערכו את ה-web. שנו את depends_on: - db (אם היה לכם) לצורה המפורשת: depends_on: db: { condition: service_healthy }. הריצו docker compose down && docker compose up -d. עכשיו: docker compose logs web ו-docker compose logs db. תוצאה צפויה: תראו את ה-db מתחיל, את ה-probe רץ, ורק אחרי שה-db הוא healthy — ה-web מתחיל. אין connection refused. אם תריצו docker compose ps באמצע התהליך, תוכלו לתפוס את הרגע שבו ה-db הוא healthy וה-web עוד לא התחיל.

בינוני 7 דקות hands-on CLI

docker compose up / down / logs / ps — טבלת ההפעלה

אחרי שיש לכם compose.yaml, רוב העבודה שלכם עם ה-stack תהיה עם חמש פקודות. הנה טבלה מלאה, ואחריה הסבר של כל אחת.

פקודה מה היא עושה מתי להשתמש
docker compose up בונה images אם צריך, יוצר רשתות, מרים volumes, מתחיל containers. רץ ב-foreground — רואים logs בזמן אמת. פיתוח, דיבאג, כשרוצים לראות מה קורה ברגע זה.
docker compose up -d אותו דבר, אבל detached — רץ ברקע. הטרמינל חוזר מיד. production-like, CI, כשרוצים שה-stack ירוץ ולהמשיך לעבוד.
docker compose up --build בונה מחדש את ה-images גם אם הם כבר קיימים. שימושי כשהקוד השתנה. אחרי שינוי ב-Dockerfile או בקוד שמשפיע על ה-image.
docker compose down עוצר ומוחק את ה-containers, הרשתות, וה-volumes האנונימיים. named volumes נשמרים. סיום עבודה, מעבר בין branches, "לאפס" את ה-stack (בלי המסד).
docker compose down -v כמו down, אבל גם מוחק את ה-named volumes. אזהרה: המסד יימחק. פעם-בחיים של "הכל מאפס, גם המסד".
docker compose ps מציג את ה-containers של ה-stack הנוכחי, הסטטוס שלהם, וה-ports. לראות מה רץ, מה עצר, מה unhealthy.
docker compose logs מציג את ה-logs של כל ה-services. עם -f, עוקב בזמן אמת. דיבאג, צפייה בפלט.
docker compose logs web רק logs של service ספציפי. כשרוצים רק את ה-web בלי רעש של ה-db.
docker compose exec web sh פותח shell בתוך container רץ. sh לרוב images, bash לחלק. דיבאג, הרצת פקודות בתוך ה-container.
docker compose restart web עוצר ומפעיל מחדש רק את ה-web. אחרי שינוי ב-env, או כשה-web תקוע.
docker compose stop עוצר את ה-containers בלי למחוק אותם. up מחדש יהיה מהיר יותר. עצירה זמנית, חיסכון במשאבים.

ה-flow היומי בפיתוח

הנה איך תעבדו ביום-יום עם ה-stack:

# בוקר — להרים את ה-stack
docker compose up -d
docker compose logs -f web   # לראות מה קורה

# אחרי שינוי בקוד של ה-web
docker compose up -d --build web   # בונה מחדש רק את ה-web
docker compose logs -f web

# אחרי שינוי ב-Dockerfile
docker compose up -d --build       # בונה מחדש את כל מה ש-build

# לבדוק מה רץ
docker compose ps
docker ps                          # רואה גם containers שלא ב-Compose

# דיבאג — להיכנס ל-container
docker compose exec web sh
# עכשיו אתם בתוך ה-web. תריצו ls, env, cat, או מה שצריך.
# exit כדי לצאת.

# סוף יום — לסגור
docker compose stop
# או:
docker compose down   # מוחק containers + רשתות, volumes נשמרים

חמש פקודות. up -d, up -d --build, ps, logs, down. זה ה-90% של העבודה היומית.

טעות שכולם עושים בהתחלה

כשה-web לא מתחבר, רוב המתחילים מיד עושים docker compose restart web. זה עובד? לפעמים. אבל זה לא דיבאג — זה קסם. במקום:

  1. docker compose ps — מה הסטטוס? ה-db healthy או starting/unhealthy?
  2. docker compose logs db — מה ה-db אומר?
  3. docker compose logs web — מה השגיאה האמיתית?
  4. docker compose exec web sh — היכנסו ובדקו env | grep DATABASE (האם ה-URL נכון?), ping db (האם ה-DNS עובד?).

ארבע פקודות אבחון במקום restart-ים עיוורים. זה ההבדל בין "אני מקווה שזה יעבוד" לבין "אני יודע למה זה לא עובד".

Do Now — 5 דקות (לתרגל את כל חמש הפקודות)

עם ה-stack הרץ שלכם, הריצו ברצף:

  1. docker compose ps — ראו את הסטטוס.
  2. docker compose logs --tail 20 — ראו את 20 השורות האחרונות של כל ה-services.
  3. docker compose logs db --tail 5 — רק את ה-db.
  4. docker compose exec db psql -U user -d mydb -c "SELECT 1" — בדקו שאתם יכולים לדבר עם ה-db מתוך ה-container שלו.
  5. docker compose down && docker compose up -d — כיבוי מוחלט והפעלה מחדש. ודאו שהכל עולה בסדר.

תוצאה צפויה: הרגל של חמש פקודות. אחרי התרגיל הזה, תרגישו ש-Compose זה לא "עוד כלי" — זה ה-API שאתם משתמשים בו כל יום.

מתקדם 4 דקות אופציונלי profiles

compose profiles בקצרה — הפעלה חלקית של ה-stack

זה קצר, ואתם לא חייבים להשתמש בזה עכשיו — אבל כשתצטרכו, זה יחכה לכם. profiles הם דרך להגיד ל-Compose: "ה-service הזה רץ רק אם ביקשתי אותו במפורש".

התסריט: יש לכם web, db, adminer (כלי web לניהול מסד), ו-mailhog (שרת דואר לפיתוח). ה-web וה-db אמורים לרוץ תמיד. adminer ו-mailhog רק בפיתוח, לא בפרודקשן.

services:
  web:
    build: .
    ports: ["3000:3000"]
    depends_on:
      db: { condition: service_healthy }

  db:
    image: postgres:16
    volumes: [pgdata:/var/lib/postgresql/data]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user"]
      interval: 5s
      timeout: 5s
      retries: 5

  adminer:
    image: adminer:latest
    ports: ["8080:8080"]
    profiles: ["dev"]   # רץ רק אם הפעלנו עם profile של dev

  mailhog:
    image: mailhog/mailhog:latest
    ports: ["1025:1025", "8025:8025"]
    profiles: ["dev"]   # גם רץ רק ב-dev

volumes:
  pgdata:

עכשיו, כשאתם מריצים docker compose up — רק web ו-db עולים. כדי להפעיל גם את ה-dev tools:

docker compose --profile dev up

או לכמה profiles ביחד:

docker compose --profile dev --profile debug up

השימוש העיקרי: להפריד "מה רץ בפיתוח" מ"מה רץ בפרודקשן" באותו compose.yaml. בפרודקשן (פרק 6) תריצו docker compose -f compose.yaml -f compose.prod.yaml up — אבל זה כבר שימוש מתקדם יותר. הכלל: אם הפרויקט שלכם פשוט (web + db), אתם לא צריכים profiles. אם הוא מורכב (כלי dev, multi-environment, observability), profiles יעזרו.

בינוני 15 דקות capstone integration

Capstone — אפליקציה + Postgres בקובץ אחד

עכשיו הכל ביחד. הנה compose.yaml שלם שמחבר את האפליקציה שלכם (נניח Node/Next.js או Python/Flask) עם PostgreSQL, כולל healthcheck, depends_on, named volume, ו-env file. זה הקובץ שאתם תשתמשו בו לאורך שארית הקורס.

services:
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 5
      start_period: 10s

volumes:
  pgdata:

ולקובץ .env (כזכור — ב-.gitignore וב-.dockerignore):

POSTGRES_USER=appuser
POSTGRES_PASSWORD=changeme-in-production
POSTGRES_DB=myapp
DATABASE_URL=postgres://appuser:changeme-in-production@db:5432/myapp

שימו לב לשני דברים:

  1. ה-DATABASE_URL ב-web מצביע על db:5432 — לא על localhost, לא על host.docker.internal. רק db, שם ה-service. זה עובד בלוקל, ב-VPS, ב-CI, בכל מקום.
  2. ה-username/password ב-healthcheck מגיעים מה-.env דרך ${POSTGRES_USER}. ה-healthcheck רץ בתוך ה-container, אבל ה-substitution של המשתנים קורה ב-Compose לפני שה-container מתחיל — אז ה-probe רואה את הערכים האמיתיים.

הרצה

בתיקיית הפרויקט, עם Dockerfile, compose.yaml, ו-.env:

docker compose up -d
# מחכה 5-10 שניות
docker compose ps
# אתם אמורים לראות:
# NAME            SERVICE   STATUS              PORTS
# myapp-db-1      db        Up (healthy)        5432/tcp
# myapp-web-1     web       Up                  0.0.0.0:3000->3000/tcp

curl http://localhost:3000
# או פתחו ב-browser

זה ה-stack. כל ה-stack. שתי שורות (אחת ל-Compose, אחת לבדיקה) ויש לכם web + db רץ, בריא, מחובר, ומוכן.

לסגור, להפעיל מחדש, לאמת

docker compose down
# containers נמחקו, named volume pgdata נשמר

docker compose up -d
# חוזר. ה-db עולה, ה-web מחכה לו, הכל מתחבר.

# עכשיו — צרו נתונים באפליקציה דרך ה-browser.
# אחר כך:
docker compose down
docker compose up -d
# האם הנתונים שם? כן — כי ה-volume שרד.

זה ה-flow. זה הסוף של הקורס בעולם הלוקלי. כל מה שעשיתם בפרקים 1-4 — image, volume, env, healthcheck, service discovery — מתחבר לקובץ אחד. בפרק 6 תיקחו בדיוק את הקובץ הזה ל-VPS, וזה יעבוד.

Framework — "מה המינימום שצריך ב-compose.yaml?"

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

  1. services: ברמה העליונה. בלי זה, אין קובץ Compose.

  2. image או build לכל service. בלי אחד מהם, ה-service לא יכול לעלות.

  3. ports לכל service שצריך להיות נגיש מ-host (web, כלי dev, admin). ה-db לא צריך — הוא רץ ברשת הפנימית.

  4. volumes לכל מסד-נתונים (named, לא anonymous). הצהרה ברמה העליונה + mount בתוך ה-service.

  5. healthcheck לכל service שצריך warmup (בעיקר db, cache, broker).

  6. depends_on עם condition: service_healthy לכל web/app שמתחבר ל-db. זה מה שמונע את ה-flakiness.

  7. env_file: .env לכל service שצריך config רגיש. .env ב-.gitignore וב-.dockerignore.

אם כל השבעה נמצאים — הקובץ שלכם production-ready. חסר משהו? חיזרו לסעיף המתאים. הקובץ הזה יחיה גם ב-VPS, גם ב-CI, גם ב-Coolify (פרק 6).

תרגיל 1 — Stack מלא עם healthcheck (20 דקות)

המטרה: לבנות compose.yaml שמחבר את האפליקציה שלכם ל-PostgreSQL, עם healthcheck, named volume, ו-depends_on מותנה — ולראות שאין connection refused באתחול ראשון.

מה תעשו:

  1. בתיקיית הפרויקט שלכם, צרו compose.yaml עם שני services: web (build מ-Dockerfile שלכם) ו-db (image postgres:16-alpine).
  2. ל-db, הוסיפו environment עם POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB (קחו מ-.env שלכם), named volume pgdata:/var/lib/postgresql/data, ו-healthcheck עם pg_isready.
  3. ל-web, הוסיפו ports: ["3000:3000"], env_file: [.env], ו-depends_on: db: { condition: service_healthy }.
  4. ב-.env, ודאו ש-DATABASE_URL מצביע על db:5432 (לא localhost).
  5. ברמה העליונה של compose.yaml, הצהירו volumes: pgdata:.
  6. הריצו: docker compose up -d.
  7. הריצו: docker compose ps. ודאו שה-db מסומן Up (healthy) וה-web Up.
  8. הריצו: docker compose logs db. חפשו שורה שאומרת "database system is ready to accept connections".
  9. הריצו: docker compose logs web. ודאו שאין connection refused או ECONNREFUSED.
  10. פתחו http://localhost:3000 ב-browser. האפליקציה אמורה לעבוד ולהציג נתונים.

תוצאה צפויה: ה-stack עולה, ה-db healthy, ה-web רץ בלי connection refused, האפליקציה נגישה ב-browser. ה-volume pgdata קיים (אפשר לאמת עם docker volume ls). אתם יכולים לראות את הקסם של Compose בעיניים.

תרגיל 2 — אפס connection refused: הוכחה ש-depends_on עובד (10 דקות)

המטרה: להוכיח ש-condition: service_healthy באמת פותר את ה-flakiness, על-ידי הרצה חוזרת של ה-stack מאפס וצפייה בסדר ההפעלה.

מה תעשו:

  1. עם ה-stack הרץ, הריצו: docker compose down (כיבוי מוחלט, volume נשמר).
  2. עכשיו, בלי לחכות, הריצו: docker compose up (foreground, רואים logs בזמן אמת).
  3. הסתכלו על הלוג. הסדר שאתם צריכים לראות:
    1. ה-db מתחיל, רושם שהוא מאזין ל-5432.
    2. ה-healthcheck רץ. הסטטוס starting.
    3. ה-probe מצליח, הסטטוס healthy.
    4. רק אז ה-web מתחיל.
    5. ה-web מתחבר בהצלחה ל-db בלי שגיאות.
  4. לחיצה על Ctrl+C כדי לצאת מ-foreground (אחרת הוא ימשיך לרוץ).
  5. אם ראיתם את הסדר הזה — הצלחתם. depends_on: condition: service_healthy עובד.

תוצאה צפויה: אפס connection refused ב-logs של ה-web. ה-web מתחיל רק אחרי שה-db הוא healthy. הקסם של ה-probe נראה בזמן אמת. אם במקרה ראיתם connection refused — חיזרו לסעיף 8 ובדקו שה-healthcheck ו-depends_on מוגדרים נכון.

תרגיל 3 — וודאו שה-volume שורד את docker compose down (12 דקות)

המטרה: להוכיח שה-named volume של המסד שורד docker compose down (ולא רק docker rm של container בודד).

מה תעשו:

  1. הריצו את ה-stack: docker compose up -d.
  2. צרו רשומה באפליקציה דרך ה-browser (פוסט, משתמש, comment — מה שה-API שלכם תומך בו). רשמו בצד כמה רשומות יש.
  3. עכשיו: docker compose down. שימו לב — זה מוחק containers וגם רשתות, אבל לא את ה-volume. אמתו: docker volume ls. ה-volume myapp_pgdata (או דומה) צריך להיות שם.
  4. הריצו שוב: docker compose up -d.
  5. חכו שה-stack יעלה (5-10 שניות).
  6. רעננו את הדף ב-browser. תוצאה צפויה: אותה כמות רשומות, אותו תוכן. המסד זוכר הכל.
  7. אם אתם רוצים לראות את ההפך: docker compose down -v (עם -v!). הריצו up -d שוב. עכשיו המסד ריק.

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

תרגיל 4 — תרחיש דיבאג: ה-web לא מתחבר (15 דקות)

המטרה: ללמוד את תהליך הדיבאג כש-stack לא עובד. הסיטואציה הקלאסית: ה-DATABASE_URL עדיין מצביע על host.docker.internal במקום db.

מה תעשו:

  1. ב-.env, שנו את DATABASE_URL בכוונה ל-ערך שגוי: postgres://user:pass@host.docker.internal:5432/mydb (הקסם הישן מפרק 4).
  2. הריצו: docker compose up -d.
  3. הריצו: docker compose ps. ה-web רץ (כי הוא לא תלוי ב-connection — הוא תלוי רק ב-startup).
  4. הריצו: docker compose logs web. צפו לראות: שגיאת connection refused או ENOTFOUND או could not translate host name "host.docker.internal".
  5. הריצו: docker compose exec web env | grep DATABASE_URL. תראו: ה-URL השגוי, כי env_file טען אותו.
  6. עכשיו, תקנו: שנו ב-.env את ה-URL בחזרה ל-postgres://user:pass@db:5432/mydb.
  7. הריצו: docker compose up -d. שימו לב — לא צריך --build כי ה-image לא השתנה, רק ה-env.
  8. הריצו: docker compose logs web. צפו לראות: החיבור הצליח, אין connection refused.

תוצאה צפויה: אתם יודעים עכשיו לאבחן בעיות חיבור. ה-flow הוא: ps (מה רץ) → logs (מה השגיאה) → exec env (מה הערך שהועבר) → תיקון ב-.env או ב-compose.yamlup שוב. ארבע פקודות אבחון שחוסכות שעות של ניחושים.

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

יומי (5-10 דקות, בתחילת העבודה): docker compose ps ו-docker compose logs --tail 20. תראו אם ה-stack רץ, מה הסטטוס, ואם יש שגיאות מאתמול. אם משהו unhealthy — docker compose restart <service> ובדקו למה.

לפני commit (3 דקות): docker compose config — פקודה שמדפיסה את ה-compose.yaml המפוענח (אחרי כל ה-substitutions של env vars). תראו את הקובץ "האמיתי" ש-Compose רואה, ותוודאו שאין שגיאות syntax. .env.example מעודכן? .env ב-.gitignore וב-.dockerignore?

שבועי (15 דקות, סוף שבוע): docker compose down (בלי -v — לשמור על המסד). תבדקו שהכל עולה מחדש עם docker compose up -d בלי connection refused. אם יש — ה-healthcheck או ה-depends_on לא מספיק טובים, וצריך לחזור לסעיף 8-9.

לפני פרק 6 (סוף שבוע שלישי): ודאו שה-stack שלכם רץ עם docker compose up -d, ה-db healthy, ה-web מתחבר, והאפליקציה נגישה ב-browser. docker compose down ו-up -d עובדים בלי אובדן נתונים. .env במקום, .env.example ב-Git. בפרק 6 ניקח את אותו compose.yaml ונדחוף את ה-image ל-ghcr.io ונפרוס על VPS — אותו קובץ, אפס שינוי.

Check Yourself — 5 שאלות הבנה
  1. שאלה: חבר שלח לכם קובץ docker-compose.yml (עם מקף בשם הקובץ) ובו docker-compose up בתיעוד. מה אתם עושים?

    תשובה: שני שינויים: (א) הקובץ עובד, אבל עדיף לשנות ל-compose.yaml בלי מקף — זה השם המומלץ בתיעוד הרשמי של v2. (ב) הפקודה בתיעוד צריכה להיות docker compose up (רווח) ולא docker-compose up (מקף). v1 הוסר מ-Docker Desktop. אחרי התיקון, ה-stack יעבוד. הלקח: הקובץ סלחן (שני השמות נתמכים), אבל הפקודה לא — מקף = לא יעבוד ב-Docker Desktop הנוכחי.

  2. שאלה: יש לכם compose.yaml עם web שתלוי ב-db עם depends_on: - db (בלי תנאי). האפליקציה רצה, אבל מדי פעם docker compose logs web מראה connection refused באתחול ראשון אחרי down && up. למה, ואיך מתקנים?

    תשובה: depends_on בלי תנאי מחכה רק שה-db יתחיל, לא שיהיה מוכן. PostgreSQL לוקח 2-5 שניות לעלות, ובזמן הזה ה-web כבר מנסה להתחבר. התיקון: להוסיף healthcheck ל-db (עם pg_isready) ולשנות את depends_on ב-web ל-depends_on: db: { condition: service_healthy }. אחרי כן, docker compose ps יראה את ה-db עובר מ-starting ל-healthy, ורק אז ה-web יתחיל. אפס connection refused.

  3. שאלה: ה-DATABASE_URL ב-.env שלכם הוא postgres://user:pass@localhost:5432/mydb. האפליקציה לא מתחברת ל-db. מה הבעיה, ולאיזה ערך צריך לשנות?

    תשובה: localhost בתוך container פירושו "ה-container עצמו", לא "המארח" ולא "ה-db". ה-db רץ ב-container אחר עם hostname db (שם ה-service ב-Compose). התיקון: DATABASE_URL=postgres://user:pass@db:5432/mydb. db הוא hostname תקף ברשת הפנימית של Compose, ועובד בלוקל, ב-VPS, וב-CI. אותו דבר נכון לכל service אחר (redis, mongo, rabbitmq) — תמיד שם ה-service.

  4. שאלה: הרצתם docker compose down -v ועכשיו המסד ריק. למה? ואיך הייתם מונעים את זה אם זה היה production?

    תשובה: docker compose down -v (עם -v) מוחק גם את ה-named volumes, מה שמוחק את המסד. בלי -v, ה-named volume נשמר. מניעה: (א) אל תשתמשו ב--v בטעות. (ב) הרגילו את עצמכם: down = "סגור, מסד נשמר" (בטוח), down -v = "מחק הכל, כולל המסד" (מסוכן). (ג) הוסיפו תיעוד פנימי בקבוצה: "אסור down -v בלי אישור של מישהו שמבין מה הוא עושה". בפרודקשן אמיתי, אתם רוצים גם backup של ה-volume לפני כל פעולה הרסנית — אבל זה כבר נושא לפרק הבא והלאה.

  5. שאלה: ה-stack שלכם רץ, אבל האפליקציה ב-browser לא נטענת (connection refused ב-port 3000). docker compose ps מראה את ה-web רץ. מה הצעד הראשון?

    תשובה: לא docker compose restart web — זה קסם, לא דיבאג. במקום, ארבעה צעדים: (א) docker compose logs web — מה ה-web אומר? יש שגיאת port? שגיאת app? (ב) docker compose ps — איזה port הוא חושף? 0.0.0.0:3000->3000/tcp? (ג) docker compose exec web sh ואז curl localhost:3000 מתוך ה-container — האפליקציה בכלל רצה? (ד) מהמארח: curl localhost:3000 — האם ה-port mapping עובד? ברוב המקרים תגלו שהבעיה היא שה-app בתוך ה-container לא מאזין ל-0.0.0.0 (רק ל-127.0.0.1) — תיקון: לשנות את הפקודת ההרצה ב-Dockerfile. ארבע פקודות אבחון במקום restart-ים עיוורים.

Just One Thing — אם תזכרו רק דבר אחד מהפרק הזה

אם תוציאו רק פעולה אחת מהפרק הזה השבוע — שתהיה זאת: פתחו את ה-compose.yaml שלכם וודאו שיש בו healthcheck על ה-db וגם depends_on: condition: service_healthy על ה-web. שני אלה יחד מסירים את ה-connection refused שמטריד מתחילים באתחול ראשון. בלי אחד מהם — ה-stack יעבוד "בערך", ולפעמים יקרוס. עם שניהם — הוא תמיד יעלה בסדר. זו ההבדלה בין stack שמרגיש "קסום" לבין stack שמרגיש "מהימן". הכל השאר — image, build, ports, volumes, env, profiles — זה תרגום של מה שאתם כבר יודעים. ה-healthcheck + condition זה מה שחדש.

סיכום הפרק — 7 לקחים שייקחו אתכם הלאה
  1. Docker Compose = קובץ אחד לכל ה-stack. במקום N פקודות docker run, קובץ compose.yaml אחד עם N services. docker compose up מרים הכל בפקודה אחת, בסדר הנכון, עם רשת פנימית.
  2. docker compose (רווח) ≠ docker-compose (מקף). v2 = plugin מודרני, רווח, ב-Docker Desktop הנוכחי. v1 = סקריפט ישן, מקף, הוסר. כל מדריך לפני 2023 וכל פלט AI לא מעודכן יפלטו מקף. תמיד תחליפו לרווח.
  3. image מול build באותו קובץ. האפליקציה שלכם = build. כל השאר (PostgreSQL, Redis, Nginx) = image מ-registry. אתם לא בונים תוכנה שמישהו אחר כבר בנה.
  4. Service discovery לפי שם. ה-web מתחבר ל-db:5432, לא ל-localhost ולא ל-host.docker.internal. זה hostname ש-Compose רושם ב-DNS הפנימי, וזה עובד בלוקל, ב-VPS, ב-CI — בלי שינוי.
  5. depends_on לבדו לא מספיק. הוא מחכה להתחלה, לא למוכנות. connection refused באתחול ראשון = תוצאה צפויה. התיקון: healthcheck + condition: service_healthy.
  6. healthcheck + condition = סוף ה-flakiness. pg_isready בודק אם PostgreSQL באמת מקבל חיבורים. depends_on: db: { condition: service_healthy } מחכה שהסטטוס יהיה healthy לפני שה-web מתחיל. אחרי כן, תמיד עולה בסדר.
  7. חמש פקודות הן 90% מהעבודה היומית. up -d (הרם), up -d --build (הרם + בנה מחדש), ps (סטטוס), logs (פלט), down (כבה, מסד נשמר). שאר הפקודות — exec, restart, stop, config — שימושיות, אבל משניות.
מה הלאה — פרק 6

בפרק 6 (debug, registries, ו-deploy ל-VPS) ניקח את ה-stack הזה לשרת אמיתי. נלמד לאבחן container שקורס (logs, inspect, exec), לדחוף את ה-image של ה-web ל-ghcr.io (כולל multi-arch למקרה שאתם על Mac M-series וה-VPS amd64), ולפרוס — או ידנית עם SSH + docker compose up -d על VPS, או דרך Coolify (PaaS קוד-פתוח לאחסון-עצמי) שבונה ומריץ ומנפיק HTTPS אוטומטית. ה-compose.yaml שבניתם היום הוא הקובץ שיחיה גם בפרודקשן — בלי שינוי. בפרק 6 פשוט נוסיף לו image: ghcr.io/<user>/myapp:v1 במקום build: ., ונפעיל אותו על שרת.

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