# 用筆電在本地起 Ubuntu Server 並部署 .NET Core 程式(2) - 準備好專案並用 GitHub Action 實現 CD 流程 接續上篇,現在已經在小筆電上把環境準備好,同時讓外網可以連網,接下來做的是本地部署。 有些人可能會問為何不使用 Docker,但我會說其實作法大同小異,最後 SSH 連入電腦內去使用部署腳本,也可以將腳本修改成觸發 Docker 更新 Image,這也是種做法。 此處暫時以不用 Docker 的方式來做,目的是讓這些操作的學習成本最小化,Docker 的好處是可以讓你在不同的環境中保持一致性、讓你能夠快速地部署和擴展應用程式,但如果只是部署在一台小筆電上,使用 Docker 可能會增加不必要的複雜性,即殺雞用牛刀。 請注意以下的指令請「明確知道這些在做啥」再進行操作,不要盲目地複製。 每個人的做法不同,有些指令是必須要取代成自己的專案或使用者名稱的。 如果上述都了解,那我們就可以開始下一步驟了。 ## DotNet 專案 部署流程 要部署就會需要有專案,我已經預先準備一個 Worker Service 來部署我的 Discord Bot,這個專案是用來當作範例的,實際上你可以用任何的專案來替代它。 以下將說明在 Ubuntu Server 上,為每個 GitHub 專案(以 ExampleBot 為例)建立獨立的部署流程,包含資料夾規劃、git pull、部署腳本、以及 GitHub Actions 觸發 CD(持續部署)。 ExampleBot 這個專案並不存在,只是我用來指定為範例的名稱,實際上你可以用任何的專案來替代它。 ## 前置作業 - 確保 Ubuntu Server 上已安裝 `git` 、 `dotnet` 、 `vim` 。 ### 安裝 git ```bash sudo apt update sudo apt install git ``` ### 安裝 dotnet SDK 9.0 版本: ```bash sudo apt install dotnet-sdk-9.0 ``` Runtime 9.0 版本: ```bash sudo apt install aspnetcore-runtime-9.0 ``` ### 安裝 vim ```bash sudo apt install vim ``` ### 安裝 systemd(如果還沒有) ```bash sudo apt install systemd ``` ## 一、 規劃資料夾結構 每個人規劃的資料夾結構不一樣,這裡提供一個範例,建議先去理解 Ubuntu 的資料夾結構再來決定要如何整理專案,此處我採用在 `/opt/github-projects/` 下建立專案資料夾的方式。 在 `/opt/github-projects/` 下,每個專案一個資料夾,部署腳本放在專案資料夾內的 `scripts` 中。 ```bash sudo mkdir -p /opt/github-projects/ExampleBot/source sudo mkdir -p /opt/github-projects/ExampleBot/deploy sudo mkdir -p /opt/github-projects/ExampleBot/logs sudo mkdir -p /opt/github-projects/ExampleBot/scripts ``` - `/opt/github-projects/ExampleBot/source`:放置專案原始碼(git clone 的位置) - `/opt/github-projects/ExampleBot/deploy`:放置專案執行檔(publish 輸出) - `/opt/github-projects/ExampleBot/logs`:放置日誌 - `/opt/github-projects/ExampleBot/scripts`:放置專案的部署腳本 想要查看資料夾結構可以安裝 `tree` 指令: ```bash sudo apt install tree ``` 這樣就可以顯示結構了: ```bash tree /opt/github-projects/ExampleBot ``` 你的專案結構可能如下: ```text /opt/github-projects/ExampleBot ├── deploy ├── logs ├── scripts └── source ``` ## 二、 建立部署腳本(`ExampleBot-deploy.sh`) 名稱也可以自行取名,但此處我採用 `ExampleBot-deploy.sh`。 用 vim 編輯 `ExampleBot-deploy.sh` 的步驟如下: ### 1. 開啟檔案 ```bash sudo vim /opt/github-projects/ExampleBot/scripts/ExampleBot-deploy.sh ``` (如果檔案不存在會自動建立) ### 2. 進入「插入模式」開始編輯 - 按下 `i` 進入「插入模式」(此時左下角會顯示 `-- INSERT --`)。 - 開始輸入你的 shell script 內容,例如: ```bash #!/bin/bash set -e # 發生錯誤時立即終止腳本 # === 參數設定 === SOURCE_DIR="/opt/github-projects/ExampleBot/source" # 原始碼目錄 DEPLOY_DIR="/opt/github-projects/ExampleBot/deploy" # 執行檔(publish)目錄 LOG_DIR="/opt/github-projects/ExampleBot/logs" # 日誌目錄 REPO_URL="git@github.com:GithubUser/ExampleBot.git" # GitHub 倉庫網址 SERVICE_NAME="ExampleBot.service" # systemd 服務名稱 # === 日誌檔案 === LOG_FILE="$LOG_DIR/deploy.log" echo "[$(date '+%Y-%m-%d %H:%M:%S')] 開始部署 ExampleBot" | tee -a "$LOG_FILE" # === 取得/更新原始碼 === if [ ! -d "$SOURCE_DIR/.git" ]; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] Clone 專案原始碼..." | tee -a "$LOG_FILE" git clone "$REPO_URL" "$SOURCE_DIR" | tee -a "$LOG_FILE" else echo "[$(date '+%Y-%m-%d %H:%M:%S')] 更新專案原始碼..." | tee -a "$LOG_FILE" cd "$SOURCE_DIR" git pull | tee -a "$LOG_FILE" fi # === 清空 deploy 目錄 === echo "[$(date '+%Y-%m-%d %H:%M:%S')] 清空 deploy 目錄..." | tee -a "$LOG_FILE" rm -rf "$DEPLOY_DIR"/* mkdir -p "$DEPLOY_DIR" # === dotnet publish === echo "[$(date '+%Y-%m-%d %H:%M:%S')] dotnet publish..." | tee -a "$LOG_FILE" cd "$SOURCE_DIR/ExampleBot.Service" dotnet clean | tee -a "$LOG_FILE" # 此處的 publish 位置是 deploy 資料夾 dotnet publish -c Release -o "$DEPLOY_DIR" | tee -a "$LOG_FILE" # === 重啟 systemd 服務 === echo "[$(date '+%Y-%m-%d %H:%M:%S')] 重啟 systemd 服務..." | tee -a "$LOG_FILE" sudo systemctl restart "$SERVICE_NAME" echo "[$(date '+%Y-%m-%d %H:%M:%S')] ✓ 部署完成!" | tee -a "$LOG_FILE" ``` ### 3. 儲存並離開 - 按下 `Esc` 回到「一般模式」。 - 輸入 `:wq`(write & quit),然後按 `Enter`,即可儲存並離開。 ### 4. 設定執行權限 ```bash sudo chmod +x /opt/github-projects/ExampleBot/scripts/ExampleBot-deploy.sh ``` 註: 如果發現 `Permission denied` 錯誤,請檢查 `/opt/github-projects/ExampleBot/scripts/ExampleBot-deploy.sh` 的權限是否正確。 ```bash ls -l /opt/github-projects/ExampleBot/scripts/ExampleBot-deploy.sh ``` ### 5. 執行腳本 記得不用 root 權限跑,即不使用 sudo,因為這個腳本會在 systemd 服務中執行。 ```bash /opt/github-projects/ExampleBot/scripts/ExampleBot-deploy.sh ``` ### 6. 故障排除 如果發現 `sudo systemctl restart "$SERVICE_NAME"` 這行錯誤,一般是要求輸入密碼,如下: ```text [2025-05-02 15:33:52] 重啟 systemd 服務... ==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ==== Authentication is required to restart 'Example.service'. Authenticating as: Ubuntu的使用者 (YourUserName) ``` #### (1) 新增新的 `visudo` 設定檔 這個檔案會在 `/etc/sudoers.d/` 下,檔名可以隨意取,但建議與專案名稱有關聯,例如 `ExampleBot`。 這麼做的原因是為了讓 `sudo` 在執行腳本時不需要輸入密碼。 而且不影響 `visudo` 的安全性,因為這個檔案的權限會是 `0440`,只有 root 和 sudo 群組的使用者可以讀取,從而確保其內容不會被未授權的使用者修改或查看。 ```bash sudo visudo -f /etc/sudoers.d/ExampleBot ``` #### (2) 在檔案中加入以下內容 操作內容如果是 `vim` 編輯器,請按下 `i` 進入插入模式,然後貼上以下內容: ```text YourUserName ALL=(ALL) NOPASSWD: /bin/systemctl restart ExampleBot.service ``` 這裡的 `YourUserName` 是你登入 Ubuntu Server 的使用者名稱,`ExampleBot.service` 是你要執行的 systemd 服務名稱。 #### (3) 儲存並離開 - 按下 `Esc` 鍵離開插入模式。 - 輸入 `:wq`(即英文冒號加 wq),然後按下 `Enter` 鍵即可儲存並離開。 - wq:write & quit,儲存並離開 vim 編輯器。 現在你就可以在腳本中使用 `sudo systemctl restart ExampleBot.service` 而不需要輸入密碼了。 這點很重要,因為使用 `appleboy/ssh-action` 時,會讀腳本,腳本中帶有 `sudo`。 如果上述沒設定,會要求輸入密碼,導致 GitHub Actions 無法正常運行。 ### vim 常用指令速查 - 進入插入模式:`i` - 回到一般模式:`Esc` - 儲存檔案:`:w` - 離開 vim:`:q` - 儲存並離開:`:wq` - 不儲存強制離開:`:q!` ## 三、 設定 SSH 金鑰讓 Ubuntu Server 可以拉取 Private Repo 1. 在 Ubuntu Server 產生 SSH 金鑰(如果還沒有): ```bash ssh-keygen -t ed25519 -C "yourEmail@gmail.com" ``` 2. 將公鑰加到伺服器的 `authorized_keys` 確保伺服器已經允許你的金鑰登入: ```bash cat ~/.ssh/id_ed25519.pub >> ~/.ssh/authorized_keys ``` 確保 `~/.ssh` 目錄和 `authorized_keys` 檔案的權限正確: ```bash chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys ``` 3. 將公鑰內容(`~/.ssh/id_ed25519.pub`)加到你的 GitHub 帳號的 SSH keys。 4. 測試連線: ```bash ssh -T git@github.com ``` ## 四、 設定 systemd 服務 ### 1. 使用 vim 建立 systemd 服務檔案 在終端機輸入以下指令,開啟(或建立)服務檔案: ```bash sudo vim /etc/systemd/system/ExampleBot.service ``` ### 2. 在 vim 中貼上服務內容 進入 vim 後,請按下 `i` 進入插入模式,然後貼上以下內容: ```ini [Unit] Description=ExampleBot Discord Bot [Service] WorkingDirectory=/opt/github-projects/ExampleBot/deploy ExecStartPre=/usr/bin/echo "準備啟動 ExampleBot" ExecStart=/usr/bin/dotnet /opt/github-projects/ExampleBot/deploy/ExampleBot.Service.dll ExecStartPost=/usr/bin/echo "ExampleBot 啟動完成" Restart=always RestartSec=10 StartLimitIntervalSec=60 StartLimitBurst=3 User=YourUserName EnvironmentFile=/etc/ExampleBot.env [Install] WantedBy=multi-user.target ``` ### 3. 儲存並離開 vim - 按下 `Esc` 鍵離開插入模式 - 輸入 `:wq`(即英文冒號加 wq),然後按下 `Enter` 鍵即可儲存並離開 ### 4. 設定環境變數 也可以直接在 `ExampleBot.service` 中設定環境變數,但建議使用獨立的環境變數檔案,這樣更方便管理和修改。 #### (1) 建立環境變數檔案 ```bash sudo touch /etc/ExampleBot.env sudo chmod 600 /etc/ExampleBot.env ``` #### (2) 編輯環境變數檔案 ```bash sudo vim /etc/ExampleBot.env ``` 在 vim 中按下 `i` 進入插入模式,然後貼上以下內容: ```bash # ExampleBot 環境變數設定檔 # 設定環境變數,例如: BOT_TOKEN=somefakeDiscordToken(替換成你的Token) ASPNETCORE_ENVIRONMENT=Production # 其他環境變數設定可以在這裡添加 ``` ### 5. 重新載入 systemd 設定 ```bash sudo systemctl daemon-reload ``` ### 6. 啟用服務(開機自動啟動) ```bash sudo systemctl enable ExampleBot.service ``` ### 7. 啟動服務 ```bash sudo systemctl start ExampleBot.service ``` ### 8. 檢查服務狀態 ```bash sudo systemctl status ExampleBot.service ``` 看到 `active (running)` 代表服務已經啟動成功。 ### 9. 如果修改服務,用Vim修改後儲存即可 更新服務,語法與上述一樣: ```bash sudo systemctl daemon-reload sudo systemctl enable ExampleBot.service sudo systemctl start ExampleBot.service ``` ## 五. 設定 GitHub Actions 觸發 CD ### 1. GitHub Actions 直接 SSH 到 Server 執行部署腳本 1. 在 GitHub 專案設定 Secrets,新增 `KEY`(內容為 Ubuntu Server 的 SSH 私鑰)。 2. 在 `.github/workflows/dotnet-cd.yml` 新增 workflow: ```yaml name: Deploy to Server on: push: branches: - master # 當 `master` 分支有新推送時觸發 jobs: deploy: name: Deploy to Ubuntu Server runs-on: ubuntu-latest steps: # 檢出最新版本代碼 - name: Checkout repository uses: actions/checkout@v3 # 執行 SSH 部署 - name: Deploy via SSH uses: appleboy/ssh-action@v1 with: host: ${{ secrets.HOST }} # 伺服器主機對外 IP 位址,例如:1.2.3.4 或www.myDomainName.com.tw username: ${{ secrets.USERNAME }} # 登入伺服器的使用者名稱,例如: YourUserName key: ${{ secrets.KEY }} # 私鑰內容,來自 GitHub Secrets port: ${{ secrets.PORT }} # SSH 連接埠,預設為 22 script: | bash /opt/github-projects/ExampleBot/scripts/ExampleBot-deploy.sh ``` 於第一篇中,我們有開放 SSH port (22) 至外網,並做 Port-Forwarding,所以此處記得是外網 IP 或 DDNS DomainName。 這裡的 key 是你在 GitHub 專案設定 Secrets 時新增的 SSH 私鑰,這個私鑰是用來連接到你的 Ubuntu Server 的。 而私鑰必須要在 Ubuntu Server 上的 `~/.ssh/authorized_keys` 中有對應的公鑰,這樣才能成功連接。 ### 2. 在 GitHub 專案設定 Secrets,新增 `HOST`、`USERNAME`、`PORT`。 在 GitHub 專案設定 Secrets,新增 `HOST`、`USERNAME`、`PORT`。 - `HOST`:伺服器主機對外 IP 位址,例如:`1.2.3.4` 或 `www.myDomainName.com.tw` - `USERNAME`:登入 Ubuntu 伺服器的使用者名稱,例如:`YourUserName` - `PORT`:SSH 連接埠,預設為 `22` - `KEY`:內容為 Ubuntu Server 的 SSH 私鑰。 - 這個私鑰是用來連接到你的 Ubuntu Server 的,必須要在 Ubuntu Server 上的 `~/.ssh/authorized_keys` 中有對應的公鑰,這樣才能成功連接。 記得從 Ubuntu Server 上取得 SSH 私鑰,並將其內容複製到 GitHub Secrets 中。 1. 取得私鑰指令會是: ```bash cat ~/.ssh/id_ed25519 ``` ```bash # 確保私鑰的權限正確 chmod 600 ~/.ssh/id_ed25519 ``` 2. 複製私鑰內容到 GitHub Secrets 中,並命名為 `KEY`。私鑰範例為: ```text --BEGIN OPENSSH PRIVATE KEY-- b3BlbnNzaC1rZDI1NiB... (省略) ...== --END OPENSSH PRIVATE KEY-- ``` ## 六. 未來新增其他專案 - 在 `/opt/github-projects/` 下新增資料夾(如 `/opt/github-projects/OtherProject`)。 - 在 `/opt/github-projects/OtherProject/` 下新增 `deploy.sh`。 - 依照上述步驟複製調整即可。 ## 結語 如果你能順利地在小筆電部署成功那就太棒了,但這途中一定會遇到各種指令衝突、權限、連網問題。 至於部署之後還有些內容必須處理,例如: - Github Action 可以在 Merge Request 觸發 CI - 完成後觸發 CD (上述 GitHub Action 已經提及) - 如何有一套好的 GitFlow,Unit Test - 如何將 Unit Test 綁入 CI 中等等,以上都是必須要自行去研究的 慶幸的是這個時代我們是有 AI 可以使用的,一邊爬官方文章一邊翻譯,或直接將問題丟給 AI 後,將得到的結果拿到網路上進行驗證,確保指令是「可信的」。 AI 也可能會為了簡單了事,給你不安全的作法,所以每一個指令都必須要再三確認,特別是自己並不懂哪種實作較佳,你可以選擇在直接使用指令之前,先將指令拿去查詢文件,也同時拿去問 AI 這是否是安全的做法。 ## 可以改善 從上述的部署流程來看,這個部署方式還留有些缺陷: ### 一、現有流程的問題 目前的流程是: 1. 在 Ubuntu Server 上 git clone 原始碼。 2. 在 Server 上 dotnet publish。 3. 在 Server 上執行部署腳本、重啟服務。 這種方式的**缺點**: - **Server 上需要安裝完整 .NET SDK**(而不是只需 Runtime),這樣會佔用較多資源。 - **Server 上有完整原始碼**,有資安風險。 - **部署流程分散**:source、deploy、logs、scripts、env、service 檔案分散各處,換主機時要重新設定很多東西。 - **維護成本高**:每次部署都要在 Server 上拉原始碼、編譯、publish,容易出錯且速度慢。 ## 二、最佳實踐建議 ### 1. **建議的自動化部署流程** - **CI/CD(如 GitHub Actions)負責編譯與 publish**,產生已經可以直接執行的檔案(dll、config、靜態資源等)。 - **用 scp/rsync 將 publish 輸出檔案直接傳到 Server**,Server 只需要 .NET Runtime。 - **Server 上不需要原始碼,也不需要 .NET SDK**,只要能執行 dotnet xxx.dll 即可。 - **Server 上只需要簡單的 systemd 服務與環境變數檔案**,部署腳本也可以很簡單(甚至只剩下重啟服務)。 ### 2. **資料夾結構建議** - `/opt/github-projects/ExampleBot/deploy`:只放 publish 輸出檔案。 - `/opt/github-projects/ExampleBot/logs`:放日誌。 - `/etc/ExampleBot.env`:環境變數。 - `/etc/systemd/system/ExampleBot.service`:systemd 服務檔案。 **不需要** `/source` 目錄,也不需要在 Server 上 git clone。 ### 3. **GitHub Actions YAML 範例** ```yaml name: Deploy to Server on: push: branches: - master jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v3 - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: '9.0.x' - name: Publish run: dotnet publish -c Release -o ./publish - name: Copy files to server uses: appleboy/scp-action@v0.1.4 with: host: ${{ secrets.HOST }} username: ${{ secrets.USERNAME }} key: ${{ secrets.KEY }} port: ${{ secrets.PORT }} source: "./publish/*" target: "/opt/github-projects/ExampleBot/deploy/" - name: Restart systemd service uses: appleboy/ssh-action@v1 with: host: ${{ secrets.HOST }} username: ${{ secrets.USERNAME }} key: ${{ secrets.KEY }} port: ${{ secrets.PORT }} script: | sudo systemctl restart ExampleBot.service ``` ## 三、這樣做的優點 - **Server 只需安裝 .NET Runtime**,更輕量、更安全。 - **Server 不會有原始碼**,資安風險降低。 - **部署速度快**,因為只傳遞已編譯好的檔案。 - **維護簡單**,換主機只要複製 deploy、logs、env、service 檔案即可。 - **CI/CD 流程標準化**,容易擴展到多個專案。 ## 四、補充說明 - 可以把部署腳本(如重啟服務)寫在 Server 上,也可以直接在 GitHub Actions 的 SSH Action 裡執行。 - 如果有多個專案,可以用同樣的結構,每個專案一個 deploy 目錄、一個 systemd 服務、一個 env 檔案。 - 若有靜態檔案、前端專案,也可以用同樣方式處理。 ### 環境變數搬移至專案資料夾 環境變數檔案(如 `ExampleBot.env`)可以放在專案資料夾,只要在 systemd 服務檔案( `/etc/systemd/system/ExampleBot.service`)中這樣寫: ```ini EnvironmentFile=/opt/github-projects/ExampleBot/ExampleBot.env ``` systemd 啟動服務時,就會讀取這個檔案的環境變數。 ### 實務建議 - **單一主機多專案**:建議把每個專案的環境變數檔案放在專案資料夾下(如 `/opt/github-projects/ExampleBot/ExampleBot.env`),方便管理與備份。 - **權限設定**:請務必設定檔案權限,避免敏感資訊外洩,例如: ```bash sudo chmod 600 /opt/github-projects/ExampleBot/ExampleBot.env sudo chown youruser:youruser /opt/github-projects/ExampleBot/ExampleBot.env ```
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up