התקנת GitHub Actions Runner על שרת VPS עם Ubuntu 24.04
מדריך מעשי להגדרת GitHub Actions runner עצמאי על Ubuntu 24.04. כולל יצירת משתמש ייעודי, בדיקת checksum, הרצה כשירות systemd והסבר על סיכוני אבטחה ב-fork pull requests.
מה עושה GitHub Actions runner בהתקנה עצמית
GitHub Actions runner בהתקנה עצמית הוא תוכנה שמתקינים על ה-VPS שלכם, הפונה ל-GitHub כדי למשוך משימות ומריצה אותן על החומרה שלכם. אתם רושמים אותו מול מאגר (repository) מסוים, מתקינים אותו כשירות systemd, והוא עולה אוטומטית לאחר כל אתחול. GitHub מתזמנת את המשימה, והשרת שלכם מבצע את העבודה.
CI (אינטגרציה רציפה) על מכונה בבעלותכם כדאית משתי סיבות. דקות הבנייה מפסיקות להימדד, ומשימה יכולה לגשת למשאבים שזמינים רק למכונה שלכם, כמו מטמון בנייה (build cache) קיים או רשת פרטית. המחיר הוא אבטחה. ה-runner מבצע כל מה שכתוב בקובץ ה-workflow, תחת המשתמש שהגדרתם לו; לכן, קובץ workflow הוא למעשה הרצת קוד מרחוק (remote code execution) מעצם תכנונו. במאגר פרטי זה תקין, כיוון שרק אנשים שאתם סומכים עליהם יכולים להוסיף קובץ כזה. במאגר ציבורי מדובר בסיכון ממשי, והסעיף העוסק ב-fork pull requests מסביר את המנגנון.
כל המפורט להלן מבוסס על Ubuntu 24.04 עם גרסת runner מספר 2.338.0, שהיא הגרסה העדכנית נכון ל-12 באוקטובר 2026.
מה דרוש לך לפני תחילת העבודה
התחל משרת VPS עם חשבון מנהל רגיל והרשאות sudo, המצב אליו מגיעים ב-עשר הדקות הראשונות בשרת VPS חדש. אין צורך לפתוח פורט נכנס. ה-runner פותח חיבור HTTPS יוצא אל GitHub ומחזיק אותו פתוח בזמן שהוא ממתין למשימות, לכן GitHub לעולם לא מתחבר לשרת שלך ישירות. ה-firewall שלך יכול להישאר סגור לעולם החיצון, והמשימות יגיעו בכל זאת.
עליך להחזיק גם בהרשאות ניהול במאגר (repository), כיוון ש-token הרישום מוצג בהגדרות המאגר.
יצירת משתמש ייעודי עבור ה-runner
לעולם אל תריצו את ה-runner כ-root או כמשתמש הניהול האישי שלכם. כל משימה יורשת את ההרשאות של משתמש ה-runner, לכן workflow שמריץ sudo יצליח אם למשתמש ה-runner יש הרשאות sudo. צרו משתמש ללא הרשאות מיוחדות שאינו מחזיק בדבר מלבד ספריית הבית שלו. המדריך חשבונות משתמש עם הרשאות מינימליות ב-VPS מכסה את התבנית הכללית. להלן היישום הספציפי.
sudo useradd -m -s /bin/bash gharunner
sudo passwd -l gharunner
sudo chmod 750 /home/gharunner
sudo install -d -m 700 -o gharunner -g gharunner /home/gharunner/actions-runnerהפקודה passwd -l נועלת את הסיסמה, כך שאף אחד לא יוכל להתחבר כ-gharunner באמצעותה. הגדרת מצב 700 על ספריית ה-runner היא קריטית, כיוון שה-runner מאחסן שם את פרטי ההזדהות שלו בטקסט גלוי, ופעולת checkout עלולה להכיל קוד מקור פרטי.
בדקו את שני המאפיינים הללו לפני שתמשיכו:
sudo passwd -S gharunner
sudo -l -U gharunnerהפקודה passwd -S מדפיסה שורה המתחילה ב-gharunner L, כאשר L מציין שהסיסמה נעולה. הפקודה sudo -l -U gharunner צריכה להחזיר is not allowed to run sudo. אם היא מדפיסה רשימה של פקודות מורשות, החשבון נמצא בקבוצת sudo והבידוד שיצרתם כרגע אינו קיים.
הורדת ה-runner ובדיקת ה-tarball
עבדו כמשתמש ה-runner החל משלב זה.
sudo -iu gharunner
cd ~/actions-runner
RUNNER_VERSION=2.338.0
curl -fL -o actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz \
"https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz"הריצו את uname -m תחילה אם אינכם בטוחים לגבי הארכיטקטורה. x86_64 משתמש בקובץ linux-x64 שלעיל. aarch64 משתמש ב-actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz.
כעת ודאו את תקינות הקובץ שהורדתם. ה-SHA256 (אלגוריתם גיבוב מאובטח, 256 סיביות) להלן מיועד ל-tarball בגרסה 2.338.0 עבור x64. GitHub מציגה את הערך עבור הגרסה הנוכחית בדף ה-release ובמסך ה-New self-hosted runner. הערך משתנה בכל גרסה, לכן העתיקו אותו משם בעת התקנת גרסה אחרת.
echo "af4b794c1bc41d73d40535e3fe092a39f9679cd8d965954c2aca25a05ca41d32 actions-runner-linux-x64-2.338.0.tar.gz" | sha256sum -cהורדה תקינה תציג שורה אחת:
actions-runner-linux-x64-2.338.0.tar.gz: OKקובץ חסר או שונה יציג הודעת כשל ואזהרה:
actions-runner-linux-x64-2.338.0.tar.gz: FAILED
sha256sum: WARNING: 1 computed checksum did NOT matchאל תדלגו על הבדיקה ואל תתנו ל-tar לגלות את הבעיה במקומכם. ארכיון שנכתב בחלקו נכשל עם gzip: stdin: unexpected end of file ו-tar: Unexpected EOF in archive, מה שמעיד על כך שהקובץ פגום אך לא מבהיר אם הוא נקטע או הוחלף.
tar xzf ./actions-runner-linux-x64-2.338.0.tar.gz
lsמה מכיל קובץ ה-tarball ומה הוא אינו מכיל
לאחר החילוץ, הספרייה מכילה את config.sh, run.sh, env.sh, safe_sleep.sh, bin/ ו-externals/. bin/ מכילה את קובצי ה-binary של ה-runner ואת bin/installdependencies.sh. externals/ מכילה את סביבת ה-Node המצורפת שעליה מורצים ה-JavaScript actions.
עדיין לא קיים svc.sh. התיעוד של GitHub מתאר אותו כסקריפט "שנוצר לאחר הוספה מוצלחת של ה-runner", כיוון שהוא נכתב מתוך תבנית שבה שם המאגר ושם ה-runner מוטמעים בתוך שם השירות. לכן, הרצה של sudo ./svc.sh install לפני ./config.sh תיכשל עם sudo: ./svc.sh: command not found. יש לבצע רישום תחילה, ורק לאחר מכן להתקין את השירות.
התקנת התלויות של ה-runner
ה-runner הוא יישום .NET, ולכן הוא זקוק למספר ספריות משותפות. צאו מה-shell של משתמש ה-runner והתקינו אותן באמצעות sudo, כיוון שהסקריפט כותב למסד הנתונים של חבילות המערכת.
exit
cd /home/gharunner/actions-runner
sudo ./bin/installdependencies.shב-Ubuntu 24.04, פעולה זו מושכת את libkrb5-3, zlib1g, liblttng-ust1t64, libssl3t64 ו-libicu74. הסקריפט מנסה מספר שמות גרסאות עבור כל ספרייה ושומר את זו שההפצה שלכם מספקת; זו הסיבה שאותו סקריפט עובד גם על גרסאות Ubuntu ישנות יותר וגם על Debian.
אם תדלגו על שלב זה ואחת מהספריות הללו תהיה חסרה, ./config.sh יעצור לפני שיבצע פעולה כלשהי:
Dependencies is missing for Dotnet Core 6.0
Execute sudo ./bin/installdependencies.sh to install any missing Dotnet Core 6.0 dependencies.חוסר ב-libicu יציג את אותה עצה תחת שורת פתיחה שונה, Libicu's dependencies is missing for Dotnet Core 6.0. שתיהן מגיעות מאותו מקור: config.sh בודק את הספריות המצורפות באמצעות ldd, ו-libicu באמצעות ldconfig, לפני ההפעלה. כך, קישור לא פתור עוצר את הסקריפט במקום לגרום לקריסה מבלבלת בשלב מאוחר יותר.
רישום ה-runner מול המאגר שלכם
השיגו אסימון (token) מהמאגר. פתחו את Settings, לאחר מכן Actions, משם Runners, ולבסוף New self-hosted runner. הדף יציג אסימון רישום. תוקפו פג שעה לאחר יצירתו, לכן צרו אותו רק כאשר אתם מוכנים להדביק אותו.
בצעו את הרישום כמשתמש ה-runner. config.sh מסרב לרוץ תחת sudo.
sudo -iu gharunner
cd ~/actions-runner
./config.sh --url https://github.com/YOUR-USER/YOUR-REPO \
--token PASTE_REGISTRATION_TOKEN_HERE \
--name vps-runner-1 \
--labels vps \
--work _work \
--unattended \
--replaceמה עושים הדגלים הללו. --name קובע כיצד ה-runner יופיע במאגר, לכן בחרו שם שתוכלו לזהות גם בעוד שישה חודשים. --labels מוסיף תוויות (labels) משלכם; ה-runner כבר נושא את self-hosted, Linux ו-X64 ללא צורך בבקשה מפורשת. --work מגדיר את שם הספרייה שבה ימוקמו ה-checkouts, בתוך ספריית ה-runner. --unattended עונה על ההנחיות האינטראקטיביות בערכי ברירת המחדל שלהן, וזה מה שנדרש כאשר הפקודה מופעלת מתוך סקריפט. --replace משתלט על רישום קיים בעל אותו שם במקום להיכשל, וזהו המצב הרצוי בעת בנייה מחדש של השרת.
הרצה מוצלחת תסתיים בשורות הבאות:
√ Runner successfully added
√ Runner connection is good
√ Settings Saved.הרישום נשמר כעת בספריית ה-runner כ-.runner, .credentials ו-.credentials_rsaparams. שני הקבצים האחרונים מזהים את ה-runner הזה מול GitHub, לכן כל מי שיכול לקרוא אותם יוכל להתחזות אליו. זו הסיבה שהספרייה מוגדרת במצב 700 ולמשתמש אין הרשאות sudo.
התקנת ה-runner כשירות systemd
./run.sh בטרמינל מתאים לבדיקה חד-פעמית, אך התהליך יסתיים עם סגירת ה-session של ה-SSH. התקינו את השירות כדי שה-runner יעלה אוטומטית בעת עליית השרת. המדריך systemd services and timers on a VPS מסביר על קובצי ה-unit עצמם. כאן svc.sh יוצר אחד עבורכם.
exit
cd /home/gharunner/actions-runner
sudo ./svc.sh install gharunner
sudo ./svc.sh start
sudo ./svc.sh statussvc.sh דורש הרשאות root כיוון שהוא כותב קובץ unit לתוך /etc/systemd/system ומפעיל אותו. הארגומנט אחרי install הוא המשתמש שתחתיו ירוץ השירות. העבירו את gharunner במפורש. ללא ארגומנט, הסקריפט יחזור להשתמש ב-$SUDO_USER, שהוא חשבון ה-admin שלכם, ואז כל משימה תרוץ כמשתמש בעל הרשאות sudo.
ה-unit נקרא על שם ה-repository וה-runner, בתבנית actions.runner.YOUR-USER-YOUR-REPO.vps-runner-1.service. לעולם לא תצטרכו להקליד זאת ידנית:
systemctl list-units 'actions.runner.*'
sudo journalctl -u 'actions.runner.*' -n 20 --no-pager-runner תקין מתעד √ Connected to GitHub ולאחר מכן שורה שמסתיימת ב-Listening for Jobs, ודף ה-Runners של ה-repository יציג אותו כ-Idle. runner שמוצג כ-Offline אינו רץ או שאינו מצליח להגיע ל-GitHub בפורט 443.
שליחת משימה ל-runner
runs-on בוחר runner לפי תווית (label). בקשו את self-hosted בצירוף התווית שלכם, כדי שמשימה לא תגיע בטעות ל-runner שלא התכוונתם אליו.
name: build
on:
push:
branches: [main]
jobs:
build:
runs-on: [self-hosted, linux, vps]
steps:
- uses: actions/checkout@v5
- run: uname -aאם המשימה ממתינה ב-Waiting for a runner to pick up this job, התוויות אינן תואמות. כל תווית ב-runs-on חייבת להיות קיימת ב-runner, לכן מילה אחת מיותרת תשאיר את המשימה בתור ללא כל הודעת שגיאה. השוו את הרשימה מול התוויות המוצגות לצד ה-runner בהגדרות ה-repository.
מדוע אין להשתמש ב-self-hosted runners עבור מאגרים ציבוריים
זהו החלק שרוב האנשים מדלגים עליו. ההנחיות של GitHub חד-משמעיות: אין להשתמש ב-self-hosted runners עבור מאגרים ציבוריים כמעט בשום מקרה. הם אינם מבטיחים הרצה בתוך מכונות וירטואליות זמניות ונקיות, ועלולים להיפרץ באופן קבוע על ידי קוד לא מהימן בתוך workflow.
המנגנון פשוט. בקשת pull request מ-fork מביאה עמה עותק משלה של קובץ ה-workflow. אם המאגר הציבורי שלכם מריץ workflows של pull request על ה-runner שלכם, כל מי שיכול לבצע fork למאגר יכול להציע workflow שמריץ פקודות משלו על ה-VPS שלכם. הם אינם זקוקים להרשאות כתיבה, כיוון שהקוד שהם מציעים הוא הקוד שמתבצע בפועל.
הגדרות אישור (approval settings) ממתנות את הסיכון אך לא פותרות אותו. המדיניות כברירת מחדל עבור מאגר ציבורי דורשת ממנהל המאגר לאשר workflow של תורם חדש. GitHub מחשיבה משתמש כ"חדש" רק עד לרגע שבו commit או pull request שלו אושרו ומוזגו למאגר. לאחר מכן, ה-pull requests הבאים שלו ירוצו ללא אישור, ו-GitHub עצמה מזהירה שתיקון טריוויאלי שמוזג מספיק כדי להגיע למצב זה. לכן, השער הוא אדם שקורא את ה-diff, וקל מאוד לפספס קוד זדוני שמוסתר שלוש רמות עמוק בתוך סקריפט בנייה.
בקשת pull request מ-fork אינה מקבלת את ה-secrets שלכם, וה-GITHUB_TOKEN שלה הוא לקריאה בלבד. זה מגביל את הנזק בתוך GitHub, אך לא עושה דבר עבור השרת שלכם. לתוקף יש shell כמשתמש gharunner, כך שהוא יכול לקרוא כל קובץ שהמשתמש הזה יכול לקרוא, להגיע לכל יעד שה-VPS יכול להגיע אליו ברשת הפרטית שלו, ולהשאיר עקבות ב-~/.bashrc או ב-unit של systemd שיופעל בעבודה הבאה.
רישום באמצעות --ephemeral גורם ל-runner לקבל עבודה אחת ואז לבטל את הרישום, כך שעבודה אחת לא יכולה לקרוא את סביבת העבודה של העבודה הבאה. זה עוזר רק אם משהו מקים מחדש את המכונה או את המכולה עבור כל עבודה, כיוון שדלת אחורית שנכתבה בתיקיית הבית של משתמש ה-runner שורדת רישום מחדש.
הכללים הבאים קצרים. השתמשו ב-self-hosted runners עבור מאגרים פרטיים בלבד. אם אתם חייבים לחבר runner למאגר ציבורי, אל תריצו עליו pull requests מ-forks, אל תשמרו שום דבר אחר על השרת הזה, והתייחסו למכונה כאל רכיב חד-פעמי.
משימות Docker, והקבוצה שהיא למעשה root
משימות במכולות, מכולות שירות וכל שלב ב-workflow שמפעיל את docker build זקוקים ל-daemon של Docker על המארח של ה-runner. התקינו את Docker בדרך המקובלת, כפי שמתואר ב-Docker and Docker Compose on a VPS, ולאחר מכן הוסיפו את משתמש ה-runner לקבוצת docker.
הבינו את המשמעות לפני שתבצעו זאת. חברות בקבוצת docker שקולה להרשאות root, כיוון שמכולה יכולה לבצע bind mount ל-/ ולהריץ תהליכים כ-root בתוכה. לכן, workflow שיכול לתקשר עם ה-socket של Docker יכול לקרוא ולכתוב כל קובץ ב-VPS, כולל /etc/shadow. במאגר פרטי עם תורמים מהימנים זה עשוי להיות מחיר מקובל. בכל מקרה אחר, הדבר מבטל את הערך של שימוש במשתמש ללא הרשאות. Rootless Docker שומר על בניית מכולות במסגרת ההרשאות של משתמש ה-runner עצמו, במחיר של מנהל אחסון איטי יותר וחוסר יכולת להריץ מכולות בעלות הרשאות מורחבות (privileged containers).
עדכונים והסרה נקייה של ה-runner
ה-runner המארח את עצמו מתעדכן כברירת מחדל. הוא מזהה גרסה חדשה, מחליף את הקבצים שלו ומפעיל מחדש את השירות, כך שבדרך כלל אין צורך בפעולה מצדך. כדי לבטל את העדכון האוטומטי, יש להעביר את --disableupdate ל-./config.sh בעת רישום ה-runner; ב-runner שכבר נרשם, המשמעות היא רישום מחדש עם הדגל הזה. הדבר אינו מאפשר להישאר בגרסה אחת לצמיתות. התיעוד של GitHub מבהיר ש-runner שהוגדר עם --disableupdate עדיין חייב לעבור עדכון ידני בתוך 30 יום ממועד יציאת גרסה חדשה, אחרת GitHub יפסיק להעביר אליו משימות.
עדכון ידני שומר על הרישום, כיוון ש-.runner ו-.credentials אינם כלולים ב-tarball. יש לעצור את השירות, להוריד ולאמת את ה-checksum של ה-tarball החדש כ-gharunner, לחלץ אותו על אותה ספרייה באמצעות tar xzf, ולאחר מכן להפעיל את השירות מחדש:
cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh startכדי להסיר את ה-runner, יש להסיר תחילה את השירות מהמערכת ולאחר מכן לבטל את הרישום. את ה-token להסרה ניתן למצוא באותו דף Runners, תחת כפתור ה-Remove של ה-runner הספציפי.
cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh uninstall
sudo -iu gharunner
cd ~/actions-runner
./config.sh remove --token PASTE_REMOVAL_TOKEN_HEREמחיקת הספרייה ללא ביטול רישום תשאיר את ה-runner במצב Offline במאגר. הוא יישאר שם עד שמנהל ילחץ על Force remove this runner, או עד ש-GitHub יסיר אותו אוטומטית לאחר 14 ימים ללא חיבור.
מצבי כשל, והודעות שתראו
Must not run with sudo. התוכנה config.sh מדפיסה זאת ומסיימת את פעולתה כאשר היא מורצת כ-root. הבדיקה מכוונת, כיוון שקבצים בבעלות root בתוך _work משבשים כל משימה עתידית שרצה כמשתמש השירות. הריצו את ./config.sh כ-gharunner. המשתנה RUNNER_ALLOW_RUNASROOT עוקף את הבדיקה, אך שימוש בו רק דוחה את התקלה לשלב מאוחר יותר.
sudo: ./svc.sh: command not found. אתם נמצאים בספרייה הנכונה. svc.sh עדיין לא קיים, כיוון ש-config.sh טרם השלים רישום. רשמו את ה-runner, ולאחר מכן התקינו את השירות.
Http response code: NotFound from 'POST https://api.github.com/actions/runner-registration'. בדרך כלל מדובר ב-token שאינו token רישום תקין. או שהוא פג תוקף, כיוון שהם תקפים לשעה אחת בלבד, או ש-personal access token הודבק במקום ה-token של הרישום מדף ה-Runners. צרו token חדש והדביקו אותו שוב, וודאו ש---url הוא כתובת ה-URL המלאה של המאגר.
Dependencies is missing for Dotnet Core 6.0. הריצו את sudo ./bin/installdependencies.sh מספריית ה-runner כ-root, ולאחר מכן בצעו רישום מחדש.
ה-runner במצב Offline לאחר אתחול. הריצו את systemctl is-enabled 'actions.runner.*'. אם לא מופיע דבר, ./svc.sh install מעולם לא הופעל, ולכן ה-runner היה קיים רק בתוך סשן הטרמינל שלכם. אם ה-unit מופעל וה-runner עדיין במצב Offline, קראו את journalctl -u 'actions.runner.*' ובדקו תקשורת HTTPS יוצאת.
הדיסק מתמלא. בדיקות (checkouts), מטמוני בנייה (build caches) ותמונות Docker מצטברים תחת _work ובספריית הבית של משתמש ה-runner, ושום תהליך לא מנקה אותם עבורכם. עקבו אחר du -sh /home/gharunner/actions-runner/_work והוסיפו ניקוי מתוזמן לפני שהדיסק יחליט עבורכם.
FAQ
מדוע sudo ./svc.sh install מציג את השגיאה command not found?
הסיבה היא ש-svc.sh אינו כלול ב-tarball של ה-runner. הוא נוצר בספריית ה-runner לאחר ש-./config.sh מסיים את תהליך הרישום, תוך שימוש בשם המאגר ובשם ה-runner כדי לבנות את שם השירות. יש להריץ את ./config.sh תחילה כמשתמש ה-runner. לאחר מכן, sudo ./svc.sh install gharunner ימצא את הסקריפט ויכתוב יחידת שירות בשם actions.runner.OWNER-REPO.RUNNER-NAME.service לתוך /etc/systemd/system.
האם עלי לפתוח פורט ב-firewall עבור runner באירוח עצמי?
לא. ה-runner פותח חיבור HTTPS יוצא אל GitHub ומחזיק אותו פתוח בזמן שהוא ממתין למשימות, לכן GitHub לעולם לא יוזמת חיבור אל ה-VPS שלך. אפשרו תעבורה יוצאת בפורט 443 והשאירו את חוקי התעבורה הנכנסת סגורים. אם ה-runner מציג מצב Offline בזמן שהשירות רץ, בדקו סינון תעבורה יוצאת ו-DNS במקום חוקי כניסה.
האם ניתן להשתמש ב-runner באירוח עצמי עבור מאגר ציבורי?
ניתן, אך GitHub ממליצה שלא לעשות זאת. בקשת משיכה (pull request) מ-fork כוללת קובץ workflow משלה, כך שכל מי שיכול לבצע fork למאגר שלך יכול להציע פקודות שירוצו על המכונה שלך. הנחיית האישור המוגדרת כברירת מחדל מכסה רק תורמים שמעולם לא אושרה להם commit או pull request במאגר. אם אתם מחברים runner למאגר ציבורי, השביתו בו workflows של fork pull request, אל תשמרו שום דבר אחר על אותו שרת, ובצעו הקמה מחדש של המכונה לפי לוח זמנים.
מדוע הרישום נכשל עם Http response code: NotFound?
קריאת הרישום מחזירה NotFound כאשר פרטי ההזדהות שגויים, ולא רק כאשר ה-URL שגוי, מה שהופך את ההודעה למטעה. אסימוני רישום (registration tokens) פוקעים שעה אחת לאחר הצגתם, ו-personal access token אינו מתקבל עבור קריאה זו. פתחו שוב את Settings, Actions, Runners, New self-hosted runner, העתיקו את האסימון החדש, וודאו שערך ה---url מצביע על מאגר שבו יש לכם הרשאות ניהול.