502 Bad Gateway אומר שה-proxy שעומד לפני האפליקציה שלכם (Caddy, nginx, Traefik, או ה-proxy ש-Coolify מריץ) העביר את הבקשה ולא קיבל תשובה תקינה. ייתכן שהאפליקציה עצורה, מאזינה על כתובת או פורט לא נכונים, נמצאת ברשת Docker אחרת, או איטית מדי; הפעלה מחדש של דברים באקראי מסתירה מה מהם. האשף הזה קורא קודם את השגיאה של ה-proxy עצמו, ואז בודק את המסלול לקונטיינר חוליה אחר חוליה.
ענו על שאלה אחת בכל פעם. כל שלב מציג פקודה להרצה בשרת שלכם ומה הפלט שלה אומר. הבדיקות רק קוראות: הן מסתכלות על הרשת של הקונטיינר מהשרת עם nsenter, בלי להיכנס לקונטיינר. כל פקודה שמשנה משהו (יצירה מחדש, טעינה מחדש) מסומנת לפני שמריצים אותה. העמוד לא מריץ שום דבר ולא שולח דבר לשום מקום; התשובות נשמרות בקישור של העמוד, אף פעם לא הפלט שלכם.
מוצאים את הסיבה, צעד אחר צעד
איזה reverse proxy עונה ב-502?
502 Bad Gateway הוא לא שגיאה של האפליקציה שלכם: זה ה-proxy שאומר שהוא ניסה להעביר את הבקשה ולא קיבל תשובה תקינה מהאפליקציה שמאחוריו. העמוד עצמו (העיצוב, הניסוח) מלמד איזה proxy ענה, וכל proxy כותב את הסיבה בלוג שלו.
Coolify מריץ קונטיינר proxy משלו, coolify-proxy: Traefik כברירת מחדל, או Caddy. אם אתם מריצים proxy אחר, בחרו את זה ששפת התצורה שלו היא זו שאתם משתמשים בה.
כל השלבים כטקסט
אותו עץ כמו באשף שלמעלה, כל הענפים בבת אחת, לקריאה או להדפסה.
הצגת כל השאלות, הבדיקות והפתרונות של האשף
איזה reverse proxy עונה ב-502?
502 Bad Gateway הוא לא שגיאה של האפליקציה שלכם: זה ה-proxy שאומר שהוא ניסה להעביר את הבקשה ולא קיבל תשובה תקינה מהאפליקציה שמאחוריו. העמוד עצמו (העיצוב, הניסוח) מלמד איזה proxy ענה, וכל proxy כותב את הסיבה בלוג שלו.
Coolify מריץ קונטיינר proxy משלו, coolify-proxy: Traefik כברירת מחדל, או Caddy. אם אתם מריצים proxy אחר, בחרו את זה ששפת התצורה שלו היא זו שאתם משתמשים בה.
תשובות
- Caddy: עוברים אל “מה כתוב בלוג של Caddy?”
- nginx: עוברים אל “מה כתוב בלוג השגיאות של nginx?”
- Traefik: עוברים אל “מה כתוב בלוג של Traefik?”
- Coolify (ה-proxy שלו): עוברים אל “מה כתוב בלוג של ה-proxy של Coolify?”
מה כתוב בלוג של Caddy?
Caddy רושם כל ניסיון העברה שנכשל כשגיאה עם הכתובת שאליה ניסה להתחבר: dial tcp 172.18.0.5:3000: connect: connection refused, dial tcp: lookup app on 127.0.0.11:53: no such host, או context deadline exceeded. החליפו את <proxy> בשם הקונטיינר של Caddy, ובקשו את האתר פעם אחת לפני שאתם מריצים את הפקודה, כדי שתהיה שורה טרייה.
אם Caddy רץ על השרת עצמו (החבילה והשירות של systemd) ולא בקונטיינר, קראו במקום זה את journalctl -u caddy --since "15 min ago".
docker logs --since 15m <proxy> 2>&1 | grep -iE 'dial|reverse_proxy' | tail -n 20תשובות
- הוא לא הצליח להתחבר: connection refused, no such host, timeout בהתחברות, או שאין שורת שגיאה בכלל: עוברים אל “האם הקונטיינר של האפליקציה רץ?”
- הוא התחבר, ואז נגמר לו הזמן בהמתנה לתשובה: עוברים אל “האפליקציה עונה לאט יותר ממה שה-proxy מחכה”
- האפליקציה סגרה את החיבור: EOF, connection reset, או upstream prematurely closed: עוברים אל “האפליקציה מנתקת חיבורים: היא קורסת או נהרגת באמצע בקשה”
מה כתוב בלוג השגיאות של nginx?
nginx כותב את הסיבה ל-502 בלוג השגיאות שלו, עם המילה upstream: connect() failed (111: Connection refused) while connecting to upstream, host not found in upstream, upstream prematurely closed connection while reading response header from upstream, או upstream sent too big header while reading response header from upstream. ה-image הרשמי של nginx שולח את לוג השגיאות לפלט של הקונטיינר.
אם nginx רץ על השרת עצמו ולא בקונטיינר, קראו במקום זה את sudo tail -n 50 /var/log/nginx/error.log.
docker logs --since 15m <proxy> 2>&1 | grep -i upstream | tail -n 20תשובות
- הוא לא הצליח להתחבר: connection refused, no such host, timeout בהתחברות, או שאין שורת שגיאה בכלל: עוברים אל “האם הקונטיינר של האפליקציה רץ?”
- הוא התחבר, ואז נגמר לו הזמן בהמתנה לתשובה: עוברים אל “האפליקציה עונה לאט יותר ממה שה-proxy מחכה”
- האפליקציה סגרה את החיבור: EOF, connection reset, או upstream prematurely closed: עוברים אל “האפליקציה מנתקת חיבורים: היא קורסת או נהרגת באמצע בקשה”
- upstream sent too big header: עוברים אל “הכותרות של התשובה גדולות מה-buffer של nginx”
מה כתוב בלוג של Traefik?
Traefik עונה 502 כשהוא לא מגיע לשרת של service, ומציין את הסיבה בלוג, למשל dial tcp 172.18.0.5:3000: connect: connection refused או EOF. ברמת הלוג ברירת המחדל חלק מהשורות האלה לא נכתבות; אם לא רואים כלום, הסתכלו ב-access log או העלו את הרמה ל-DEBUG לזמן קצר.
404 מ-Traefik היא בעיה אחרת: אף router לא התאים לדומיין. האשף הזה עוסק ב-502.
docker logs --since 15m <proxy> 2>&1 | grep -iE '502|dial|eof' | tail -n 20תשובות
- הוא לא הצליח להתחבר: connection refused, no such host, timeout בהתחברות, או שאין שורת שגיאה בכלל: עוברים אל “האם הקונטיינר של האפליקציה רץ?”
- הוא התחבר, ואז נגמר לו הזמן בהמתנה לתשובה: עוברים אל “האפליקציה עונה לאט יותר ממה שה-proxy מחכה”
- האפליקציה סגרה את החיבור: EOF, connection reset, או upstream prematurely closed: עוברים אל “האפליקציה מנתקת חיבורים: היא קורסת או נהרגת באמצע בקשה”
מה כתוב בלוג של ה-proxy של Coolify?
Coolify מריץ קונטיינר proxy אחד בכל שרת, coolify-proxy, ומעביר כל דומיין דרך רשת Docker לקונטיינר של המשאב. הלוג שלו נמצא גם בממשק, תחת Servers ← השרת שלכם ← Proxy ← Logs. Traefik כותב שגיאות כמו dial tcp …: connect: connection refused; Caddy כותב שורות dial tcp … באותו אופן.
בשלבים הבאים, <proxy> הוא coolify-proxy, ו-<container> הוא הקונטיינר של האפליקציה שלכם: Coolify מוסיף לשם שלו סיומת שנוצרת אוטומטית, אז העתיקו אותו מ-docker ps.
docker logs --since 15m coolify-proxy 2>&1 | tail -n 50תשובות
- הוא לא הצליח להתחבר: connection refused, no such host, timeout בהתחברות, או שאין שורת שגיאה בכלל: עוברים אל “האם הקונטיינר של האפליקציה רץ?”
- הוא התחבר, ואז נגמר לו הזמן בהמתנה לתשובה: עוברים אל “האפליקציה עונה לאט יותר ממה שה-proxy מחכה”
- האפליקציה סגרה את החיבור: EOF, connection reset, או upstream prematurely closed: עוברים אל “האפליקציה מנתקת חיבורים: היא קורסת או נהרגת באמצע בקשה”
האם הקונטיינר של האפליקציה רץ?
לקונטיינר שנעצר אין כתובת ואין שם ברשת, ולכן ה-proxy מקבל connection refused או no such host. הסתכלו על ה-STATUS של הקונטיינר של האפליקציה: Up 3 hours, Up 2 minutes (healthy), Up 10 seconds (health: starting), Up (unhealthy), Restarting (1) 5 seconds ago או Exited (137).
docker ps -a --format 'table {{.Names}}\t{{.Status}}'תשובות
- Up, healthy או בלי healthcheck: עוברים אל “על איזו כתובת ואיזה פורט האפליקציה מאזינה בתוך הקונטיינר?”
- Up, אבל health: starting או unhealthy: עוברים אל “האפליקציה עדיין עולה, או נכשלת ב-healthcheck”
- Restarting, Exited, או לא מופיע ברשימה: עוברים אל “הקונטיינר של האפליקציה לא רץ”
על איזו כתובת ואיזה פורט האפליקציה מאזינה בתוך הקונטיינר?
לכל קונטיינר יש network namespace משלו, עם 127.0.0.1 משלו. אפליקציה שמאזינה שם על 127.0.0.1 נגישה רק מתוך אותו קונטיינר, אף פעם לא מה-proxy, ולכן ה-proxy מקבל connection refused. היא צריכה להאזין על כל הכתובות: 0.0.0.0, * או [::].
הפקודה הזו קוראת את ה-sockets שמאזינים ב-namespace של הקונטיינר עם ה-ss של השרת עצמו, בלי להיכנס לקונטיינר ובלי לשנות אותו (nsenter -n מחליף רק את ה-network namespace). מצאו את הפורט שה-proxy שלכם פונה אליו: ב-Caddy reverse_proxy app:3000, ב-nginx proxy_pass http://app:3000, ב-Traefik ה-label traefik.http.services.<name>.loadbalancer.server.port, וב-Coolify השדה Ports Exposes.
sudo nsenter -t "$(docker inspect -f '{{.State.Pid}}' <container>)" -n ss -ltnpתשובות
- הפורט של ה-proxy, על 0.0.0.0, * או [::]: עוברים אל “ה-proxy רץ בקונטיינר או על השרת עצמו?”
- הפורט של ה-proxy, אבל רק על 127.0.0.1 (או [::1]): עוברים אל “האם האפליקציה היא שרת Next.js (output: standalone)?”
- היא מאזינה, אבל על פורט אחר מזה שה-proxy פונה אליו: עוברים אל “ה-proxy פונה לפורט אחר מזה שהאפליקציה מאזינה עליו”
- שום דבר לא מאזין (רשימה ריקה): עוברים אל “שום דבר לא מאזין בקונטיינר”
האם האפליקציה היא שרת Next.js (output: standalone)?
התיקון זהה לכל אפליקציה, האזנה על 0.0.0.0, אבל להגדרה יש שם אחר בכל framework. Next.js במצב standalone לוקח אותה ממשתנה הסביבה HOSTNAME, ולמשתנה הזה יש מלכודת משלו מאחורי proxy.
תשובות
- כן, Next.js standalone: עוברים אל “Next.js standalone מאזין על 127.0.0.1: מגדירים HOSTNAME=0.0.0.0”
- לא, אפליקציה אחרת: עוברים אל “האפליקציה מאזינה על 127.0.0.1 בתוך הקונטיינר”
ה-proxy רץ בקונטיינר או על השרת עצמו?
האפליקציה מאזינה כמו שצריך, אז השאלה היא איך ה-proxy מגיע אליה. proxy בקונטיינר (זה של Coolify תמיד כזה) מגיע לאפליקציה דרך רשת Docker משותפת, לפי שם ולפי הפורט של הקונטיינר עצמו. proxy שמותקן על השרת (חבילת nginx או Caddy) לא יכול לפענח שמות של קונטיינרים; הוא מגיע לאפליקציה דרך פורט שפורסם על השרת.
תשובות
- בקונטיינר (כולל Coolify): עוברים אל “האם ה-proxy והאפליקציה חולקים רשת Docker?”
- על השרת עצמו: עוברים אל “האם הפורט של האפליקציה פורסם על השרת?”
האם ה-proxy והאפליקציה חולקים רשת Docker?
קונטיינרים מגיעים זה לזה רק דרך רשת ששניהם מחוברים אליה. Compose מציב כל פרויקט ברשת משלו (<project>_default), ולכן proxy מפרויקט Compose אחד לא רואה אפליקציה מפרויקט אחר, אלא אם שניהם מצטרפים לרשת משותפת. הפקודה הזו מדפיסה את הרשתות של שני הקונטיינרים, שורה לכל אחד.
docker inspect -f '{{.Name}}: {{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}' <proxy> <container>תשובות
- לפחות רשת אחת מופיעה בשתי השורות: עוברים אל “האם ה-proxy משתמש בשם שהרשת מכירה?”
- אף רשת לא מופיעה בשתי השורות: עוברים אל “ה-proxy והאפליקציה ברשתות שונות”
האם ה-proxy משתמש בשם שהרשת מכירה?
ברשת שהוגדרה על ידי המשתמש, ה-DNS של Docker עונה לשם של הקונטיינר ולכינויים (aliases) שלו; Compose מוסיף את שם השירות ככינוי. הפקודה הזו מדפיסה, לכל רשת, את הכתובת של הקונטיינר ואת הכינויים שלו. השוו אותם לשם השרת בתצורה של ה-proxy (app ב-reverse_proxy app:3000 או ב-proxy_pass http://app:3000). Traefik ו-Coolify משתמשים ישירות בכתובת של הקונטיינר; אצלם בדקו שהכתובת נמצאת ברשת המשותפת.
רשמו את הכתובת ברשת המשותפת: השלב הבא משתמש בה בתור <ip>.
docker inspect -f '{{range $k, $v := .NetworkSettings.Networks}}{{$k}}: {{$v.IPAddress}} {{$v.Aliases}}{{println}}{{end}}' <container>תשובות
- שם השרת של ה-proxy הוא השם של הקונטיינר או אחד הכינויים שלו ברשת המשותפת: עוברים אל “האם האפליקציה עונה מהמקום שבו ה-proxy עומד?”
- ה-proxy משתמש בשם אחר, או בכתובת IP קבועה: עוברים אל “ה-proxy משתמש בשם או בכתובת שהרשת לא מכירה”
האם האפליקציה עונה מהמקום שבו ה-proxy עומד?
הפקודה שולחת בקשת HTTP אחת לאפליקציה מה-network namespace של ה-proxy, עם ה-curl של השרת עצמו, לכתובת שרשמתם ולפורט של הקונטיינר. היא מדפיסה את קוד התגובה וכמה זמן לקחה התשובה, ומוותרת אחרי 90 שניות. כל קוד תגובה, גם 404 או 401, אומר שהמסלול עובד: האפליקציה ענתה.
sudo nsenter -t "$(docker inspect -f '{{.State.Pid}}' <proxy>)" -n curl -sS --max-time 90 -o /dev/null -w '%{http_code} %{time_total}s\n' http://<ip>:<port>/תשובות
- קוד תגובה, תוך כמה שניות: עוברים אל “מתי מופיע ה-502?”
- קוד תגובה אחרי זמן רב, או curl: (28) timeout: עוברים אל “האפליקציה עונה לאט יותר ממה שה-proxy מחכה”
- Empty reply from server, או 400 על HTTP רגיל שנשלח לפורט HTTPS: עוברים אל “האפליקציה מדברת HTTPS וה-proxy מדבר אליה HTTP”
- Connection refused, או no route to host: עוברים אל “עדיין אין התאמה: מה לאסוף”
האם הפורט של האפליקציה פורסם על השרת?
proxy על השרת מגיע לקונטיינר דרך פורט מפורסם: ports: ["127.0.0.1:3000:3000"] ב-Compose. פרסום על ה-127.0.0.1 של השרת נכון כאן, בניגוד להאזנה על 127.0.0.1 בתוך הקונטיינר: הוא מרחיק את הפורט מהאינטרנט, וה-proxy של השרת עדיין מגיע אליו. הפקודה מציגה את הפורטים המפורסמים של הקונטיינר בצורה 3000/tcp -> 127.0.0.1:3000.
docker port <container>תשובות
- הפורט של ה-proxy מופיע, על 127.0.0.1 או 0.0.0.0: עוברים אל “האם האפליקציה עונה על הפורט של השרת?”
- שום דבר לא מופיע, או פורט אחר: עוברים אל “ל-proxy שעל השרת אין פורט מפורסם להגיע אליו”
האם האפליקציה עונה על הפורט של השרת?
זו הבקשה שה-proxy שלכם שולח: מהשרת, לפורט המפורסם. היא מדפיסה את קוד התגובה וכמה זמן לקחה התשובה, ומוותרת אחרי 90 שניות. כל קוד תגובה אומר שהאפליקציה ענתה.
curl -sS --max-time 90 -o /dev/null -w '%{http_code} %{time_total}s\n' http://127.0.0.1:<port>/תשובות
- קוד תגובה, תוך כמה שניות: עוברים אל “מתי מופיע ה-502?”
- קוד תגובה אחרי זמן רב, או curl: (28) timeout: עוברים אל “האפליקציה עונה לאט יותר ממה שה-proxy מחכה”
- Empty reply from server, או 400 על HTTP רגיל שנשלח לפורט HTTPS: עוברים אל “האפליקציה מדברת HTTPS וה-proxy מדבר אליה HTTP”
- Connection refused או reset: עוברים אל “עדיין אין התאמה: מה לאסוף”
מתי מופיע ה-502?
האפליקציה עונה במסלול שה-proxy משתמש בו, אז הבקשה נשברת ב-proxy עצמו או רק בחלק מהבקשות. מתי מופיע ה-502, ובאילו בקשות, מבדיל בין הסיבות האלה.
תשובות
- בכל בקשה, למרות שהאפליקציה ענתה לבדיקה: עוברים אל “ה-proxy שולח בקשות לכתובת אחרת מזו שבדקתם”
- רק בתכונות חיות: WebSocket, עדכונים חיים, טרמינל בדפדפן: עוברים אל “ה-proxy לא מעביר את ה-upgrade של WebSocket”
- רק בחלק מהעמודים, לרוב אחרי התחברות או עם cookies גדולים: עוברים אל “הכותרות של התשובה גדולות מה-buffer של nginx”
- לכמה שניות או דקות בכל פריסה: עוברים אל “הקונטיינר הישן נעצר לפני שהחדש מוכן”
- באקראי, יותר בעומס: עוברים אל “האפליקציה מנתקת חיבורים: היא קורסת או נהרגת באמצע בקשה”
הקונטיינר של האפליקציה לא רץ
אין ל-proxy לאן להתחבר: לקונטיינר שנעצר אין כתובת ברשת. קונטיינר שמופעל מחדש כל הזמן מציג את השגיאה האמיתית בלוג שלו; ה-502 של ה-proxy הוא רק התוצאה.
מה לעשות
- קראו למה הוא נעצר: השורות האחרונות לפני היציאה.
docker logs --tail 50 <container> - אם הוא בלולאת הפעלה מחדש, עקבו אחרי קוד היציאה שלו באשף לולאת ההפעלה מחדש באתר הזה לפני שאתם מפעילים אותו שוב.
- אחרי שהסיבה תוקנה, הפעילו את השירות שוב.
docker compose up -d <service> - בקשו את האתר שוב דרך ה-proxy; קוד התגובה הוא המספר הראשון.
curl -sS -o /dev/null -w '%{http_code}\n' https://<domain>/
האפליקציה עדיין עולה, או נכשלת ב-healthcheck
התהליך רץ, אבל עוד לא עונה, או עונה בשגיאות. חלק מה-proxies מפסיקים לשלוח תעבורה לקונטיינר שאינו healthy, והפורט של האפליקציה עשוי להיפתח רק כשהעלייה שלה מסתיימת (migrations, חימום cache, טעינת מודל).
מה לעשות
- קראו את תוצאות ה-healthcheck האחרונות: הפלט וקוד היציאה של הפקודה מראים מה נכשל.
docker inspect -f '{{json .State.Health}}' <container> - אם האפליקציה רק איטית בעלייה, תנו לה זמן: הגדילו את
start_period(ואתstart_interval) של ה-healthcheck במקום אתretries, כדי שעלייה איטית לא תיספר ככישלון. - אם הבדיקה עצמה שגויה (נתיב שלא קיים, כלי שחסר ב-image כמו
curl), תקנו את ה-testכך שיבדוק את הפורט שה-proxy משתמש בו. - צרו את הקונטיינר מחדש כדי שהשינוי ייכנס לתוקף (Compose מחליף את הקונטיינר; הנתונים ב-volumes שלו נשארים).
docker compose up -d --force-recreate <service>
האפליקציה מאזינה על 127.0.0.1 בתוך הקונטיינר
הרבה frameworks מאזינים כברירת מחדל על 127.0.0.1 במצב פיתוח. בתוך קונטיינר הכתובת הזו שייכת לקונטיינר בלבד, ולכן ה-proxy, שמגיע מקונטיינר אחר או מהשרת, נדחה. האפליקציה צריכה להאזין על 0.0.0.0 (כל הכתובות); הפורטים המפורסמים והרשת המשותפת הם שקובעים מי מגיע אליה.
מה לעשות
- הגדירו את כתובת ההאזנה בפקודה או בסביבה של האפליקציה: למשל
--host 0.0.0.0(uvicorn, Vite), --bind 0.0.0.0:8000(gunicorn), -b 0.0.0.0(Rails), --host=0.0.0.0(Flask), אוHOST=0.0.0.0כשהאפליקציה קוראת אותו. - צרו את הקונטיינר מחדש כדי שהשינוי ייכנס לתוקף (Compose מחליף את הקונטיינר; הנתונים ב-volumes שלו נשארים).
docker compose up -d --force-recreate <service> - בדקו שהפורט מאזין עכשיו על 0.0.0.0, * או [::].
sudo nsenter -t "$(docker inspect -f '{{.State.Pid}}' <container>)" -n ss -ltnp - בקשו את האתר שוב דרך ה-proxy; קוד התגובה הוא המספר הראשון.
curl -sS -o /dev/null -w '%{http_code}\n' https://<domain>/
Next.js standalone מאזין על 127.0.0.1: מגדירים HOSTNAME=0.0.0.0
ה-server.js של פלט ה-standalone מאזין על הכתובת שב-HOSTNAME, ו-Next.js מתעד את HOSTNAME=0.0.0.0 להאזנה על כל הכתובות. ערך של 127.0.0.1 או localhost משאיר את השרת לא נגיש ל-proxy; ה-HOSTNAME ש-Docker עצמו מגדיר, המזהה של הקונטיינר, קושר אותו לכתובת אחת ברשת אחת בלבד.
האתר הזה רץ באותה דרך, שרת Next.js standalone מאחורי ה-proxy של Coolify, ותיעד מלכודת נוספת: כשהשרת היה קשור לכתובת 127.0.0.1 המילולית, Next.js התייחס ל-rewrite הפנימי של ה-proxy כבקשה cross-origin ושלח אותה ללולאת הפניות. HOSTNAME=0.0.0.0 תיקן את שתי הבעיות.
מה לעשות
- הגדירו
HOSTNAME=0.0.0.0(ואתPORT, אם הוא לא 3000) ב-image (ENV HOSTNAME=0.0.0.0ב-Dockerfile) או בסביבה של השירות; ב-Coolify, כמשתנה סביבה של המשאב. - צרו את הקונטיינר מחדש כדי שהשינוי ייכנס לתוקף (Compose מחליף את הקונטיינר; הנתונים ב-volumes שלו נשארים).
docker compose up -d --force-recreate <service> - בדקו שהשרת מאזין עכשיו על 0.0.0.0.
sudo nsenter -t "$(docker inspect -f '{{.State.Pid}}' <container>)" -n ss -ltnp - פתחו את האתר ועקבו אחרי הפניה: היא צריכה להסתיים בעמוד, לא לקפוץ בין שתי כתובות.
curl -sS -o /dev/null -L --max-redirs 5 -w '%{http_code} %{url_effective}\n' https://<domain>/
שום דבר לא מאזין בקונטיינר
התהליך רץ, אבל לא פתח פורט TCP: הוא עדיין עולה, הוא נכשל אחרי העלייה ונשאר חי, הוא מאזין על Unix socket, או שהקונטיינר מריץ פקודה אחרת מזו שמפעילה את השרת.
מה לעשות
- חפשו בלוג של האפליקציה את השורה שאומרת איפה היא מאזינה, או את השגיאה שאחריה.
docker logs --since 15m <container> 2>&1 | tail -n 50 - בדקו את הפקודה שהקונטיינר מריץ בפועל.
docker inspect -f '{{.Path}} {{.Args}}' <container> - אם האפליקציה מאזינה על Unix socket (PHP-FPM, חלק מהגדרות gunicorn), כוונו את ה-proxy אליו בפרוטוקול המתאים (
fastcgi_passב-nginx, php_fastcgiב-Caddy) או גרמו לה להאזין על פורט TCP.
ה-proxy פונה לפורט אחר מזה שהאפליקציה מאזינה עליו
ה-proxy צריך לפנות לפורט שבתוך הקונטיינר, לא לפורט שפורסם על השרת: עם ports: ["8080:3000"], proxy ברשת המשותפת משתמש ב-3000. Traefik משתמש בפורט הראשון שה-image חושף, אלא אם label אומר אחרת, וזה 502 נפוץ ב-images שחושפים יותר מפורט אחד.
מה לעשות
- Caddy:
reverse_proxy app:3000. nginx: proxy_pass http://app:3000;. Traefik: ה-label traefik.http.services.<name>.loadbalancer.server.port=3000. Coolify: השדה Ports Exposes של המשאב. - ב-Coolify, הסירו גם מיפוי פורט לשרת שאינו נחוץ: ה-proxy מגיע לקונטיינר דרך הרשת, ופורט מפורסם עוקף את הניתוב של ה-proxy.
- Caddy: טענו מחדש את ה-Caddyfile (Caddy בודק אותו קודם, ומשאיר את התצורה הקודמת אם היא לא תקינה).
docker exec <proxy> caddy reload --config /etc/caddy/Caddyfile - nginx: בדקו את התצורה, ואז טענו אותה מחדש בלי לנתק חיבורים פתוחים.
docker exec <proxy> sh -c 'nginx -t && nginx -s reload' - Traefik ו-Coolify קוראים labels מהקונטיינר של האפליקציה: אחרי שינוי label (או הגדרה בממשק של Coolify), צרו מחדש או פרסו מחדש את האפליקציה, לא את ה-proxy.
- בקשו את האתר שוב דרך ה-proxy; קוד התגובה הוא המספר הראשון.
curl -sS -o /dev/null -w '%{http_code}\n' https://<domain>/
ה-proxy והאפליקציה ברשתות שונות
כל פרויקט Compose מקבל רשת ברירת מחדל משלו, ו-Coolify מציב משאבים ברשת של ה-destination שלהם. proxy מגיע רק לקונטיינרים שחולקים איתו אחת מהרשתות שלו; התיקון הרגיל הוא רשת חיצונית אחת שה-proxy וכל אפליקציה מאחוריו מצטרפים אליה.
מה לעשות
- כדי לבדוק עכשיו, חברו את האפליקציה שרצה לרשת של ה-proxy. זה נשאר עד שהקונטיינר נוצר מחדש.
docker network connect <network> <container> - כדי שזה יישאר, הצהירו על הרשת בקובץ ה-Compose של האפליקציה:
networks: { proxy: { external: true, name: <network> } }ברמה העליונה, ו-networks: [default, proxy]בשירות. כש-Traefik מחובר לכמה רשתות, הוסיפו את ה-label traefik.docker.network=<network>כדי שישתמש בה. - ב-Coolify, למשאב Docker Compose שצריך להגיע למשאבים אחרים של ה-destination שלו, הפעילו את Connect to Predefined Network, ובדקו את השם שנוצר לפני שאתם משתמשים בו כשם שרת.
- צרו את הקונטיינר מחדש כדי שהשינוי ייכנס לתוקף (Compose מחליף את הקונטיינר; הנתונים ב-volumes שלו נשארים).
docker compose up -d --force-recreate <service>
ה-proxy משתמש בשם או בכתובת שהרשת לא מכירה
שם השרת של ה-proxy צריך להיות השם של הקונטיינר, שם השירות ב-Compose או כינוי, ברשת ששניהם חולקים; ברשת bridge ברירת המחדל של Docker שמות לא מתפענחים בכלל. כתובת IP קבועה נשברת בפעם הבאה שהקונטיינר נוצר מחדש, כי הוא מקבל כתובת חדשה.
מה לעשות
- השתמשו בשם השירות של Compose (או בכינוי מהשלב הקודם) בתצורה של ה-proxy, אף פעם לא בכתובת IP.
- ב-Coolify, לשמות מתווספת סיומת שנוצרת אוטומטית: העתיקו את השם המדויק מ-
docker ps, או תנו ל-Coolify לנתב לפי דומיין במקום לפי שם שהקלדתם. - Caddy: טענו מחדש את ה-Caddyfile (Caddy בודק אותו קודם, ומשאיר את התצורה הקודמת אם היא לא תקינה).
docker exec <proxy> caddy reload --config /etc/caddy/Caddyfile - nginx: בדקו את התצורה, ואז טענו אותה מחדש בלי לנתק חיבורים פתוחים.
docker exec <proxy> sh -c 'nginx -t && nginx -s reload' - בקשו את האתר שוב דרך ה-proxy; קוד התגובה הוא המספר הראשון.
curl -sS -o /dev/null -w '%{http_code}\n' https://<domain>/
ל-proxy שעל השרת אין פורט מפורסם להגיע אליו
proxy שמותקן על השרת לא יכול להשתמש בשמות של קונטיינרים או להגיע לפורט שלא פורסם. פרסמו את הפורט של האפליקציה על כתובת ה-loopback של השרת, כך שרק ה-proxy המקומי מגיע אליו, וכוונו את ה-proxy ל-127.0.0.1 ולפורט הזה.
מה לעשות
- בשירות:
ports: ["127.0.0.1:3000:3000"](קודם הפורט של השרת, אחריו הפורט של הקונטיינר). - צרו את הקונטיינר מחדש כדי שהשינוי ייכנס לתוקף (Compose מחליף את הקונטיינר; הנתונים ב-volumes שלו נשארים).
docker compose up -d --force-recreate <service> - ב-proxy:
reverse_proxy 127.0.0.1:3000(Caddy) אוproxy_pass http://127.0.0.1:3000;(nginx), ואז טענו מחדש את השירות של ה-proxy. - בדקו את המיפוי.
docker port <container>
האפליקציה מדברת HTTPS וה-proxy מדבר אליה HTTP
חלק מה-images מגישים רק HTTPS על הפורט שלהם, עם תעודה משלהם. proxy ששולח HTTP רגיל לפורט הזה מקבל תשובה ריקה או 400, ועונה למבקר ב-502. או שה-proxy מדבר HTTPS עם האפליקציה, או שהאפליקציה מגישה HTTP רגיל בתוך הרשת וה-proxy מחזיק את ה-TLS בקצה.
מה לעשות
- עדיף HTTP רגיל בתוך הרשת: לרוב ה-images יש הגדרה או פורט שני לזה.
- אחרת הגדירו את זה ב-proxy: Caddy
reverse_proxy https://app:8443, nginx proxy_pass https://app:8443;, Traefik ה-label traefik.http.services.<name>.loadbalancer.server.scheme=https. תעודה self-signed באפליקציה דורשת גם שה-proxy יסמוך עליה. - Caddy: טענו מחדש את ה-Caddyfile (Caddy בודק אותו קודם, ומשאיר את התצורה הקודמת אם היא לא תקינה).
docker exec <proxy> caddy reload --config /etc/caddy/Caddyfile - nginx: בדקו את התצורה, ואז טענו אותה מחדש בלי לנתק חיבורים פתוחים.
docker exec <proxy> sh -c 'nginx -t && nginx -s reload' - Traefik ו-Coolify קוראים labels מהקונטיינר של האפליקציה: אחרי שינוי label (או הגדרה בממשק של Coolify), צרו מחדש או פרסו מחדש את האפליקציה, לא את ה-proxy.
האפליקציה עונה לאט יותר ממה שה-proxy מחכה
כל proxy מחכה זמן מוגבל לכותרות התשובה. nginx מחכה 60 שניות כברירת מחדל (proxy_read_timeout) ואז עונה 504; proxy שמוותר בזמן ההתחברות, או אפליקציה שמנתקת בקשה איטית, מופיעים כ-502. הגדלת המגבלה מסתירה בקשה איטית; היא לא מתקנת אותה.
מה לעשות
- מצאו את הבקשה האיטית בלוג של האפליקציה ותקנו אותה קודם (אינדקס חסר, קריאה לשירות אחר, משימה ארוכה שמקומה ב-worker ברקע).
docker logs --since 15m <container> 2>&1 | tail -n 50 - אם מסלול חייב להיות איטי (ייצוא, העלאה), הגדילו את המגבלה רק למסלול הזה: nginx
proxy_read_timeout 300s;, Caddy transport http { response_header_timeout 300s }, Traefik forwardingTimeouts.responseHeaderTimeoutשל serversTransport. - בדקו גם את ה-timeout של האפליקציה עצמה: worker שנהרג אחרי 30 שניות (ברירת המחדל של
--timeoutב-gunicorn) סוגר את החיבור, וה-proxy עונה 502. - nginx: בדקו את התצורה, ואז טענו אותה מחדש בלי לנתק חיבורים פתוחים.
docker exec <proxy> sh -c 'nginx -t && nginx -s reload'
ה-proxy לא מעביר את ה-upgrade של WebSocket
WebSocket מתחיל כבקשת HTTP עם Upgrade: websocket ו-Connection: upgrade. Caddy ו-Traefik מעבירים אותה בלי הגדרה. nginx לא: הוא מדבר HTTP/1.0 עם האפליקציה ומשמיט את הכותרות האלה אם לא אומרים לו אחרת, והוא סוגר חיבור שקט אחרי proxy_read_timeout. Caddy סוגר WebSockets פתוחים כשהתצורה שלו נטענת מחדש, אלא אם מוגדר stream_close_delay.
מה לעשות
- nginx: ב-
location, proxy_http_version 1.1;, proxy_set_header Upgrade $http_upgrade;ו-proxy_set_header Connection $connection_upgrade;, עםmap $http_upgrade $connection_upgrade { default upgrade; '' close; }בבלוקhttp. - nginx: הגדילו את
proxy_read_timeoutל-locationהזה (למשל3600s), או גרמו לאפליקציה לשלוח pings בתדירות גבוהה מהמגבלה. - Caddy: הגדירו
stream_close_delayב-reverse_proxyאם טעינות מחדש מנתקות חיבורים חיים. - מאחורי proxy שני או CDN, בדקו שגם הוא מאפשר WebSockets.
- nginx: בדקו את התצורה, ואז טענו אותה מחדש בלי לנתק חיבורים פתוחים.
docker exec <proxy> sh -c 'nginx -t && nginx -s reload'
הכותרות של התשובה גדולות מה-buffer של nginx
nginx קורא את כותרות התשובה ל-buffer אחד, proxy_buffer_size, דף זיכרון אחד כברירת מחדל (4k או 8k). כותרות Set-Cookie גדולות (session עם הרבה claims, כמה cookies אחרי התחברות) או כותרת Link ארוכה גולשות ממנו, nginx רושם upstream sent too big header, והמבקר מקבל 502 בדיוק בעמודים האלה.
מה לעשות
- הגדילו את ה-buffers ב-
location(או ב-server): proxy_buffer_size 16k;ו-proxy_buffers 8 16k;, ו-proxy_busy_buffers_size 32k;אם nginx מתלונן עליו אחר כך. - nginx: בדקו את התצורה, ואז טענו אותה מחדש בלי לנתק חיבורים פתוחים.
docker exec <proxy> sh -c 'nginx -t && nginx -s reload' - בדקו למה הכותרות כל כך גדולות: session שנשמר כולו ב-cookie מקומו בשרת.
הקונטיינר הישן נעצר לפני שהחדש מוכן
בזמן פריסה, ה-proxy שולח בקשות לקונטיינר החדש ברגע שהוא קיים, כשהאפליקציה בתוכו עדיין עולה, או לישן אחרי שנעצר. בלי healthcheck ה-proxy לא יכול להבדיל ביניהם.
מה לעשות
- תנו לאפליקציה healthcheck שעונה רק כשהיא יכולה לשרת בקשות (עם
start_periodשמכסה את העלייה שלה). - ב-Coolify, אפליקציה (לא Docker Compose) מקבלת rolling update: הקונטיינר החדש עולה בזמן שהישן רץ, ועם health check Coolify מחכה שהוא יעבור. פורט שפורסם על השרת או שם קונטיינר מותאם גורמים ל-Coolify לעצור קודם את הישן; הסירו אותם אם ה-proxy הוא הדרך היחידה פנימה.
- עקבו אחרי ה-health של הקונטיינר החדש בפריסה הבאה.
docker ps --format 'table {{.Names}}\t{{.Status}}'
האפליקציה מנתקת חיבורים: היא קורסת או נהרגת באמצע בקשה
ה-proxy היה מחובר, והאפליקציה סגרה את החיבור בלי תשובה. לרוב worker מת: נהרג בגלל זיכרון (ה-OOM killer של הקרנל, או מגבלת הזיכרון של הקונטיינר), נהרג בגלל ה-timeout של עצמו, או קרס על הבקשה הזו. בעומס זה קורה יותר, כי יותר workers מגיעים למגבלה בבת אחת.
מה לעשות
- בדקו אם הקונטיינר נהרג בגלל זיכרון וכמה פעמים הופעל מחדש.
docker inspect -f 'oom={{.State.OOMKilled}} restarts={{.RestartCount}} exit={{.State.ExitCode}}' <container> - קראו את הלוג של האפליקציה סביב הזמן של ה-502: timeout של worker, stack trace, או הפעלה מחדש של worker.
docker logs --since 15m <container> 2>&1 | tail -n 50 - עקבו אחרי הזיכרון מול המגבלה בזמן שה-502 מופיעים.
docker stats --no-stream <container> - אם הקונטיינר עצמו מופעל מחדש, עקבו אחרי קוד היציאה שלו באשף לולאת ההפעלה מחדש; אם הזיכרון הוא הסיבה, הגדילו את המגבלה או הקטינו את מספר ה-workers, וחשבו מחדש את גודל השרת.
ה-proxy שולח בקשות לכתובת אחרת מזו שבדקתם
האפליקציה עונה, ובכל זאת כל בקשה דרך ה-proxy נכשלת, אז ה-proxy לא שולח לכתובת הזו. המקרה הקלאסי הוא nginx: שם ב-proxy_pass מתפענח פעם אחת, כש-nginx עולה. אחרי שהקונטיינר של האפליקציה נוצר מחדש הוא מקבל כתובת IP חדשה, ו-nginx ממשיך לשלוח לישנה עד שהוא נטען מחדש. כתובת IP קבועה בתצורה נכשלת באותה דרך.
מה לעשות
- השוו את הכתובת בשורת השגיאה של ה-proxy לכתובת הנוכחית של האפליקציה משלב בדיקת השם.
- nginx: בדקו את התצורה, ואז טענו אותה מחדש בלי לנתק חיבורים פתוחים.
docker exec <proxy> sh -c 'nginx -t && nginx -s reload' - כדי ש-nginx לא ישמור את הכתובת, השתמשו ב-DNS של Docker עם תוקף קצר ובמשתנה:
resolver 127.0.0.11 valid=10s;ואזset $app http://app:3000;ו-proxy_pass $app;. - ב-Coolify, בדקו שהדומיין מוגדר על המשאב שפרסתם ופרסו אותו מחדש, כדי שה-labels של ה-proxy יפנו לקונטיינר הנוכחי.
- בקשו את האתר שוב דרך ה-proxy; קוד התגובה הוא המספר הראשון.
curl -sS -o /dev/null -w '%{http_code}\n' https://<domain>/
עדיין אין התאמה: מה לאסוף
האפליקציה מאזינה, ובכל זאת בקשה מהצד של ה-proxy לא מגיעה אליה. משהו ביניהם חוסם את התעבורה: כלל firewall בשרשרת DOCKER-USER, רשת שנוצרה עם internal: true במקום הלא נכון, או שרת עם הגדרת Docker לא סטנדרטית (Docker במצב rootless, או תוכנת firewall שכותבת מחדש את הכללים של Docker). אספו את אלה לפני שאתם מבקשים עזרה; אף אחד מהם לא משנה את השרת.
מה לעשות
- בלוק התצורה של ה-proxy לאתר הזה (בלי סודות).
- שורות השגיאה האחרונות של ה-proxy.
docker logs --since 15m <proxy> 2>&1 | tail -n 50 - הרשת המשותפת: ה-driver שלה, האם היא internal, והקונטיינרים שמחוברים אליה.
docker network inspect <network> - כללי ה-firewall ש-Docker ואתם הוספתם.
sudo iptables -S DOCKER-USER - הגרסה וההגדרה של Docker.
docker info --format '{{.ServerVersion}} {{.SecurityOptions}}'
מה לא מכוסה כאן
האשף מכסה שרת Linux יחיד עם Docker Engine, שבו ה-proxy רץ בקונטיינר או על השרת עצמו. הוא לא בודק את השרת שלכם מבחוץ, ולא מכסה CDN או load balancer לפני ה-proxy, ingress controllers של Kubernetes, שירותי Docker Swarm או Docker Desktop.
הפקודות מניחות הרשאות root דרך sudo ואת ה-ss וה-curl של השרת עצמו; Docker במצב rootless מציב קונטיינרים ב-namespace אחר, והבדיקות שם שונות. כשאף ענף לא מתאים, השלב האחרון מפרט מה לאסוף; הוא לא מנחש.
עמודים קשורים
מקורות
- Docker Docs: docker container ls
- Docker Docs: docker container logs
- Coolify Docs: Bad Gateway error
- Compose file reference: healthcheck
- Dockerfile reference: HEALTHCHECK
- Docker Docs: docker container inspect
- ss(8), Linux manual page
- nsenter(1), Linux manual page
- Docker Docs: Networking overview
- Next.js Docs: output (standalone)
- nginx: Module ngx_http_proxy_module
- Caddy Documentation: reverse_proxy
- Traefik Docs: Docker routing configuration
- Docker Docs: docker network connect
- Compose file reference: networks
- Docker Docs: Networking in Compose
- Coolify Docs: Docker Compose
- Docker Docs: Bridge network driver
- Docker Docs: Port publishing and mapping
- Docker Docs: docker container port
- curl man page
- Traefik Docs: ServersTransport
- nginx: WebSocket proxying
- Coolify Docs: Rolling updates
- Coolify Docs: Health checks
- Traefik Docs: Logs
- nginx: Module ngx_http_core_module, resolver
האופן שבו ANAROQ מתעד מקורות לעובדות שבעמודים שלו מתואר במתודולוגיה. למתודולוגיה