Docker Compose: הגדרת הפעלה אוטומטית של שירותים ב-boot
למדו כיצד להגדיר restart policy ב-docker-compose.yaml כדי להבטיח שהמכולות יעלו לאחר אתחול. הסבר על ההבדל בין always ל-unless-stopped ומתי נדרש שירות systemd מותאם.
התשובה הקצרה
שירותי Docker Compose עולים בעת האתחול כאשר מתקיימים שני תנאים בו-זמנית. ה-daemon של Docker חייב להיות מופעל כשירות מערכת, ולכל שירות בקובץ חייבת להיות מדיניות restart של unless-stopped או always. הוסיפו restart: unless-stopped לכל שירות, הריצו docker compose up -d פעם אחת, והמכולות יחזרו לפעול מעצמן לאחר reboot. שום דבר נוסף אינו נדרש עבור המקרה הנפוץ.
יש צורך ב-systemd unit רק כאשר סדר העלייה קריטי: stack התלוי בכונן ממופה (mounted disk), בממשק VPN, או בשיתוף רשת שאינו זמין ברגע שבו ה-daemon של Docker עולה. זהו מקרה אמיתי, והחלק השני של מדריך זה עוסק בו. אם אתם עדיין לומדים את יסודות הגדרות השירותים וה-volumes, התחילו ב-יסודות Docker Compose ב-VPS וחזרו לכאן לאחר מכן.
הגדרת מדיניות הפעלה מחדש ב-compose.yaml
המדיניות מוגדרת בשורה אחת לכל שירות. לא קיים מתג גלובלי, לכן שירות שנשכח יישאר כבוי לאחר אתחול השרת, בעוד שאר ה-stack יעלה.
services:
app:
image: nginx:1.27
restart: unless-stopped
ports:
- "8080:80"
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_PASSWORD: change-me
volumes:
- dbdata:/var/lib/postgresql/data
volumes:
dbdata:החילו את השינוי ולאחר מכן בדקו את המדיניות של המכולה הפעילה:
docker compose up -d
docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q app)הפקודה תדפיס unless-stopped. אם מודפס no, הקובץ נערך אך המכולה לא נוצרה מחדש.
זוהי נקודת הכשל הנפוצה ביותר. מדיניות ההפעלה מחדש נשמרת במכולה, לא בקובץ ה-YAML. עריכת compose.yaml אינה משנה דבר במכולה קיימת. גם docker compose restart לא תועיל, כיוון שהיא עוצרת ומפעילה את אותו אובייקט מכולה מבלי לגעת בתצורה שלו. רק docker compose up -d משווה בין הקובץ לבין המכולות הרצות, מזהה שהמדיניות השתנתה, ויוצרת אותן מחדש.
עבור מכולה שאינכם רוצים ליצור מחדש כרגע, שנו את המדיניות בשידור חי:
docker update --restart unless-stopped my-containerעדיין יש לערוך גם את קובץ ה-YAML. docker update משנה את המכולה הפעילה, והפקודה docker compose up -d הבאה תקרא את הקובץ ותחזיר את הערך הישן.
מה עושה כל ערך restart בפועל
Docker מגדיר ארבעה ערכים, וההבדל ביניהם בא לידי ביטוי רק כאשר המכונה מבצעת reboot או כאשר ה-daemon מופעל מחדש.
noהוא ערך ברירת המחדל. המכולה לעולם לא תופעל מחדש באופן אוטומטי, בשום נסיבות.alwaysמפעיל את המכולה מחדש בכל פעם שהיא נעצרת. אם עצרתם אותה ידנית, היא תחזור לפעול בכל מקרה בפעם הבאה שה-daemon של Docker יעלה. זה מפתיע לעיתים קרובות: מכולה שעצרתם במכוון בשבוע שעבר חוזרת לפעול לאחר reboot.unless-stoppedמתנהג כמוalways, מלבד העובדה שמכולה שנעצרה ידנית תישאר כבויה גם לאחר הפעלה מחדש של ה-daemon. זהו הערך הרצוי עבור שירות שאתם משביתים מדי פעם לצורכי תחזוקה.on-failureמפעיל את המכולה מחדש רק כאשר היא מסתיימת עם exit code שאינו אפס. ניתן להגביל את מספר הניסיונות, כפי שמופיע ב-restart: on-failure:3.
עבור stack שאמור פשוט להיות פעיל בכל פעם שהשרת פעיל, unless-stopped הוא ברירת המחדל הנכונה. בחרו ב-always רק כאשר אתם רוצים מכולה שתמשיך לפעול גם אם ניסו להשאיר אותה כבויה.
מדוע restart: on-failure לא שורד אתחול
אנשים רבים בוחרים ב-on-failure כי זה נשמע זהיר, ואז מגלים שכל המכולות נעצרו לאחר האתחול הראשון. הסיבה טמונה בהגדרה. on-failure מגיב לדבר אחד בלבד: תהליך המכולה שנסגר עם קוד שגיאה.
אתחול אינו נחשב לשגיאה. כאשר המארח נכבה, systemd עוצר את docker.service, וה-daemon עוצר כל מכולה באופן יזום. המכולה לא קרסה, לכן למדיניות אין על מה להגיב. בעלייה מחדש, ה-daemon בודק אילו מכולות עליו להפעיל, ומכולה עם on-failure שנעצרה בצורה מסודרת אינה נכללת ברשימה. היא נשארת במצב exited.
ניתן לראות זאת ישירות. הגדירו restart: on-failure בשירות, הריצו docker compose up -d, בצעו אתחול, ואז הריצו:
docker compose ps -aהשירות מופיע במצב Exited עם סטטוס כמו Exited (0) 2 minutes ago. שום דבר לא שבור ושום דבר לא נרשם כשגיאה, וזה מה שהופך את הבעיה לקשה לאבחון. המדיניות ביצעה בדיוק את מה שהוגדר לה.
on-failure עדיין שימושי. הוא מתאים למכולה שמריצה משימה ועלולה לקרוס. כשלעצמו, restart: on-failure אינו מגדיר גבול וממשיך להפעיל מחדש משימה שנכשלת שוב ושוב, לכן הוסיפו הגבלה כמו restart: on-failure:3 כאשר אתם רוצים מספר מוגבל של ניסיונות חוזרים ללא לולאת הפעלה מחדש. זהו הכלי הלא נכון לשמירה על שירות ארוך-טווח פעיל לאחר אתחולים.
מדיניות Restart פועלת רק אם שירות Docker עולה בזמן ה-boot
מדיניות ה-Restart נאכפת על ידי ה-daemon של Docker. אם ה-daemon אינו עולה, שום דבר לא ייאכף. בדקו זאת:
systemctl is-enabled docker
systemctl is-enabled containerdשתי הפקודות צריכות להחזיר enabled. החבילות מהמאגר הרשמי של Docker מפעילות אותן בזמן ההתקנה, לכן בשרת חדש זה בדרך כלל תקין. אם אחת מהן מחזירה disabled, תקנו זאת:
sudo systemctl enable --now docker containerdיש כאן מלכודת שחשוב להבין. Ubuntu מספקת גם את docker.socket, שמפעיל את ה-daemon לפי דרישה ברגע שגורם כלשהו פונה ל-API של Docker. משתמשים רואים ש-docker.socket מופעל, מניחים שה-daemon מכוסה, ומנטרלים את docker.service כדי לחסוך בזיכרון. בזמן ה-boot, אף אחד לא פונה ל-API, לכן ה-socket לא נגיש, ה-daemon לא עולה, ואף container לא יופעל עד שתקלידו את פקודת ה-docker הראשונה שלכם. הפעלת socket אינה תחליף להפעלה של docker.service.
מתי יחידת systemd היא הפתרון העדיף
למדיניות הפעלה מחדש (restart policies) אין מנגנון לניהול סדר פעולות מול שאר המערכת. ה-daemon עולה ומפעיל את המכולות שלך ברגע שהוא יכול. אם ה-stack שלך מבצע bind-mount לתיקייה מתוך כונן נפרד, שיתוף NFS (רשת קבצים), או כונן מוצפן, המכולות עלולות לעלות לפני שהנתיב קיים. Docker ייצור בשמחה תיקייה ריקה בנקודת ה-mount ויפעיל את המכולה מולה, והתוצאה תהיה מסד נתונים שעולה ללא נתונים.
יש לכתוב יחידת systemd בכל אחד מהמקרים הבאים: ה-stack זקוק ל-mount, לממשק VPN, או ליחידה אחרת שתהיה מוכנה קודם לכן. אתה מעוניין ש-systemctl stop myapp ו-systemctl start myapp יפעלו כפי שהם פועלים עבור כל שירות אחר בשרת. לחלופין, אם ברצונך שה-stack ייסגר בצורה מסודרת בזמן כיבוי המערכת, במקום שייקטל יחד עם ה-daemon. אם יחידות systemd הן נושא חדש עבורך, המדריך כתיבת שירות וטיימר ב-systemd מפרט את מבנה הקבצים לעומק.
כתיבת ה-unit של systemd
מקמו את ה-stack בנתיב קבוע מחוץ לספריית הבית. /srv/myapp היא בחירה טובה, כיוון ש-unit שרץ לפני שמישהו התחבר למערכת לא אמור לקרוא מ-/home.
צרו את /etc/systemd/system/myapp.service:
[Unit]
Description=myapp docker compose stack
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target
RequiresMountsFor=/srv/myapp/data
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/srv/myapp
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=0
[Install]
WantedBy=multi-user.targetהפעילו והריצו אותו:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
systemctl status myapp.serviceunit תקין מציג Active: active (exited). זה נראה שגוי בפעם הראשונה שרואים זאת. זה נכון: Type=oneshot עם RemainAfterExit=yes אומר שה-unit הריץ את הפקודה שלו, הפקודה הסתיימה, ו-systemd שומר על ה-unit מסומן כ-active כדי ש-ExecStop ירוץ בעת כיבוי.
לכל שורה יש תפקיד. Requires=docker.service אומר שה-unit ייכשל מהר במקום להריץ docker compose מול socket מת. After= קובע את סדר הפעולות, כיוון ש-Requires= לבדו אינו עושה זאת. RequiresMountsFor= גורם ל-systemd למשוך את ה-unit של ה-mount עבור אותו נתיב ולהמתין לו, וזו הסיבה העיקרית להשתמש ב-unit במקום במדיניות הפעלה מחדש. TimeoutStartSec=0 מונע מ-systemd להרוג את תהליך ההפעלה בזמן ש-image גדול עדיין נמשך.
הערה על שילוב שני המנגנונים. התיעוד של Docker ממליץ לא לשלב מדיניות הפעלה מחדש עם מנהל תהליכים ברמת המארח, בטענה שזה יוצר התנגשויות. ההתנגשות נוצרת ממנהל תהליכים שמפקח על תהליך המכולה עצמו ומפעיל אותו מחדש בזמן שה-daemon מנסה לעשות את אותו הדבר. unit מסוג Type=oneshot אינו מפקח על דבר, לכן השארת restart: unless-stopped בקובץ ה-compose לצד ה-unit הזה היא תקינה, וזה מה שאתם רוצים. systemd מטפל בסדר הפעולות בעלייה, וה-daemon מטפל במכולה שקורסת בשלוש לפנות בוקר.
ה-unit נראה אחרת כאשר הדבר שאתם שומרים עליו פעיל הוא תהליך פשוט וארוך-טווח ולא stack, כיוון שאז אין daemon מתחתיו וה-Restart= של systemd עצמו חייב לבצע את הפיקוח; הרצת dsh ללא ממשק מאחורי systemd היא דוגמה מעשית לצורה זו, כולל המשתמש הייעודי וה-journal.
אימות באמצעות אתחול מלא
אין תחליף לבדיקה אמיתית. systemctl restart docker אינו בוחן את סדר ה-mount, וביצוע docker compose down ולאחריו docker compose up -d אינו בוחן דבר הקשור לתהליך האתחול.
sudo rebootהמתינו, התחברו מחדש ובדקו לפי הסדר הבא:
uptime
systemctl is-active docker
docker compose psהפקודה uptime מאשרת שאתם עובדים על מכונה שאכן עברה אתחול. docker compose ps, בהרצה מתוך תיקיית ה-stack, אמורה להציג כל שירות כ-running עם זמן פעילות (uptime) הקרוב לזה של המכונה. שירות המציג Exited הוא השירות שיש לבדוק.
אם רכיב כלשהו לא עלה, לוג ה-daemon מכסה את חלון הזמן של האתחול:
journalctl -u docker.service -b --no-pager | tail -50עבור stack המנוהל על ידי unit, הפקודה journalctl -u myapp.service -b --no-pager מציגה את פלט ה-docker compose המדויק מהאתחול, כולל משיכת image שנכשלה או קובץ .env חסר. האתחול שאתם מתזמנים הוא זה שאתם מנטרים, לכן תנו ל-unit לדווח לכם על אלו שלא: שורת OnFailure= המופנית אל שרת ntfy בניהול עצמי הופכת stack שנכשל בעלייה להתראה בדחיפה, במקום תקלה שתגלו רק כעבור ימים.
גורמים להפסקת עלייה אוטומטית שקטה
מכולות שנוצרו באמצעות docker compose run לעולם לא יקבלו את מדיניות ה-restart המוגדרת בקובץ. Compose מתייחס אליהן כמכולות חד-פעמיות. אם נראה ששירות מתעלם מהמדיניות שלו, בדקו אם הוא הופעל באמצעות run במקום up.
נתיב יחסי ב-volume או בערך env_file מחושב ביחס לתיקייה שבה נמצא קובץ ה-compose. זה עובד מה-shell שלכם, וזה עובד מיחידת systemd שמגדירה WorkingDirectory. זה נכשל מיחידה ללא הגדרה כזו, כיוון שתיקיית העבודה היא אז /.
Docker במצב rootless הוא מקרה נפרד. ה-daemon רץ כשירות משתמש, ושירות משתמש נעצר כאשר הסשן האחרון של אותו משתמש מסתיים. הפעילו אותו עבור המשתמש ואפשרו לו להמשיך לרוץ גם כשאין אף משתמש מחובר:
systemctl --user enable docker
sudo loginctl enable-linger $USERללא enable-linger, ה-daemon במצב rootless נכבה בעת התנתקות (logout) והמכולות נסגרות יחד איתו, מה שנראה בדיוק כמו מדיניות restart תקולה.
דבר אחרון. עדכוני אבטחה אוטומטיים יכולים לבצע reboot לשרת בשעה קבועה, וזה דבר חיובי רק אם ה-stack שלכם חוזר לעבוד מעצמו. הגדרת נושא זה במכונה חדשה היא חלק מהעבודה שיש לבצע בשעה הראשונה, כפי שמתואר ב-עשר הדקות הראשונות ב-VPS חדש.
FAQ
מה ההבדל בין restart: always לבין restart: unless-stopped?
שתי ההגדרות מפעילות מחדש את המכולה כאשר היא נעצרת מעצמה. ההבדל ביניהן בא לידי ביטוי לאחר עצירה ידנית של המכולה. עם always, המכולה תופעל מחדש בפעם הבאה שבה ה-daemon של Docker יעלה, כך שאתחול של השרת מבטל את העצירה הידנית שביצעת. עם unless-stopped, ה-daemon זוכר שהמכולה נעצרה בכוונה ומשאיר אותה כבויה. השתמשו ב-unless-stopped אלא אם כן אתם זקוקים ספציפית למכולה שלעולם לא תישאר כבויה.
הוספתי restart: unless-stopped אך המכולה עדיין לא עולה לאחר אתחול. מדוע?
מדיניות ההפעלה מוגדרת על המכולה עצמה ולא בקובץ, ומכולה קיימת אינה מתעדכנת רק על ידי עריכת ה-YAML. הריצו את docker compose up -d כדי ש-Compose ייצור אותה מחדש, ולאחר מכן אשרו זאת עם docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q app). אם הפלט הוא no, סימן שהמכולה נוצרה לפני העדכון שביצעתם. סיבה נפוצה נוספת היא ש-docker.service אינו מופעל, דבר שניתן לבדוק באמצעות systemctl is-enabled docker.
האם אני זקוק ל-systemd unit אם אני כבר משתמש במדיניות הפעלה מחדש?
בדרך כלל לא. מדיניות הפעלה מחדש מספיקה עבור רוב ה-stacks, שזקוקים רק לרשת. הוסיפו unit כאשר המכולות תלויות ברכיב שאינו זמין בעת עליית ה-daemon של Docker, כגון כונן חיצוני, כרך מוצפן, שיתוף NFS או ממשק VPN. ה-unit מאפשר לכם לקבוע סדר עלייה באמצעות After= ו-RequiresMountsFor=, יכולת שאינה קיימת במדיניות הפעלה מחדש.
כיצד עוצרים stack לצמיתות מבלי שיחזור לאחר אתחול?
עם unless-stopped, הפקודה docker compose stop מספיקה, כיוון שמכולה שנעצרה ידנית אינה מופעלת מחדש כאשר ה-daemon עולה. עם always, עצירה אינה מספיקה והמכולה תחזור לאחר אתחול. עליכם להריץ את docker compose down, שמסיר את המכולות, או לשנות תחילה את המדיניות עם docker update --restart no my-container. אם ה-stack מנוהל על ידי systemd unit, הריצו גם את sudo systemctl disable myapp.service, אחרת ה-unit יפעיל אותו מחדש.