用 GitHub Actions 自動部署到自架 Mac Mini Server 完整指南

內網 Mac Mini 不開 port、不需 VPN,用 self-hosted runner 讓 GitHub Actions 自動觸發 docker compose 部署,含 launchd service 設定與 Docker PATH 修正。

分享
用 GitHub Actions 自動部署到自架 Mac Mini Server 完整指南
Photo by Growtika on Unsplash

你有一台放在家裡或公司的 Mac Mini,想讓它跑 Docker 服務,又希望每次 push 到 GitHub 就自動部署——但問題來了:Mac Mini 在內網,沒有公開 IP,GitHub 的雲端機器根本連不進來。

解法是反過來讓 Mac Mini 主動出擊。這篇文章說明如何透過 GitHub Actions self-hosted runner,讓內網 Mac Mini 自己去 GitHub 拉 job 來執行,不需要開 port、不需要 VPN。

為什麼不用雲端 Runner

GitHub 提供的雲端 runner(ubuntu-latest 等)每次都是乾淨的全新環境,適合無狀態的測試或打包。但對內網機器的部署場景有根本性的限制:

  • GitHub 無法主動 SSH 進內網:雲端 runner 要連到你的機器,內網機器必須有公開 IP 或 tunnel,設定複雜
  • 每次重新 clone 很慢:雲端環境沒有快取,大型 repo 或 Docker image 每次都要重新下載
  • 沒有本機 .env 存取權:機密設定無法直接讀取本機檔案

Self-hosted runner 解決這三個問題:它是 Mac Mini 上的常駐 service,主動 poll GitHub,有 job 就自己拉下來跑,保有本機快取與檔案存取權。

整體架構

觸發流程:

開發者 merge PR → main 有新 push
  → GitHub 發出 push event
  → Mac Mini 上的 runner 收到 job
  → runner 在本機執行 git pull + docker compose rebuild
元件位置說明
GitHub repogithub.com/<owner>/<repo>原始碼來源
Workflow 檔.github/workflows/deploy-mac-mini.yml定義部署步驟
Self-hosted runnerMac Mini ~/actions-runner/執行 workflow job 的 agent
部署目標Mac Mini <project-path>/repo 在 Mac Mini 的 clone
Docker ComposeMac Mini <project-path>/管理容器生命週期

Workflow 設定

在 repo 建立 .github/workflows/deploy-mac-mini.yml

name: Deploy to Mac Mini

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: mac-mini

    steps:
      - name: Pull latest code
        working-directory: <project-path>/
        run: git pull origin main

      - name: Rebuild and restart services
        working-directory: <project-path>/
        run: |
          docker compose down
          docker compose up -d --build

幾個關鍵設定:

  • runs-on: mac-mini:指定只跑在 label 為 mac-mini 的 runner,不會跑到雲端機器
  • 兩段 working-directorygit pull 在 git repo 根目錄,docker compose 在 docker-compose.yml 所在位置
  • docker compose downup:先關掉舊容器再重建,確保每次都用最新程式碼

Self-hosted Runner 安裝步驟

在 Mac Mini 上執行以下指令,完成 runner 安裝並設為開機自動啟動。

1. 取得 Runner 註冊 Token

到 GitHub repo 頁面取得一次性 token(有效期約 1 小時):

https://github.com/<owner>/<repo>/settings/actions/runners/new?arch=arm64

2. 安裝 Runner

# 建立工作目錄
mkdir -p ~/actions-runner && cd ~/actions-runner

# 取得最新版本號
curl -s https://api.github.com/repos/actions/runner/releases/latest   | grep -o '"tag_name": "v[^"]*"' | head -1

# 下載 ARM64 macOS 版(Intel 改用 x64)
RUNNER_VERSION=<版本號,例如 2.334.0>
curl -o actions-runner-osx-arm64-${RUNNER_VERSION}.tar.gz -L   https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-osx-arm64-${RUNNER_VERSION}.tar.gz
tar xzf ./actions-runner-osx-arm64-${RUNNER_VERSION}.tar.gz

# 設定(--unattended 避免互動提示)
./config.sh   --url https://github.com/<owner>/<repo>   --token <TOKEN>   --labels mac-mini   --name mac-mini   --unattended

# 安裝為 launchd service(開機自動啟動)
./svc.sh install
./svc.sh start

./svc.sh install 而非直接 ./run.sh 的原因:terminal 關掉後 run.sh 進程就死了;launchd service 會在背景常駐、開機自動啟動。

Service 管理指令

cd ~/actions-runner
./svc.sh status    # 確認狀態
./svc.sh stop      # 停止
./svc.sh start     # 啟動
./svc.sh uninstall # 移除(重設時先執行)

Docker PATH 問題修正

launchd service 啟動時不會讀取 ~/.zshrc,預設 PATH 裡沒有 /usr/local/bin,導致 runner 找不到 docker 指令。

~/actions-runner/.env 設定 runner 的執行環境 PATH:

