Docker Compose: הפעלה אוטומטית לאחר אתחול
כך שירותי Docker Compose חוזרים לאחר אתחול: מדיניות restart מתאימה, מדוע on-failure אינה מספיקה, ומתי יחידת systemd פותרת תלות בסדר ההפעלה.
התשובה הקצרה
שירותי Docker Compose מופעלים בעת האתחול כאשר שני תנאים מתקיימים בו-זמנית. ה-Docker daemon חייב להיות מופעל כשירות מערכת, ולכל שירות בקובץ חייבת להיות מדיניות הפעלה מחדש של unless-stopped או always. הוסיפו restart: unless-stopped לכל שירות, הריצו פעם אחת את docker compose up -d, והקונטיינרים יופעלו מחדש באופן עצמאי לאחר אתחול. במקרה הנפוץ אין צורך בדבר נוסף.
יש צורך ביחידת systemd רק כאשר סדר ההפעלה חשוב: למשל, כאשר ה-stack תלוי בדיסק שעבר mount, בממשק VPN או בשיתוף רשת שאינם מוכנים בזמן הפעלת ה-Docker daemon. זהו תרחיש ממשי, והחלק השני של המדריך עוסק בו. אם עדיין אינכם מכירים היטב הגדרות שירותים ו-volumes, התחילו ביסודות Docker Compose ב-VPS וחזרו לכאן.
הגדרת מדיניות האתחול מחדש ב- compose.yaml
המדיניות מוגדרת בשורה אחת לכל שירות. אין מתג גלובלי, לכן שירות ששכחתם להגדיר יישאר מושבת לאחר האתחול, בעוד ששאר המחסנית יופעלו.
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 הבאה תקרא את הקובץ ותחזיר את הערך הישן.
מה כל ערך אתחול עושה בפועל
Docker מגדיר ארבעה ערכים. ההבדל ביניהם מתגלה רק כאשר המחשב מופעל מחדש או כאשר ה-daemon מופעל מחדש.
noהוא ערך ברירת המחדל. המכולה לעולם אינה מופעלת מחדש באופן אוטומטי, בשום מצב.alwaysמפעיל מחדש את המכולה בכל פעם שהיא נעצרת. אם עצרתם אותה ידנית, היא תופעל שוב בפעם הבאה שה-daemon של Docker יופעל. הדבר מפתיע לעיתים קרובות: מכולה שעצרתם בכוונה בשבוע שעבר פועלת שוב לאחר אתחול.unless-stoppedפועל כמוalways, למעט העובדה שמכולה שנעצרה ידנית נשארת עצורה לאחר הפעלה מחדש של ה-daemon. זהו הערך המתאים לשירות שאתם משביתים מדי פעם לצורכי תחזוקה.on-failureמפעיל מחדש את המכולה רק כאשר היא מסתיימת עם קוד יציאה שאינו אפס. ניתן להגביל את מספר הניסיונות, כפי שמוצג ב-restart: on-failure:3.
עבור stack שאמור לפעול בכל פעם שהשרת פועל, unless-stopped הוא ערך ברירת המחדל המתאים. בחרו ב-always רק כאשר אתם רוצים למנוע ממכולה להישאר מושבתת.
מדוע הפעלה מחדש: on-failure אינה נשמרת לאחר אתחול
רבים בוחרים ב-on-failure משום שהוא נשמע זהיר, ואז מגלים שכל הקונטיינרים הופסקו לאחר האתחול הראשון. הסיבה נמצאת בהגדרה. on-failure מגיב לדבר אחד בלבד: לתהליך הקונטיינר שמסתיים עם קוד שגיאה.
אתחול אינו שגיאה. בעת כיבוי המארח, systemd עוצר את docker.service, והדמון עוצר כל קונטיינר באופן יזום. הקונטיינר לא נכשל, ולכן למדיניות אין דבר שאליו עליה להגיב. בעת העלייה מחדש, הדמון בודק אילו קונטיינרים נדרשים לחידוש הפעולה, וקונטיינר מסוג on-failure שהופסק באופן תקין אינו נכלל בהם. הוא נשאר במצב exited.
אפשר לראות זאת ישירות. הגדירו restart: on-failure בשירות, הריצו docker compose up -d, בצעו אתחול, ולאחר מכן הריצו:
docker compose ps -aהשירות יופיע עם מצב של Exited ועם סטטוס כגון Exited (0) 2 minutes ago. דבר אינו מקולקל, ושום דבר אינו נרשם כשגיאה, ולכן קשה לאבחן את הבעיה. המדיניות ביצעה בדיוק את מה שהוגדר לה.
on-failure עדיין שימושי. הוא מתאים לקונטיינר שמריץ משימה ועלול לקרוס, כאשר נדרש מספר מוגבל של ניסיונות חוזרים ללא לולאת הפעלה מחדש. זהו הכלי הלא נכון לשמירה על שירות שפועל לאורך זמן גם לאחר אתחולים.
מדיניות ההפעלה מחדש פועלת רק אם שירות Docker מופעל בעת האתחול
מדיניות ההפעלה מחדש נאכפת על ידי דמון Docker. אם הדמון אינו מופעל, אין מי שיאכוף את המדיניות. בדקו זאת:
systemctl is-enabled docker
systemctl is-enabled containerdבשני המקרים הפלט צריך להיות enabled. החבילות מהמאגר הרשמי של Docker מפעילות אותם בעת ההתקנה, ולכן בשרת חדש הבדיקה בדרך כלל מצליחה. אם אחד מהם מציג disabled, תקנו זאת:
sudo systemctl enable --now docker containerdיש כאן מלכודת שכדאי להבין. Ubuntu מספקת גם את docker.socket, שמפעיל את הדמון לפי דרישה בפעם הראשונה שתהליך כלשהו פונה ל־Docker API. משתמשים רואים ש־docker.socket מופעל, מניחים שהדמון מכוסה, ומשביתים את docker.service כדי לחסוך בזיכרון. בעת האתחול שום תהליך אינו פונה ל־API, ולכן לא מתבצעת גישה ל־socket, הדמון אינו מופעל, ושום קונטיינר אינו עולה עד שמקלידים את הפקודה הראשונה מסוג docker. הפעלה באמצעות socket אינה תחליף להפעלת docker.service.
מתי יחידת systemd היא הפתרון המתאים יותר
למדיניות הפעלה מחדש אין יכולת להגדיר סדר מול שאר המערכת. הדמון מופעל ומעלה את הקונטיינרים ברגע שהוא יכול. אם ה-stack שלך משתמש ב-bind mount של ספרייה מאמצעי אחסון נפרד, משיתוף NFS (מערכת קבצים ברשת) או מדיסק מוצפן, הקונטיינרים עלולים להתחיל לפני שהנתיב קיים. Docker ייצור ללא בעיה ספרייה ריקה בנקודת ה-mount ויפעיל את הקונטיינר כשהוא משתמש בה, ובסיס הנתונים שלך יעלה ללא נתונים.
כתוב יחידת systemd כאשר אחד מהתנאים הבאים מתקיים. ה-stack תלוי ב-mount, בממשק VPN או ביחידה אחרת שצריכים להיות מוכנים תחילה. אתה רוצה ש-systemctl stop myapp ו-systemctl start myapp יפעלו כפי שהם פועלים עבור כל שירות אחר במחשב. לחלופין, אתה רוצה שה-stack ייסגר בצורה מסודרת במהלך הכיבוי, במקום להיסגר בכוח יחד עם הדמון. אם יחידות systemd חדשות לך, כתיבת שירות ו-timer של systemd מסבירה את מבנה הקובץ בפירוט רב יותר.
כתיבת יחידת systemd
מקמו את המחסנית בנתיב קבוע, מחוץ לספריית בית. /srv/myapp היא בחירה טובה, משום שליחידה שפועלת לפני שמשתמש כלשהו מתחבר אין סיבה לקרוא את /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.serviceיחידה תקינה מציגה את Active: active (exited). בפעם הראשונה שרואים זאת, התוצאה נראית שגויה. היא תקינה: Type=oneshot יחד עם RemainAfterExit=yes פירושם שהיחידה הריצה את הפקודה שלה, הפקודה הסתיימה, ו-systemd משאיר את היחידה מסומנת כפעילה כדי ש-ExecStop יופעל בעת הכיבוי.
לכל שורה יש תפקיד. Requires=docker.service פירושו שהיחידה נכשלת במהירות, במקום להריץ את docker compose מול socket שאינו פעיל. After= מגדיר את סדר ההפעלה, משום ש-Requires= לבדו אינו עושה זאת. RequiresMountsFor= גורם ל-systemd לטעון את יחידת ה-mount עבור הנתיב הזה ולהמתין לה. זו הסיבה העיקרית להשתמש ביחידה במקום במדיניות restart. TimeoutStartSec=0 מונע מ-systemd להפסיק את משימת ההפעלה בזמן שעדיין מתבצעת משיכת image גדול.
הערה לגבי שילוב שני המנגנונים. התיעוד של Docker ממליץ שלא לשלב מדיניות restart עם מנהל תהליכים של המארח. האזהרה מתייחסת למנהל תהליכים שמפקח ישירות על תהליך ה-container ומפעיל אותו מחדש בזמן שה-daemon מנסה לעשות זאת בעצמו. יחידת Type=oneshot אינה מפקחת על דבר, לכן אפשר להשאיר את restart: unless-stopped בקובץ compose לצד יחידה זו, וזה גם המצב הרצוי. systemd מטפל בסדר ההפעלה בעת האתחול, וה-daemon מטפל ב-container שקורס בשלוש לפנות בוקר.
אימות באמצעות אתחול מחדש בפועל
אין תחליף לבדיקה בפועל. systemctl restart docker אינו בודק את סדר טעינת מערכות הקבצים, ו-docker compose down ולאחריו docker compose up -d אינם בודקים דבר הקשור לאתחול.
sudo rebootהמתינו, התחברו מחדש ובדקו בסדר הזה:
uptime
systemctl is-active docker
docker compose psuptime מאשר שאתם מחוברים למכונה שבאמת ביצעה אתחול מחדש. docker compose ps, כאשר הוא מופעל מתיקיית ה-stack, אמור להציג כל שירות במצב running, עם זמן פעולה קרוב לזה של המכונה. שירות שמופיע במצב Exited הוא השירות שיש לבדוק.
אם שירות מסוים לא הופעל, יומן ה-daemon מכסה את חלון האתחול:
journalctl -u docker.service -b --no-pager | tail -50עבור stack המנוהל באמצעות unit, journalctl -u myapp.service -b --no-pager מציג את פלט docker compose המדויק מהאתחול, כולל כשל במשיכת image או קובץ .env חסר.
דברים שמפסיקים בשקט את ההפעלה האוטומטית
Containers שנוצרו באמצעות docker compose run לעולם אינם מקבלים את מדיניות ההפעלה מחדש מהקובץ. Compose מתייחס אליהם כאל containers חד-פעמיים. אם נראה ששירות מתעלם מהמדיניות שלו, בדקו אם הוא הופעל באמצעות run במקום באמצעות up.
נתיב יחסי ב-volume או ברשומת env_file נפתר ביחס לתיקייה של קובץ ה-compose. הדבר פועל מה-shell, וגם מיחידה שמגדירה את WorkingDirectory. הוא נכשל מיחידה שאינה מגדירה אותו, משום שתיקיית העבודה היא אז /.
Docker ללא root הוא מקרה נפרד. ה-daemon פועל כשירות משתמש, ושירות משתמש נעצר כאשר מסתיימת ההפעלה האחרונה של אותו משתמש. הפעילו אותו עבור המשתמש ואפשרו לו להמשיך לפעול גם כאשר אף משתמש אינו מחובר:
systemctl --user enable docker
sudo loginctl enable-linger $USERללא enable-linger, ה-daemon ללא root נכבה כאשר אתם מתנתקים, וה-containers נעצרים יחד איתו. מצב זה נראה בדיוק כמו מדיניות הפעלה מחדש שאינה פועלת.
נקודה אחרונה. עדכוני אבטחה אוטומטיים יכולים לאתחל שרת בשעה קבועה. הדבר מועיל רק אם ה-stack שלכם עולה מחדש באופן אוטומטי. הגדרת התנהגות זו במכונה חדשה היא חלק משאר פעולות השעה הראשונה, תחת עשר הדקות הראשונות ב-VPS חדש.
FAQ
מה ההבדל בין restart: always לבין restart: unless-stopped?
שניהם מפעילים מחדש את הקונטיינר כאשר הוא נעצר מעצמו. ההבדל מתבטא לאחר שעוצרים קונטיינר באופן ידני. עם always, הקונטיינר מופעל שוב בפעם הבאה ש־Docker daemon מופעל, ולכן אתחול מחדש מבטל את העצירה הידנית. עם 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 אם אני כבר משתמש במדיניות restart?
בדרך כלל לא. מדיניות restart מספיקה ל־stack שזקוק רק לרשת, וזה המצב ברוב ה־stacks. הוסיפו יחידה כאשר הקונטיינרים תלויים ברכיב שאינו מוכן בעת הפעלת Docker daemon, כגון דיסק חיצוני, אמצעי אחסון מוצפן, שיתוף NFS או ממשק VPN. היחידה מספקת סדר הפעלה באמצעות After= ו־RequiresMountsFor=, דבר שמדיניות restart אינה יכולה לבטא.
כיצד עוצרים stack לצמיתות, בלי שהוא יופעל שוב באתחול הבא?
עם unless-stopped, די ב־docker compose stop, משום שקונטיינר שנעצר באופן ידני אינו מופעל מחדש כאשר ה־daemon מופעל מחדש. עם always, עצירה אינה מספיקה והקונטיינר יחזור לאחר אתחול מחדש. הפעילו את docker compose down, שמסיר את הקונטיינרים, או שנו תחילה את המדיניות באמצעות docker update --restart no my-container. אם יחידת systemd מנהלת את ה־stack, הפעילו גם את sudo systemctl disable myapp.service, אחרת היחידה תפעיל אותו שוב.