- לכתוב
compose.yamlעם services מרובים (web + db) שמשלבbuildמ-Dockerfile מקומי,imageמוכן מ-Registry,ports,volumes,environment, ו-env_file— ולהרים את כל ה-stack בפקודה אחתdocker compose up. - להבחין בין
docker compose(רווח — Docker Compose v2, plugin של Docker CLI, הצורה הנתמכת היום) לביןdocker-compose(מקף — v1, Python script, deprecated והוסר מ-Docker Desktop) — ולתקן כל מדריך ישן או פלט AI שעדיין פולט את הצורה הישנה. - לחבר את ה-web ל-db לפי שם ה-service (למשל
postgres://user:pass@db:5432/mydb) דרך הרשת האוטומטית של Compose, בלי כתובות IP קשיחות, בליhost.docker.internal, בלי קסם — שמתאים גם ל-VPS. - להוסיף
healthcheckל-db (למשלpg_isready) ולהשתמש ב-depends_on: condition: service_healthyכך שה-web מחכה עד שהמסד מוכן לקבל חיבורים — ולא רק "התחיל לרוץ". סוף ל-connection refusedבאתחול ראשון. - להפעיל את ה-stack בשלוש צורות:
up(רץ ב-foreground ומציג logs),up -d(ברקע),down(כיבוי וניקוי) — ולדעת לבדוק מה רץ עםdocker compose psולהוציא logs עםdocker compose logs -f.
- פרקים קודמים: פרקים 1-4. אתם צריכים את ה-mental model של image מול container (פרק 1), התקנת Docker שעובדת (פרק 2), image בנוי של האפליקציה שלכם שרץ עם
docker run -p 3000:3000 myapp(פרק 3), והרגל של named volume +--env-file .envל-config רגיש (פרק 4). - מה תצטרכו: תיקיית הפרויקט שלכם עם
Dockerfile+.dockerignore+ קובץ.envשמכיל את ה-config (למשלDATABASE_URL,POSTGRES_PASSWORD). אם ה-Dockerfile שלכם לא קיים — תריצוdocker initבתיקייה. - זמן משוער: 80-100 דקות קריאה + תרגול. הפרק הזה קצר יותר מהקודמים, אבל כל תרגיל מייצר stack אמיתי שרץ.
- קובץ
compose.yamlאחד שמגדיר את כל ה-stack: שני services לפחות (webעםbuildמ-Dockerfile שלכם, ו-dbעםimage: postgres:16-alpine), עםports,volumes,env_file, ו-healthcheck ל-db — ושעולה בפקודה אחתdocker compose up. - healthcheck עובד +
depends_on: condition: service_healthy— הוכחה ב-docker compose psשה-db עובר מ-startingל-healthyלפני שה-web מתחיל, ושאיןconnection refusedבאתחול ראשון אחריdocker compose down && up. - stack ששורד restart בלי אובדן נתונים — הוכחה ש-
docker compose down(בלי-v) לא מוחק את המסד, וה-named volumepgdataמוצהר ברמה העליונה של הקובץ. - Service discovery עובד לפי שם — ה-DATABASE_URL ב-
.envמצביע עלdb:5432(לאlocalhost, לאhost.docker.internal), ואתם יכולים להוכיח את זה עםdocker compose exec web nslookup dbמתוך ה-container של ה-web. - טבלת פעולות יומיות —
up -d,up -d --build,ps,logs,down,exec— ולכל אחת אתם יודעים מתי להשתמש ומה היא מציגה. זה ה-API שלכם לעבודה יומיומית עם ה-stack. - flow אבחון בסיסי — כשמשהו לא עובד, אתם יודעים את הסדר:
ps(סטטוס) →logs(שגיאה) →exec env(איזה env var הועבר) → תיקון →upשוב. בלי restart-ים עיוורים.
אתם בלב הפרויקט המרכזי של הקורס. בפרק 1 הבנתם למה "רץ אצלי במחשב" לא מספיק. בפרק 2 התקנתם את Docker והרצתם nginx. בפרק 3 בניתם image של האפליקציה שלכם. בפרק 4 הוספתם named volume למסד, --env-file לסודות, והוכחתם שהנתונים שורדים docker rm.
בפרק הזה אתם פותרים את הבעיה האחרונה שמפרידה אתכם מ-stack שעובד בכל מקום:
- שתי פקודות במקום אחת — היום אתם מריצים
docker runל-db,docker runלאפליקציה, ומקווים שהסדר נכון. אחרי הפרק — פקודה אחתdocker compose upמרימה הכל, בסדר הנכון, עם המתנה למסד מוכן. - service discovery בלי קסם — היום האפליקציה מתחברת למסד דרך
host.docker.internal(קסם שעובד רק בלוקל). אחרי הפרק — האפליקציה מתחברת ל-db(פשוט המילהdb) כ-hostname, וזה עובד בלוקל וגם ב-VPS. - הכל בקובץ אחד — היום הגדרות ה-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 שתבנו היום הוא הקובץ שיחיה גם בפרודקשן — בלי שינוי.
למה צריך את זה — הבעיה שפקודות 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
זה עובד, אבל יש בזה ארבע בעיות:
- שתי פקודות, שני טרמינלים, וסדר לא מובטח. הרצתם את ה-web לפני ה-db? הוא יקרוס. הפעלתם את שניהם ביחד? תקבלו
connection refusedבאקראי. host.docker.internalהוא קסם לוקלי. זה עובד רק ב-Docker Desktop על Mac/Windows, ולא ב-Linux ולא ב-VPS. אם תעתיקו את הפקודה לשרת — היא תישבר.- הגדרות ה-stack חיות רק בראש שלכם. אין קובץ שמתעד איזה port, איזה volume, איזה env var. חבר שירצה להריץ את הפרויקט? תצטרכו לשלוח לו את הפקודות ב-DM.
- אין דרך פשוטה להפעיל חלק מה-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 בקובץ אחד.
פתחו terminal והריצו docker ps -a. תראו את כל ה-containers שרצים או נשארו מהפרקים הקודמים. עכשיו תנסחו בעצמכם, בשורה אחת: "כדי להריץ את ה-stack המלא שלי, אני צריך ____ פקודות docker run נפרדות." המספר שכתבתם הוא המספר ש-compose.yaml יחליף. תוצאה צפויה: הבנה אינטואיטיבית ש-Compose לא "עוד כלי" — הוא תחליף לפקודות docker run שאתם כבר מריצים היום.
מבנה 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 יש כמה שדות עיקריים. הנה מה כל אחד עושה:
image— שם ה-image ש-Compose ימשוך מ-registry (Docker Hub, ghcr.io, וכו'). דוגמאות:postgres:16,redis:7-alpine,nginx:latest.build— נתיב לתיקייה עםDockerfile. Compose יריץdocker buildבשבילכם.build: .אומר "התיקייה הנוכחית" (כמוdocker build -t web .).ports— מיפוי port מ-host ל-container, בפורמט"host:container"(אותו כלל כמו-pב-docker run).volumes— רשימת mounts.pgdata:/var/lib/postgresql/data= named volume../src:/app= bind mount. אותם כללים מפרק 4.environment— רשימת משתני סביבה.KEY=valueinline, או רשימה עם- KEY=value(שתי הצורות תקפות).env_file— נתיב לקובץ.envעם משתנים. בדיוק כמו--env-fileב-docker run.depends_on— סדר הפעלה. בלי תנאי = "חכה שיתחיל". עםcondition: service_healthy= "חכה שיהיה בריא".healthcheck— הגדרת probe שבודקת אם השירות באמת מוכן. נפרט בסעיף 8.
שתי רמות: 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.
אם הרצתם docker init בפרק 3 — בדקו אם יש compose.yaml בתיקייה. אם לא — צרו אחד ריק ותריצו docker init עכשיו (זה לא הופך כלום). פתחו את compose.yaml ותראו מה יש שם. תוצאה צפויה: קובץ עם services: ובתוכו שירות אחד (שם הפרויקט שלכם) עם build, ports, ועוד כמה שדות. הבנה ש-docker init כבר נתן לכם בסיס — אתם רק צריכים להוסיף את ה-db ולחבר.
docker compose (רווח) מול docker-compose (מקף) — המעבר שכולם מפספסים
זה אחד המקומות הכי מבלבלים ב-Docker היום, ושווה לעצור עליו לרגע. יש שתי פקודות שנראות אותו דבר אבל שונות לחלוטין:
docker compose(עם רווח) — Docker Compose v2. זה plugin שמובנה ב-Docker CLI מגרסה 20.10 ומעלה, וב-Docker Desktop. זו הצורה הנתמכת היום.docker-compose(עם מקף) — Docker Compose v1. זה היה Python script נפרד שהותקן בנפרד. הוא deprecated מאז 2023, ו-הוסר לחלוטין מ-Docker Desktop ב-2024. בהתקנות חדשות הוא בכלל לא קיים.
ההבדל בפועל:
# ✅ הצורה הנכונה (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 up(מקף) במקוםdocker compose up(רווח).- הוראה להתקין עם
pip install docker-composeאוcurl ... docker-compose— זה היה רלוונטי ב-2018, לא היום. - קובץ
docker-compose.ymlעם מקף בשם (במקוםcompose.yaml). שני השמות עובדים ב-v2, אבל השם המומלץ בתיעוד הרשמי הואcompose.yamlבלי מקף. - פקודות עם
--buildבלי ערך אחריו, אוdocker-compose --compatibility— דגלים של v1.
ההגירה בפועל
אם יש לכם docker-compose.yml ישן — אין צורך למחוק אותו. Docker Compose v2 תומך גם בשם הישן (עם מקף) וגם בחדש. אבל אם אתם כותבים חדש — תכתבו compose.yaml בלי מקף, ותשתמשו בפקודות עם רווח.
הרגל פשוט שיחסוך לכם שעות תסכול: תמיד חפשו את הרווח. docker compose = נכון. docker-compose = ישן.
זה קורה בכל פעם שמשתמשים ב-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 עם רווח.
image מול build באותו קובץ — לערבב image מוכן ו-image מקומי
אחד הדברים היפים ב-Compose הוא שאתם לא צריכים לבחור: או build או image. אתם יכולים לערבב באותו קובץ — שירות אחד שאתם בונים מקוד שלכם, ושירותים אחרים שאתם פשוט מושכים מ-registry.
הסיטואציה הקלאסית:
- web — זה הקוד שלכם, שאתם כותבים/עורכים. צריך
build: .כי אין image מוכן ב-Docker Hub לאפליקציה הספציפית שלכם. - db — זה PostgreSQL סטנדרטי. מישהו כבר בנה, תייג, ודחף ל-Docker Hub. אתם רק צריכים
image: postgres:16. - cache (אופציונלי) — Redis. באותו הגיון:
image: redis:7-alpine.
ההפרדה הזו חשובה: אתם לא בונים 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 יראה אותו. שימושי כשרוצים לעקוב אחרי גרסאות.
שאלה אחת פשוטה קובעת:
-
האם הקוד שלי חי בתוך ה-image?
- כן (האפליקציה שלי, ה-API שלי, ה-worker שלי) →
build:. ה-image לא קיים ב-registry — רק אתם יכולים לבנות אותו. - לא (PostgreSQL, Redis, Nginx, MongoDB, RabbitMQ) →
image:. הקוד של ה-image נכתב על-ידי מישהו אחר, נבדק, ומתוחזק.
- כן (האפליקציה שלי, ה-API שלי, ה-worker שלי) →
הכלל המעשי: אם אתם יכולים להחליף את ה-service בלי לכתוב Dockerfile — זה image. אם אתם חייבים לכתוב Dockerfile כדי שהוא יעבוד — זה build. ב-stack טיפוסי של vibe coder: web = build, הכל השאר = image.
פתחו רשימה ב-Notepad או בקובץ זמני. לכל שירות בפרויקט שלכם, כתבו: (א) שם ה-service (כמו web, db), (ב) האם זה image (מוכן) או build (מקומי), (ג) על איזה port הוא רץ, (ד) איזה volume הוא צריך. תוצאה צפויה: רשימה של 2-4 שירותים. זה ה-backlog של ה-compose.yaml שלכם. אם אתם לא יודעים איזה services יש — תריצו את האפליקציה ותראו לאן היא מתחברת (מסד, cache, API חיצוני).
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 מסוימים — אבל זה מתקדם יותר, ולרוב לא תצטרכו את זה.
אחרי שתריצו docker compose up (גם אם זה רק web + db מהדוגמה הקודמת), הריצו בטרמינל נפרד: docker network ls. תראו רשת עם שם שמתחיל ב-<project>_default (למשל myapp_default). עכשיו: docker network inspect myapp_default (או השם שראיתם). תוצאה צפויה: רשת bridge עם שני containers (web, db) מחוברים אליה, כל אחד עם IP משלו. זו הרשת שבה db הוא hostname תקף.
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, רק עם תחביר שונה.
קחו את פקודת ה-docker run שהרצתם בפרק 4 למסד-נתונים (אתם אמורים לזכור — הייתה שם -v pgdata:... ו--e POSTGRES_PASSWORD=...). תרשמו אותה. עכשיו תתרגמו אותה ל-services.db בקובץ compose.yaml — אותם volumes, אותם environment, אותו image. תוצאה צפויה: הבנה ש-Compose לא "עושה משהו אחר" — הוא רק מתעד את מה שאתם עושים עם docker run, בצורה שאפשר לשמור ב-Git ולשתף.
הבעיה ש-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 — אתם צריכים שני דברים:
healthcheck:על ה-db, שמגדיר איך לבדוק אם ה-db באמת מוכן.depends_on: db: { condition: service_healthy }על ה-web, שאומר "אל תתחיל אותי עד שה-db בריא".
בלי שני אלה יחד — ה-flakiness נשאר.
הרבה מתחילים רואים depends_on ומניחים שזה מספיק. "ה-db תלוי-ב-web? לא, ה-web תלוי-ב-db. הוספתי, שמרתי, עובד." עובד? ב-80% מהפעמים. ב-20% הנותרים — connection refused, ואתם מבלים שעה להבין למה. הכלל: depends_on בלי condition: service_healthy הוא לא מספיק לכל service שצריך זמן לעלות. כל מסד, כל cache, כל broker — דורש healthcheck. הסעיף הבא מראה איך.
אם יש לכם כבר compose.yaml עם web ו-db בלי healthcheck, הריצו: docker compose down && docker compose up (כיבוי מוחלט והפעלה מאפס). הסתכלו ב-logs: docker compose logs. תוצאה צפויה: לפעמים תראו connection refused ב-web לפני שה-db מסיים לעלות. זה ה-flakiness שעלינו לפתור. אחרי שתוסיפו healthcheck + condition — הוא ייעלם.
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
חמישה שדות, כל אחד עם תפקיד ברור:
test— הפקודה שרצה בתוך ה-container.pg_isready -U user -d mydbבודק אם PostgreSQL מקבל חיבורים לדטבייס הספציפי.exit 0= בריא.interval— כל כמה זמן רץ ה-probe.5s= כל חמש שניות.timeout— כמה זמן לחכות לפני שה-probe נחשב נכשל.5s= אם לא סיים תוך חמש שניות, הוא נכשל.retries— כמה כשלונות רצופים לפני שה-container מסומן unhealthy.5= חמש פעמים שלא ענה, ואז בריאות ירודה.start_period— זמן חסד בהתחלה, שבו probes כושלים לא נספרים.10s= תן למסד 10 שניות לעלות לפני שמתחילים לבדוק ברצינות.
probes נפוצים ל-services שתכירו
הנה רשימה קצרה של probes שתזדקקו להן. העתיקו והתאימו:
- PostgreSQL:
test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"] - MySQL:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$MYSQL_ROOT_PASSWORD"] - Redis:
test: ["CMD", "redis-cli", "ping"] - MongoDB:
test: ["CMD", "mongo", "--eval", "db.adminCommand('ping')"] - RabbitMQ:
test: ["CMD", "rabbitmq-diagnostics", "ping"] - HTTP (Next.js, FastAPI, Flask, Express):
test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"](דורש endpoint בריאות באפליקציה) - nginx:
test: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"]
איך לראות את הסטטוס
אחרי 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.
קחו את ה-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 עובד.
condition: service_healthy — החיבור שמסיר את ה-flakiness
עכשיו, אחרי שה-db יודע להגיד "אני בריא", אפשר לגרום ל-web להקשיב. ב-depends_on של ה-web, במקום הצורה הפשוטה:
# ❌ הצורה הפשוטה — רק "התחל לפני"
depends_on:
- db
נכתוב את הצורה המפורשת:
# ✅ הצורה הנכונה — "חכה שיהיה בריא"
depends_on:
db:
condition: service_healthy
זה ההבדל בין "התחל את ה-db לפני שאתה מתחיל אותי" לבין "אל תתחיל אותי בכלל עד שה-db בריא".
מה קורה עכשיו בפועל
תרחיש מלא עם השניים ביחד:
- אתם מריצים
docker compose up. - Compose רואה שיש שני services. רואה שה-web תלוי ב-db עם תנאי
service_healthy. - Compose מתחיל את ה-db.
- ה-db מתחיל לעלות. ה-healthcheck probe רץ כל 5 שניות. עד שהוא מחזיר 0 (בריא) — הסטטוס הוא
starting. - ברגע שה-probe מצליח (בדרך כלל תוך 3-8 שניות ל-PostgreSQL), ה-db מסומן
healthy. - עכשיו, ורק עכשיו, Compose מתחיל את ה-web.
- ה-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).
לכל depends_on ב-Compose, שאלו את השאלה:
-
האם ה-dependency צריך זמן "להתחמם"?
- כן (PostgreSQL, Redis, MongoDB, RabbitMQ, כל מסד) →
condition: service_healthy+healthcheck:על ה-dependency. - לא (service פנימי שלכם שעולה מיד, static config) →
depends_on: [db]פשוט (רק סדר, בלי תנאי).
- כן (PostgreSQL, Redis, MongoDB, RabbitMQ, כל מסד) →
-
האם ה-dependency חיוני ל-web?
- כן (בלי ה-db, האפליקציה לא יכולה לעבוד) →
depends_onעםservice_healthy(כדי שה-web לא יעלה בלי ה-db). - לא (cache, observability, sidecar) → בלי
depends_on; ה-web עולה גם בלי, ומתמודד עם היעדרות בזמן ריצה.
- כן (בלי ה-db, האפליקציה לא יכולה לעבוד) →
הכלל המעשי: כל מסד-נתונים → service_healthy. כל cache → service_healthy. כל service שהוא "חובה-לפני-העלייה" → service_healthy. השאר → פשוט או בלי.
זו הטעות השנייה הכי נפוצה אחרי ה-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 אנונימיים.
עכשיו, באותו 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 עוד לא התחיל.
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. זה עובד? לפעמים. אבל זה לא דיבאג — זה קסם. במקום:
docker compose ps— מה הסטטוס? ה-db healthy או starting/unhealthy?docker compose logs db— מה ה-db אומר?docker compose logs web— מה השגיאה האמיתית?docker compose exec web sh— היכנסו ובדקוenv | grep DATABASE(האם ה-URL נכון?),ping db(האם ה-DNS עובד?).
ארבע פקודות אבחון במקום restart-ים עיוורים. זה ההבדל בין "אני מקווה שזה יעבוד" לבין "אני יודע למה זה לא עובד".
עם ה-stack הרץ שלכם, הריצו ברצף:
docker compose ps— ראו את הסטטוס.docker compose logs --tail 20— ראו את 20 השורות האחרונות של כל ה-services.docker compose logs db --tail 5— רק את ה-db.docker compose exec db psql -U user -d mydb -c "SELECT 1"— בדקו שאתם יכולים לדבר עם ה-db מתוך ה-container שלו.docker compose down && docker compose up -d— כיבוי מוחלט והפעלה מחדש. ודאו שהכל עולה בסדר.
תוצאה צפויה: הרגל של חמש פקודות. אחרי התרגיל הזה, תרגישו ש-Compose זה לא "עוד כלי" — זה ה-API שאתם משתמשים בו כל יום.
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 יעזרו.
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
שימו לב לשני דברים:
- ה-
DATABASE_URLב-web מצביע עלdb:5432— לא עלlocalhost, לא עלhost.docker.internal. רקdb, שם ה-service. זה עובד בלוקל, ב-VPS, ב-CI, בכל מקום. - ה-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, וזה יעבוד.
אם אתם רק רוצים לוודא שלא שכחתם כלום, הנה checklist של המינימום:
-
services: ברמה העליונה. בלי זה, אין קובץ Compose.
-
image או build לכל service. בלי אחד מהם, ה-service לא יכול לעלות.
-
ports לכל service שצריך להיות נגיש מ-host (web, כלי dev, admin). ה-db לא צריך — הוא רץ ברשת הפנימית.
-
volumes לכל מסד-נתונים (named, לא anonymous). הצהרה ברמה העליונה + mount בתוך ה-service.
-
healthcheck לכל service שצריך warmup (בעיקר db, cache, broker).
-
depends_on עם
condition: service_healthyלכל web/app שמתחבר ל-db. זה מה שמונע את ה-flakiness. -
env_file: .env לכל service שצריך config רגיש.
.envב-.gitignoreוב-.dockerignore.
אם כל השבעה נמצאים — הקובץ שלכם production-ready. חסר משהו? חיזרו לסעיף המתאים. הקובץ הזה יחיה גם ב-VPS, גם ב-CI, גם ב-Coolify (פרק 6).
המטרה: לבנות compose.yaml שמחבר את האפליקציה שלכם ל-PostgreSQL, עם healthcheck, named volume, ו-depends_on מותנה — ולראות שאין connection refused באתחול ראשון.
מה תעשו:
- בתיקיית הפרויקט שלכם, צרו
compose.yamlעם שני services:web(build מ-Dockerfile שלכם) ו-db(imagepostgres:16-alpine). - ל-
db, הוסיפו environment עםPOSTGRES_USER,POSTGRES_PASSWORD,POSTGRES_DB(קחו מ-.envשלכם), named volumepgdata:/var/lib/postgresql/data, ו-healthcheck עםpg_isready. - ל-
web, הוסיפוports: ["3000:3000"],env_file: [.env], ו-depends_on: db: { condition: service_healthy }. - ב-
.env, ודאו ש-DATABASE_URLמצביע עלdb:5432(לא localhost). - ברמה העליונה של
compose.yaml, הצהירוvolumes: pgdata:. - הריצו:
docker compose up -d. - הריצו:
docker compose ps. ודאו שה-db מסומןUp (healthy)וה-webUp. - הריצו:
docker compose logs db. חפשו שורה שאומרת "database system is ready to accept connections". - הריצו:
docker compose logs web. ודאו שאיןconnection refusedאוECONNREFUSED. - פתחו
http://localhost:3000ב-browser. האפליקציה אמורה לעבוד ולהציג נתונים.
תוצאה צפויה: ה-stack עולה, ה-db healthy, ה-web רץ בלי connection refused, האפליקציה נגישה ב-browser. ה-volume pgdata קיים (אפשר לאמת עם docker volume ls). אתם יכולים לראות את הקסם של Compose בעיניים.
המטרה: להוכיח ש-condition: service_healthy באמת פותר את ה-flakiness, על-ידי הרצה חוזרת של ה-stack מאפס וצפייה בסדר ההפעלה.
מה תעשו:
- עם ה-stack הרץ, הריצו:
docker compose down(כיבוי מוחלט, volume נשמר). - עכשיו, בלי לחכות, הריצו:
docker compose up(foreground, רואים logs בזמן אמת). - הסתכלו על הלוג. הסדר שאתם צריכים לראות:
- ה-db מתחיל, רושם שהוא מאזין ל-5432.
- ה-healthcheck רץ. הסטטוס
starting. - ה-probe מצליח, הסטטוס
healthy. - רק אז ה-web מתחיל.
- ה-web מתחבר בהצלחה ל-db בלי שגיאות.
- לחיצה על Ctrl+C כדי לצאת מ-foreground (אחרת הוא ימשיך לרוץ).
- אם ראיתם את הסדר הזה — הצלחתם.
depends_on: condition: service_healthyעובד.
תוצאה צפויה: אפס connection refused ב-logs של ה-web. ה-web מתחיל רק אחרי שה-db הוא healthy. הקסם של ה-probe נראה בזמן אמת. אם במקרה ראיתם connection refused — חיזרו לסעיף 8 ובדקו שה-healthcheck ו-depends_on מוגדרים נכון.
המטרה: להוכיח שה-named volume של המסד שורד docker compose down (ולא רק docker rm של container בודד).
מה תעשו:
- הריצו את ה-stack:
docker compose up -d. - צרו רשומה באפליקציה דרך ה-browser (פוסט, משתמש, comment — מה שה-API שלכם תומך בו). רשמו בצד כמה רשומות יש.
- עכשיו:
docker compose down. שימו לב — זה מוחק containers וגם רשתות, אבל לא את ה-volume. אמתו:docker volume ls. ה-volumemyapp_pgdata(או דומה) צריך להיות שם. - הריצו שוב:
docker compose up -d. - חכו שה-stack יעלה (5-10 שניות).
- רעננו את הדף ב-browser. תוצאה צפויה: אותה כמות רשומות, אותו תוכן. המסד זוכר הכל.
- אם אתם רוצים לראות את ההפך:
docker compose down -v(עם-v!). הריצוup -dשוב. עכשיו המסד ריק.
תוצאה צפויה: אחרי down (בלי -v) — הרשומות שרדו. אחרי down -v (עם -v) — המסד ריק. זו ההוכחה הישירה ש-down רגיל לא מוחק volumes, אבל down -v כן. בחרו את הווריאנט הנכון לכל סיטואציה.
המטרה: ללמוד את תהליך הדיבאג כש-stack לא עובד. הסיטואציה הקלאסית: ה-DATABASE_URL עדיין מצביע על host.docker.internal במקום db.
מה תעשו:
- ב-
.env, שנו אתDATABASE_URLבכוונה ל-ערך שגוי:postgres://user:pass@host.docker.internal:5432/mydb(הקסם הישן מפרק 4). - הריצו:
docker compose up -d. - הריצו:
docker compose ps. ה-web רץ (כי הוא לא תלוי ב-connection — הוא תלוי רק ב-startup). - הריצו:
docker compose logs web. צפו לראות: שגיאתconnection refusedאוENOTFOUNDאוcould not translate host name "host.docker.internal". - הריצו:
docker compose exec web env | grep DATABASE_URL. תראו: ה-URL השגוי, כיenv_fileטען אותו. - עכשיו, תקנו: שנו ב-
.envאת ה-URL בחזרה ל-postgres://user:pass@db:5432/mydb. - הריצו:
docker compose up -d. שימו לב — לא צריך--buildכי ה-image לא השתנה, רק ה-env. - הריצו:
docker compose logs web. צפו לראות: החיבור הצליח, אין connection refused.
תוצאה צפויה: אתם יודעים עכשיו לאבחן בעיות חיבור. ה-flow הוא: ps (מה רץ) → logs (מה השגיאה) → exec env (מה הערך שהועבר) → תיקון ב-.env או ב-compose.yaml → up שוב. ארבע פקודות אבחון שחוסכות שעות של ניחושים.
יומי (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 — אותו קובץ, אפס שינוי.
-
שאלה: חבר שלח לכם קובץ
docker-compose.yml(עם מקף בשם הקובץ) ובוdocker-compose upבתיעוד. מה אתם עושים?תשובה: שני שינויים: (א) הקובץ עובד, אבל עדיף לשנות ל-
compose.yamlבלי מקף — זה השם המומלץ בתיעוד הרשמי של v2. (ב) הפקודה בתיעוד צריכה להיותdocker compose up(רווח) ולאdocker-compose up(מקף). v1 הוסר מ-Docker Desktop. אחרי התיקון, ה-stack יעבוד. הלקח: הקובץ סלחן (שני השמות נתמכים), אבל הפקודה לא — מקף = לא יעבוד ב-Docker Desktop הנוכחי. -
שאלה: יש לכם
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. -
שאלה: ה-DATABASE_URL ב-
.envשלכם הואpostgres://user:pass@localhost:5432/mydb. האפליקציה לא מתחברת ל-db. מה הבעיה, ולאיזה ערך צריך לשנות?תשובה:
localhostבתוך container פירושו "ה-container עצמו", לא "המארח" ולא "ה-db". ה-db רץ ב-container אחר עם hostnamedb(שם ה-service ב-Compose). התיקון:DATABASE_URL=postgres://user:pass@db:5432/mydb.dbהוא hostname תקף ברשת הפנימית של Compose, ועובד בלוקל, ב-VPS, וב-CI. אותו דבר נכון לכל service אחר (redis, mongo, rabbitmq) — תמיד שם ה-service. -
שאלה: הרצתם
docker compose down -vועכשיו המסד ריק. למה? ואיך הייתם מונעים את זה אם זה היה production?תשובה:
docker compose down -v(עם-v) מוחק גם את ה-named volumes, מה שמוחק את המסד. בלי-v, ה-named volume נשמר. מניעה: (א) אל תשתמשו ב--vבטעות. (ב) הרגילו את עצמכם:down= "סגור, מסד נשמר" (בטוח),down -v= "מחק הכל, כולל המסד" (מסוכן). (ג) הוסיפו תיעוד פנימי בקבוצה: "אסורdown -vבלי אישור של מישהו שמבין מה הוא עושה". בפרודקשן אמיתי, אתם רוצים גם backup של ה-volume לפני כל פעולה הרסנית — אבל זה כבר נושא לפרק הבא והלאה. -
שאלה: ה-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-ים עיוורים.
אם תוציאו רק פעולה אחת מהפרק הזה השבוע — שתהיה זאת: פתחו את ה-compose.yaml שלכם וודאו שיש בו healthcheck על ה-db וגם depends_on: condition: service_healthy על ה-web. שני אלה יחד מסירים את ה-connection refused שמטריד מתחילים באתחול ראשון. בלי אחד מהם — ה-stack יעבוד "בערך", ולפעמים יקרוס. עם שניהם — הוא תמיד יעלה בסדר. זו ההבדלה בין stack שמרגיש "קסום" לבין stack שמרגיש "מהימן". הכל השאר — image, build, ports, volumes, env, profiles — זה תרגום של מה שאתם כבר יודעים. ה-healthcheck + condition זה מה שחדש.
- Docker Compose = קובץ אחד לכל ה-stack. במקום N פקודות
docker run, קובץcompose.yamlאחד עם N services.docker compose upמרים הכל בפקודה אחת, בסדר הנכון, עם רשת פנימית. - docker compose (רווח) ≠ docker-compose (מקף). v2 = plugin מודרני, רווח, ב-Docker Desktop הנוכחי. v1 = סקריפט ישן, מקף, הוסר. כל מדריך לפני 2023 וכל פלט AI לא מעודכן יפלטו מקף. תמיד תחליפו לרווח.
- image מול build באותו קובץ. האפליקציה שלכם =
build. כל השאר (PostgreSQL, Redis, Nginx) =imageמ-registry. אתם לא בונים תוכנה שמישהו אחר כבר בנה. - Service discovery לפי שם. ה-web מתחבר ל-
db:5432, לא ל-localhostולא ל-host.docker.internal. זה hostname ש-Compose רושם ב-DNS הפנימי, וזה עובד בלוקל, ב-VPS, ב-CI — בלי שינוי. - depends_on לבדו לא מספיק. הוא מחכה להתחלה, לא למוכנות.
connection refusedבאתחול ראשון = תוצאה צפויה. התיקון:healthcheck+condition: service_healthy. - healthcheck + condition = סוף ה-flakiness.
pg_isreadyבודק אם PostgreSQL באמת מקבל חיבורים.depends_on: db: { condition: service_healthy }מחכה שהסטטוס יהיהhealthyלפני שה-web מתחיל. אחרי כן, תמיד עולה בסדר. - חמש פקודות הן 90% מהעבודה היומית.
up -d(הרם),up -d --build(הרם + בנה מחדש),ps(סטטוס),logs(פלט),down(כבה, מסד נשמר). שאר הפקודות —exec,restart,stop,config— שימושיות, אבל משניות.
בפרק 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: ., ונפעיל אותו על שרת.
- ☐ אני יכול להסביר למה צריך Docker Compose — הבעיה של שתי פקודות
docker runנפרדות, ה-flakiness של סדר ידני, וה-hostnamehost.docker.internalשעובד רק בלוקל. - ☐ יש לי
compose.yamlשמכיל לפחות שני services:web(build) +db(image), עםports,volumes,env_file, ו-depends_on. - ☐ אני יודע את ההבדל בין
docker compose(רווח, v2, plugin מודרני) לביןdocker-compose(מקף, v1, deprecated והוסר), ותמיד משתמש ברווח. - ☐ ה-DATABASE_URL ב-
.envמצביע עלdb:5432(לא עלlocalhostאוhost.docker.internal), והאפליקציה מתחברת בהצלחה דרך service discovery. - ☐ ל-service של ה-db יש
healthcheckעםpg_isready(או probe מקבילה), ול-service של ה-web ישdepends_on: db: { condition: service_healthy }. - ☐
docker compose up -dמעלה את ה-stack בליconnection refusedבאתחול ראשון, ו-docker compose psמראה את ה-db ב-healthy. - ☐ הרצתי
docker compose down(בלי-v) ואזup -dוהנתונים במסד שרדו — הוכחה שה-named volume מוצהר נכון ברמה העליונה. - ☐ אני יודע את חמש הפקודות היומיות:
up -d,up -d --build,ps,logs,down— ומתי כל אחת רלוונטית. - ☐ אני יודע את ארבע פקודות האבחון כשמשהו לא עובד:
ps→logs→exec env→ תיקון →upשוב. לא restart-ים עיוורים. - ☐ קיים
.env.exampleציבורי (ב-Git) שמראה את המבנה של המשתנים בלי ערכים אמיתיים, ו-.envמוחרג גם מ-.gitignoreוגם מ-.dockerignore. - ☐ אני מבין ש-
docker compose down -vמוחק גם את ה-volume (ולכן את המסד), ולא משתמש בזה בטעות. - ☐ אני מרגיש/ה שאני מוכן/ה לפרק 6 (deploy ל-VPS). ה-
compose.yamlשלי רץ יציב, המסד שורד restart, ואני יודע לדבג אותו בלי קסם.