echo 'PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin' > ~/actions-runner/.env

設定後重啟 service:

cd ~/actions-runner
./svc.sh stop && ./svc.sh start

日常維運

確認 runner 狀態有兩個方式:

GitHub 頁面https://github.com/<owner>/<repo>/settings/actions/runners):

  • Idle:等待 job
  • Active:正在執行
  • Offline:service 沒跑

Mac Mini 本機

cd ~/actions-runner && ./svc.sh status
tail -f ~/Library/Logs/actions.runner.<owner>-<repo>.mac-mini/Runner_*.log

確認部署是否成功:

ssh <mac-mini-ssh-alias>
docker ps                          # 確認容器狀態
cd <project-path>/node/app
git log --oneline -3               # 確認最新 commit

常見問題

Q:Runner 顯示 Offline 怎麼辦?到 Mac Mini 執行 cd ~/actions-runner && ./svc.sh status,確認 service 是否在跑。若 token 已過期需重新設定:先 ./svc.sh uninstall,再用新 token 執行 ./config.sh remove 和重新安裝流程。Q:部署後容器沒更新?確認 Workflow 的 working-directory 路徑正確,以及 git pull 有抓到最新 commit(git log --oneline -3)。Q:docker: command not found 錯誤?這是 PATH 問題,依照上方「Docker PATH 問題修正」步驟設定 ~/actions-runner/.env 即可解決。Q:可以用在 public repo 嗎?不建議。Self-hosted runner 上任何能開 PR 的人都能觸發執行任意程式碼,public repo 有安全風險。僅限 private repo 使用。

結語

這套架構的核心思路是「讓內網機器主動出去拉工作,而不是等外部連進來」。一旦 runner service 跑起來,整個部署流程就是全自動的:push 到 main → GitHub 通知 runner → runner 在本機執行 git pull + docker compose rebuild,完全不需要人工介入。

對於個人或小團隊的內網服務來說,這是成本最低、維護最簡單的 CI/CD 方案之一。

Read more

Gaze Guard 短影片旁白的 TTS 成本與隱私防護示意圖

短影片旁白的成本,其實比我想像低

我最近在做一支 Gaze Guard 的 20 秒產品介紹影片。 原本想說簡單做個動畫就好。 後來想說如果有配音,會感覺比較生動也比較能夠描述使用的情境和達成的效果。 這次我測了什麼 我測的是產品介紹影片用的英文旁白。 工具底層使用的是 Edge TTS,也就是 Microsoft Edge 的線上 Text-to-Speech 服務。常見的底層工具是 edge-tts。 這不是正式 Azure Speech API 的計費路徑。 在我這次的環境裡,它沒有要求 API key,也沒有回傳可見的 per-use billing。 所以這次測試本身,比較像是快速 prototype。 如果未來要正式商用、穩定大量產出,就要回頭看 Azure Speech、OpenAI TTS、ElevenLabs 這類正式付費方案。

By SteepingLogic
用 Cloudflare Tunnel,從外面連回本機 Hermes Dashboard

用 Cloudflare Tunnel,從外面連回本機 Hermes Dashboard

# 用 Cloudflare Tunnel,從外面連回本機 Hermes Dashboard 我有一台 local Hermes。 平常它就在自己的機器上跑。 可能是家裡的桌機。 可能是 NAS。 可能是一台一直開著的 Linux box。 也可能只是我自己的筆電。 問題是,人一離開那個網路,Hermes dashboard 就變成一個只存在於 localhost 的東西。 我想在外面打開它。 但我沒有 static IP。 沒有買網域。 也不想進路由器開 port forwarding。 因為那條路通常長這樣: * 找 public IP * 設定 NAT * 開 port * 處理防火牆 * 補 HTTPS * 設定憑證 * 再擔心 origin IP 被掃 非常友善。 所以這篇先走另一條路:

By SteepingLogic
Invoshot 開發日記|正向回饋

Invoshot 開發日記|正向回饋

想信很多人在開發的時候都常常會做一件事,那就是: 不斷地反覆思考,在腦袋內把流程跑過很多次,然後思考哪裡可以做得更好 不限坐在電腦前的時後,可能是在捷運上,在人行道上, 在馬桶上或是在洗澡的時候 🤪 我不斷地在想:AI 辨識完一張發票後,使用者接下來最需要的是什麼? 每個欄位都要核對嗎? 這麼多欄位真的不會漏看嗎? 所以我在 Invoshot 裡加了提醒功能。 辨識結果出來後,系統會把比較需要注意的地方標出來。 不是把所有不確定都丟給使用者,而是把他真正需要介入的那幾秒鐘留出來。 今天收到一則訊息: 今天用了有新增提醒功能很棒! 真的有變輕鬆 😎 看到這句的時候,我第一個想到的是: 原來那些只是我腦內風暴的優化項目,使用者是真的可以感受到差異的! 感覺又有動力了!

By SteepingLogic