הרצת Docker על גבי VPS: מה באמת משתנה?
הרצת Docker על שרת VPS דורשת התייחסות לבעיות נפוצות כמו ניצול זיכרון RAM, עקיפת חוקי UFW, ניהול נפח דיסק וקונפיגורציית restart policy למכולות לאחר אתחול השרת.
מה משתנה בעת הרצת Docker על גבי VPS
Docker על גבי VPS מריץ את אותו מנוע ואותם images כמו Docker על המחשב הנייד שלכם, לכן כל פקודה שאתם כבר מכירים תמשיך לעבוד. מה שמשתנה הוא הסביבה מסביב. למחשב נייד יש זיכרון פנוי, firewall שאף אחד לא סורק, ודיסק גדול מספיק כך שלעולם לא תצטרכו לבדוק אותו. שרת שכור כולל תקרה קבועה של זיכרון, כתובת IP ציבורית שנסרקת תוך דקות מרגע העלייה, ומערכת קבצים של root ש־Docker ימלא ללא התראה.
ארבעה הבדלים גורמים לרוב התקלות בשרת קטן:
- הזיכרון מוגבל, וה־kernel פותר מחסור בזיכרון על ידי הריגת תהליך.
- פורט שפורסם עובר ישירות דרך UFW (uncomplicated firewall), כיוון ש־Docker כותב חוקי firewall משלו.
- מכולות לא חוזרות לפעול לאחר reboot אלא אם ביקשתם זאת מראש.
- תמונות (images), מכולות, volumes ו־build cache גדלים עד שהדיסק מתמלא.
כל סעיף להלן מציין את הכשל, את המחרוזת שתראו בפועל, ואת המדריך שמתקן זאת לעומק. אם עדיין לא כתבתם קובץ compose, קראו תחילה את יסודות Docker Compose על גבי VPS וחזרו לכאן. דף זה מניח שאתם כבר יודעים להעלות stack.
כמה זיכרון RAM צורך קונטיינר Docker?
פחות ממה שרוב האנשים מצפים. קונטיינר הוא תהליך בתוך cgroup (קבוצת בקרה), ולא מכונה וירטואלית, לכן אין בו kernel אורח ואין הקצאה קבועה. העלות היא כל מה שהתהליך שבפנים ניגש אליו. זו הסיבה שכל ה-stack נכנס ב-2 GB, בעוד שאותו stack הבנוי ממכונות וירטואליות לא היה נכנס.
הנתונים להלן הם מספרים טיפוסיים למצב סרק עבור אימג'ים סטנדרטיים ב-Ubuntu 24.04 עם תצורה ברירת מחדל, כפי שנקראו מ-docker stats דקות ספורות לאחר ההפעלה. הם מהווים נקודת התחלה לתכנון, לא מדד (benchmark) לעומס העבודה שלכם. הריצו את docker stats --no-stream על השרת שלכם לפני שתסתמכו על נתון כלשהו, כולל אלו.
The data behind this chart
[
{
"label": "nginx",
"idle_mb": 8,
"budget_mb": 64
},
{
"label": "Redis 7",
"idle_mb": 12,
"budget_mb": 128
},
{
"label": "Traefik v3",
"idle_mb": 40,
"budget_mb": 128
},
{
"label": "PostgreSQL 16",
"idle_mb": 45,
"budget_mb": 512
},
{
"label": "Uptime Kuma",
"idle_mb": 95,
"budget_mb": 256
},
{
"label": "MariaDB 11",
"idle_mb": 190,
"budget_mb": 512
},
{
"label": "Nextcloud (Apache)",
"idle_mb": 210,
"budget_mb": 768
}
]שתי העמודות מבצעות תפקידים שונים. idle_mb הוא מה שהקונטיינר צורך בזמן שהוא לא עושה כלום. budget_mb הוא מה שיש להקצות בעת התכנון, כיוון ששימוש אמיתי אינו מצב סרק. PostgreSQL נמצא במצב סרק סביב 45 MB ודורש 512 MB ברגע שחיבורים, מייונים וזיכרון מטמון פעילים. תכננו לפי עמודת התקציב. בצעו ניפוי שגיאות (debug) לפי עמודת הסרק.
שימו לב למבנה של 7 השורות הללו. nginx נמצא במצב סרק ב-8 MB ו-Nextcloud ב-210 MB. ה-proxy שלפני היישומים שלכם הוא כמעט בחינם. מסד הנתונים ויישום ה-PHP הם אלו שלפיהם קובעים את גודל השרת.
אזהרה אחת לגבי docker stats: נתון הזיכרון כולל page cache שקריאות הקבצים של הקונטיינר עצמו משכו פנימה, לכן הוא עולה במשך זמן מה לאחר ההפעלה ואז מתייצב. עקבו אחריו במשך שעה לפני שתחליטו שמשהו דולף.
הערכת גודל ל-VPS: מה נכנס ב-2 GB, 4 GB ו-8 GB
ראשית, יש להפחית את המשאבים שצורך המארח. ה-kernel, ה-systemd, ה-journald, ה-sshd ו-daemon של Docker פועלים כולם על אותו ה-RAM שבו רצים ה-containers שלכם, ו-dockerd יחד עם containerd תופסים כ-100 MB מתוכו. עליכם להשאיר גם זיכרון פנוי עבור ה-page cache, ועבור עומסים רגעיים שנוצרים בזמן בניית image או ביצוע dump למסד נתונים.
The data behind this chart
[
{
"plan": "2 GB VPS",
"total_mb": 2048,
"host_reserve_mb": 768,
"container_mb": 1280
},
{
"plan": "4 GB VPS",
"total_mb": 4096,
"host_reserve_mb": 1024,
"container_mb": 3072
},
{
"plan": "8 GB VPS",
"total_mb": 8192,
"host_reserve_mb": 1536,
"container_mb": 6656
}
]host_reserve_mb מכסה את מערכת ההפעלה, את ה-daemon של Docker ואת מרווח הביטחון ששומר על השרת מגיב תחת עומס. מה שנותר הוא container_mb, וזהו המספר היחיד שעומד לרשותכם לשימוש. הרזרבה גדלה בהתאם לתוכנית, מ-768 MB בשרת הקטן ביותר ועד 1536 MB בגדול ביותר, כיוון ששרת גדול יותר מריץ יותר containers, כותב יותר לוגים וזקוק ליותר page cache.
תוכנית של 2 GB משאירה 1280 MB עבור ה-containers. אם תקצו 512 MB מתוכם עבור PostgreSQL ו-128 MB עבור Traefik, מחצית מהזיכרון כבר נוצלה. היתרה מספיקה לשני יישומים קטנים של כ-256 MB כל אחד. זהו שרת ממשי ושימושי. אין בו מקום ל-Nextcloud וגם ל-search cluster מעליו.
תוכנית של 4 GB משאירה 3072 MB, שזהו המקום שבו מסד נתונים, reverse proxy, שלושה יישומים ו-container לניטור נכנסים כולם בבת אחת. זהו הגודל הקטן ביותר שכדאי להשתמש בו עבור כל דבר שחשוב לכם, כיוון שהזיכרון הפנוי הוא מה שבולם השפעות של פריסה (deploy) לא מוצלחת.
תוכנית של 8 GB משאירה 6656 MB מתוך ה-8192 MB שלה, והמגבלה עוברת בדרך כלל מהזיכרון ל-CPU או לתפוקת הדיסק. אם החישוב מראה שה-stack שלכם לא נכנס, רכשו תוכנית גדולה יותר במקום לנסות לבצע אופטימיזציות מורכבות: מה העלות האמיתית של VPS מפרט מה הערך של גיגה-בייטים נוספים בחודש.
שני כללים שומרים על החישובים מדויקים. הגדירו מגבלת זיכרון לכל שירות, כך שתהליך שיוצא משליטה לא יפיל את השרת כולו. והשאירו חלק מהתקציב לא מנוצל, כיוון ש-docker compose build ו-pg_dump זקוקים שניהם לזיכרון ברגעים הכי פחות מתאימים. מגבלות זיכרון ב-Docker Compose מפרט את התחביר ואת המלכודות הנפוצות.
מדוע המכולה שלי מסתיימת עם קוד 137?
הסיבה היא שהליבה (kernel) סגרה אותה. הקוד 137 הוא 128 ועוד 9, כאשר אות 9 הוא SIGKILL. המכולה דרשה יותר זיכרון ממה שהוקצה לה, ומנגנון ה-out of memory (OOM) killer סיים את פעולתה.
CONTAINER ID IMAGE STATUS
9f2c1a4d7b3e postgres:16 Exited (137) 4 minutes agoאמתו את הסיבה במקום לנחש:
docker inspect my-db | grep -i oomkilled
sudo journalctl -k | grep -i "out of memory""OOMKilled": true מציין שהמכולה הגיעה למגבלת ה-cgroup שלה, ולוג הליבה מציין את התהליך שנבחר:
Memory cgroup out of memory: Killed process 2417 (postgres) total-vm:1284416kBזהו המקרה העדיף, כיוון שהנזק נעצר במכולה אחת בלבד. המקרה הגרוע הוא מכולה ללא כל הגבלה. ללא הגבלה, תקרת הזיכרון שלה היא המכונה כולה, כך שדליפת זיכרון בשירות אחד מרעיבה את המארח, והליבה בוחרת קורבן לפי גודל התהליך בכל המערכת. שורת הלוג מאבדת את הקידומת Memory cgroup ומופיעה כ-Out of memory: Killed process 2417 (postgres). התהליך שנבחר הוא לעיתים קרובות מסד הנתונים שלכם, בעוד המכולה שדלפה ממשיכה לרוץ. זו הסיבה שהגבלה על כל שירות חשובה יותר מהערך המדויק של הגבלה בודדת.
שימוש ב-swap משנה את התזמון, לא את החישוב. רוב תמונות ה-VPS מגיעות ללא swap. בדקו זאת עם swapon --show, שאינו מדפיס דבר אם אין swap. קובץ swap נותן לליבה מקום לאחסן דפים לא פעילים, מה שקונה לכם דקות יקרות להבחין בבעיה.
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -hfree -h אמור כעת להציג ערך שאינו אפס בשורת ה-Swap. ה-swap אינו מוסיף RAM. מכונה תחת עומס זיכרון מתמיד תהיה איטית עד כדי כך שלא תוכלו להתחבר ב-SSH כדי לתקן אותה, לכן התייחסו ל-swap כאל חוצץ התרעה ותקנו את הקצאת המשאבים.
מדוע UFW לא חוסם את הפורט שפרסמתי ב-Docker?
התעבורה לעולם אינה מגיעה לשרשרת (chain) ש-UFW מגן עליה. כאשר אתם מפרסמים פורט באמצעות -p 5432:5432 או הגדרת ports: ב-compose, ה-daemon כותב חוק DNAT (תרגום כתובת רשת יעד) לתוך טבלת ה-nat וחוק accept לתוך שרשרת ה-DOCKER הייעודית שלו. חבילה המיועדת למכולה מועברת ישירות לאותה מכולה במקום להימסר למארח (host), לכן היא מטופלת בנתיב ה-FORWARD ולעולם אינה עוברת דרך חוקי ה-INPUT ש-UFW כותב.
ניתן לצפות בתהליך זה בשרת:
sudo ufw status | grep 5432
sudo iptables -t nat -L DOCKER -n | grep 5432UFW עשוי להציג 5432 DENY IN Anywhere בזמן שטבלת ה-nat מחזיקה חוק DNAT tcp ... to:172.18.0.2:5432 עבור אותו פורט. ממכונה אחרת, nc -vz your.server.ip 5432 עדיין יתחבר. מסד הנתונים חשוף לאינטרנט הציבורי למרות שה-firewall מדווח אחרת.
הפתרון הוא לפרסם פחות. מכולות באותו פרויקט compose חולקות רשת ומגיעות זו לזו לפי שם השירות, לכן מסד נתונים שמשרת רק את היישום שלצידו אינו זקוק כלל להגדרת ports:. כאשר אתם בכל זאת זקוקים לגישה מקומית, קשרו את הפרסום ל-loopback:
services:
db:
image: postgres:16
ports:
- "127.0.0.1:5432:5432"לאחר docker compose up -d, הגישה ב-nc -vz your.server.ip 5432 מבחוץ תיכשל, אך psql -h 127.0.0.1 -p 5432 בתוך השרת עדיין יעבוד. ב-stack קטן ותקין, רק ה-reverse proxy מפרסם פורטים, בדרך כלל 80 ו-443. המאמר מדוע פורטים שפורסמו ב-Docker עוקפים את UFW מסביר על שרשרת ה-DOCKER-USER למקרים שבהם עליכם לפרסם פורט ועדיין לסנן אותו, והמאמר יסודות ה-firewall של UFW מכסה את חוקי המארח הבסיסיים.
מדוע המכולות שלי נעלמות לאחר אתחול (reboot)?
מכיוון ששום דבר לא הורה להן לחזור. מכולה נוצרת עם מדיניות restart כברירת מחדל של no אלא אם הגדרת אחרת, לכן אתחול משאיר אותן במצב עצור וה-daemon לא מתייחס אליהן. אתחולים אינם אירוע נדיר ב-VPS: עדכוני ליבה מ-unattended upgrades, תחזוקת ספק, ורצף ה-OOM שתואר לעיל, כולם מסתיימים באתחול.
שני תנאים חייבים להתקיים. ה-daemon חייב לעלות בזמן ה-boot:
systemctl is-enabled dockerפקודה זו מדפיסה enabled בהתקנת Ubuntu סטנדרטית. לאחר מכן, כל שירות זקוק למדיניות:
services:
app:
image: ghcr.io/example/app:1.4
restart: unless-stoppedהערך unless-stopped מחזיר את המכולה לפעולה לאחר אתחול ומכבד מכולה שעצרת באופן יזום. הערך always יפעיל מחדש גם מכולות שעצרת בכוונה בכל פעם שה-daemon עולה, מה שעלול להפתיע במהלך ניפוי שגיאות (debugging). עריכת הקובץ לבדה אינה מספיקה, כיוון שמדיניות ה-restart נקבעת בעת יצירת המכולה. הרצו את docker compose up -d כדי ליצור אותה מחדש, ולאחר מכן בדקו את הערך הפעיל:
docker inspect my-app | grep -A3 RestartPolicyלאחר מכן בצעו אתחול יזום לשרת והריצו את docker compose ps בתיקיית הפרויקט. Stack ששורד אתחול מתוכנן ישרוד גם אתחול בלתי צפוי. אם ה-stack שלכם דורש הבטחת סדר פעולות או הרצה חד-פעמית בזמן ה-boot, יחידת systemd היא הכלי המתאים יותר: הפעלת Docker Compose בזמן ה-boot מכיל את קובץ ה-unit. כדי לדעת אם מכולה שחזרה אכן מספקת שירות, הוסיפו בדיקות תקינות (healthchecks) ב-Compose.
מדוע הדיסק בשרת ה-VPS שלי מלא?
מכיוון ש-Docker שומר את כל המידע עד שתורה לו אחרת. כל תגית של image שמשכת, כל container שעצרת, כל volume אנונימי שנותר מאחור לאחר יצירה מחדש, וכל שכבה של build cache נשארים על הדיסק. במערכת קבצים ראשית של 40 GB או 80 GB, גודל סטנדרטי בתוכניות אירוח אלו, הדבר מוביל להשבתת השירות בתוך חודשים ספורים.
דיסק מלא אינו נראה כמו קריסה. אתם תקבלו no space left on device מ-container, מ-apt, מ-journald ומ-docker pull באותה שעה. PostgreSQL יפסיק לקבל כתיבות. השרת עדיין פעיל, מה שהופך את הבעיה לקשה יותר לזיהוי מאשר לולאת אתחול.
בדקו לפני שאתם מוחקים:
docker system df
df -h /הפקודה docker system df מחלקת את סך השימוש ל-images, ל-containers, ל-volumes מקומיים ול-build cache, עם עמודת RECLAIMABLE לצד כל אחד. בשרת שבנה בעצמו את ה-images שלו, ה-build cache הוא בדרך כלל השורה הגדולה ביותר.
docker image prune -a
docker builder prune
docker system dfהפקודה docker image prune -a מסירה כל image ששום container לא משתמש בו. docker builder prune מנקה את ה-build cache. שתי הפעולות בטוחות בזמן שהשירותים רצים, כיוון שכל מה שנמצא בשימוש נדלג עליו. מה שאינו בטוח הוא docker system prune --volumes, שמוחק כל volume ששום container לא מפנה אליו כרגע. stack שעצרתם לסוף השבוע נראה בדיוק כך, וה-volume של מסד הנתונים שלו יימחק יחד איתו. קראו על bind mounts versus named volumes לפני שתקלידו את הדגל הזה, ובצעו גיבוי תחילה.
לוגים של containers הם גורם צמיחה שקט יותר. ל-driver ברירת המחדל json-file אין הגבלת גודל, לכן container פטפטן עלול לכתוב גיגה-בייטים לתוך /var/lib/docker/containers. הגבילו זאת עבור כל container ב-/etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}החילו זאת עם sudo systemctl restart docker, פעולה שמפעילה מחדש את ה-containers שלכם, לכן בחרו את העיתוי המתאים. ההגבלה חלה על containers שנוצרו לאחר השינוי, לכן צרו מחדש את אלו שרצים עם docker compose up -d --force-recreate ואשרו:
docker inspect my-app | grep -A5 LogConfig
sudo du -sh /var/lib/docker/containersפלט ה-inspect אמור להראות ש-max-size מוגדר. אם הוא ריק, ה-container נוצר לפני השינוי והוא עדיין כותב ללא הגבלה.
הרגלים לשמירה על תקינות שרת Docker קטן
אין צורך בלוח בקרה או בכלי מורכב כדי ליישם זאת.
- הריצו את
docker system dfואתdf -h /בראשון לכל חודש. שתי פקודות, שלושים שניות, ואתם תראו את המגמה הרבה לפני שהיא הופכת להשבתה. - הגדירו מגבלת זיכרון לכל שירות, גם לאלו שנראים לכם קטנים. המגבלה הופכת קריסה של כל השרת להפעלה מחדש של מכולה בודדת בלבד.
- נטרו את השרת ממקור חיצוני, כך שתקבלו התראה על עומס זיכרון או דיסק לפני שהליבה (kernel) תתערב. Uptime Kuma רץ בתוך מכולה וצורך במצב המתנה כ-95 MB.
- בצעו גיבוי ל-volumes, לא למכולות. המכולה היא זמנית, ה-volume אינו כזה. המדריך restic backups on a VPS מכסה תזמון גיבויים ובדיקת שחזור.
- קבעו גרסאות (pin) לתגיות ה-image בקובץ ה-compose ועדכנו אותן ביום שתבחרו. עם
latest, הגרסה שתקבלו ב-docker compose pullהבא תהיה זו ששוחררה באותו בוקר.
שרת VPS קטן שמריץ Docker יישאר תקין במשך שנים אם ארבעה נתונים יישארו בטווח הרצוי: תקציב הזיכרון, רשימת הפורטים החשופים, מדיניות ה-restart של כל שירות, ושטח הדיסק הפנוי. כל השאר הוא ה-Docker הרגיל שאתם כבר מריצים.
FAQ
כמה זיכרון RAM דרוש להרצת Docker על שרת VPS?
Docker עצמו צורך מעט משאבים. ה-daemon וה-containerd יחד תופסים כ-100 MB, ושאר הדרישות תלויות במכולות שלכם. הקצו תחילה את חלקו של המארח: 768 MB מתוך 2048 MB עבור מערכת ההפעלה, ה-daemon ומרווח ביטחון, מה שמותיר 1280 MB לשימוש. מסד נתונים ב-512 MB, reverse proxy ב-128 MB ושני יישומים קטנים יכנסו למסגרת זו. מדדו את צריכת הזיכרון של ה-stack שלכם באמצעות docker stats --no-stream במקום להסתמך על נתונים מפורסמים.
האם ניתן להריץ Docker על שרת VPS עם 1 GB RAM?
כן, עבור מכולה אחת או שתיים קלות, אך הוסיפו קובץ swap לפני שתתחילו. בערך מחצית מהזיכרון בשרת של 1 GB נצרך על ידי מערכת ההפעלה וה-Docker daemon, מה שמותיר מקום ליישום קטן ול-reverse proxy, אך לא למסד נתונים תחת עומס אמיתי. בניית images על שרת בגודל כזה תיכשל או תגרום לקריסת תהליכים אחרים, לכן בנו אותם במקום אחר ומשכו את ה-image המוכן.
האם UFW מגן על מכולת Docker?
לא עבור פורטים שחשפתם. Docker כותב חוקי DNAT ו-forward משלו, לכן חבילת מידע המיועדת לפורט של מכולה חשופה מועברת ישירות למכולה במקום להימסר למארח, וחוקי ה-INPUT שמנהל UFW כלל לא רואים אותה. ufw deny 5432 יכול להיות פעיל בזמן שהפורט מגיב לבקשות מהאינטרנט. חשפו פורטים ל-loopback בלבד באמצעות 127.0.0.1:5432:5432, השאירו שירותים פנימיים ללא חשיפה, או בצעו סינון בשרשרת ה-DOCKER-USER.
האם המכולות שלי יופעלו מחדש לאחר אתחול ה-VPS?
רק אם הן נוצרו עם מדיניות הפעלה מחדש (restart policy). הגדירו restart: unless-stopped לכל שירות, הריצו docker compose up -d כדי שהמכולות ייווצרו מחדש עם הגדרה זו, וודאו ש-systemctl is-enabled docker מדפיס enabled. לאחר מכן בצעו אתחול יזום ובדקו את docker compose ps. מדיניות הפעלה מחדש שלא בדקתם אינה מדיניות הפעלה מחדש.
באיזו תדירות כדאי לנקות (prune) images של Docker?
פעם בחודש זה מספיק לרוב השרתים הקטנים, או בכל פעם ש-docker system df מדווח על שטח שניתן לפינוי שחשוב לכם. docker image prune -a ו-docker builder prune בטוחים לשימוש בזמן שהשירותים רצים, כיוון ש-images ו-cache שנמצאים בשימוש אינם נמחקים. הימנעו מ-docker system prune --volumes אלא אם אתם יודעים בדיוק אילו volumes אינם בשימוש, כיוון שהוא מוחק את הנתונים של כל stack שאינו פעיל באותו רגע.