Community程式設計與開發github.com

shared-server-docker-deploy

在已託管其他應用程式的 Docker 伺服器上部署 Node 應用程式(含原生相依性或 Chromium),不影響現有服務。涵蓋:建構前釋放磁碟空間、每個服務的記憶體限制、僅 HTTP 服務的健康檢查、根據 Node 版本的原生模組相容性、透過共享 Caddy 代理並帶 Basic Auth 發布,以及無停機時間的代理驗證與重新載入。

shared-server-docker-deploy 是什麼?

此技能專為在共享 Docker 伺服器上部署 Node.js 應用程式而設計,該伺服器已運行其他專案。它解決了在非專用環境中部署的關鍵挑戰,例如在建構 Chromium 應用程式時避免磁碟空間不足、防止因原生模組版本不符(如 `better-sqlite3`)導致的程序崩潰,以及確保新的服務容器不會因 `EADDRINUSE`、OOM 或磁碟滿載而影響其他正在運行的堆疊。技能提供了一套完整的步驟:部署前透過 `docker builder prune` 測量並釋放磁碟空間;將繁重的後台工作(如瀏覽器自動化)與 HTTP API 容器分離,並為每個容器設定精確的 `mem_limit`;建立多階段 Dockerfile 以妥善處理 Playwright 瀏覽器與原生模組;以及安全地將新服務整合到現有的共享 Caddy 反向代理中,包含 Basic Auth 設定與無停機時間的設定檔重新載入。此外,還涵蓋從 Windows 機器透過 SSH 管線化 `tar` 進行部署,以及最終的檢查清單以確保共享環境的穩定性。

相容平台~Claude Code~Codex CLI~Cursor
Part ofMobile
npx skills add https://github.com/leoarayas/agent-skills/tree/main/shared-server-docker-deploy

Installed? Explore more 程式設計與開發 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

在共享 Docker 伺服器上部署

使用時機

  • 伺服器非專用:已運行其他人或其他專案的部署。
  • 應用程式有原生相依性(better-sqlite3、sharp、canvas)或瀏覽器(Chromium)。
  • 已有反向代理(Caddy 或 nginx)發布多個網域。
  • 任何 EADDRINUSE、OOM 或磁碟空間不足都可能影響其他堆疊。

黃金法則:新應用程式不能成為其他服務崩潰的原因。 因此所有步驟都需預先規劃與測量。


0. 建構前:測量並釋放磁碟空間

使用 Chromium 建構會下載約 2 GB 的映像檔。如果磁碟使用率達 78%,建構會失敗或使伺服器處於危險邊緣,下一次的 prune 可能會刪除生產環境的映像檔。

ssh SERVIDOR 'docker system df; df -h /'

通常可以安全釋放的項目:

docker builder prune -af      # 建構快取(通常佔用最大空間)
docker image prune -af        # 未使用的映像檔
docker container prune -f     # 已停止的容器

前後都要計算容器數量,以確認沒有任何服務中斷:

docker ps -q | wc -l   # 記錄數字,之後進行比較

根據實際案例:磁碟使用率從 78% 降至 32%,且 41 個生產容器完好如初。


1. 原生模組與 Node 版本

為不同 major 版本的 Node 編譯的 .node 檔案不會給出明確的錯誤訊息:它會因內部 assert 而導致程序崩潰,並產生無用的原生堆疊追蹤。

典型症狀:

# node.exe[61084]: void __cdecl node::RemoveEnvironmentCleanupHook(Isolate *, CleanupHook, void *)
# Assertion failed: (env) != nullptr
----- Native stack trace -----

原因:better-sqlite3@11 搭配 Node 24。支援在 better-sqlite3@12 中引入。

規則:

  • 在整合前,先確認原生模組版本與 Node 的 major 版本相容。
  • 在建構用的 Dockerfile 中安裝工具鏈(python3 make g++),以便在不提供預編譯二進位檔時能即時編譯。
  • 執行環境中,不要假設需要工具鏈:最好使用較小的最終映像檔。
  • 如果模組被 workspace 中的多個套件引用,請在一個不執行其他任務的程序中測試:延遲的 GC 崩潰可能在數小時後才出現。

2. 將繁重程序與回應程序分離

將 Chromium 和原生模組放在與 HTTP 伺服器相同的程序中會導致崩潰,而且會在長時間工作中阻塞事件循環。

api     : HTTP + SSE,低記憶體,HTTP 健康檢查
worker  : 佇列 + 瀏覽器,高記憶體,無埠號,無 HTTP 健康檢查

兩者透過共享儲存(SQLite、Redis、Postgres)通訊。API 不需要知道 worker 是否存活。

健康檢查僅定義在提供 HTTP 服務的容器上。 在 worker 中繼承 Dockerfile 的健康檢查會使其永遠處於 unhealthy 狀態,因為它從不開啟埠號:

worker:
  healthcheck:
    disable: true

3. 限制每個服務的記憶體

api:
  mem_limit: 400m
worker:
  mem_limit: 1600m
  mem_reservation: 400m
  volumes:
    - /dev/shm:/dev/shm:rw,size=256m   # Chromium 使用 /dev/shm;64 MB 會使其崩潰
  tmpfs:
    - /tmp:size=256m
  security_opt:
    - no-new-privileges:true

此外,在程式碼中減少工作量:爬蟲在伺服器上的 DETAIL_MAX / MAX_PAGES 應該比本機低。如果伺服器哪天 RAM 不足,罪魁禍首幾乎總是那個數字,而不是 Docker。

在實際操作前後進行測量:

docker stats --no-stream --format '{{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}' <api> <worker>
free -m | head -2

4. 建構:確實有效的階段與快取

FROM mcr.microsoft.com/playwright:v1.63.0-noble AS build   # 版本鎖定至 npm 套件版本
...
FROM mcr.microsoft.com/playwright:v1.63.0-noble AS runtime
  • 鎖定映像檔版本為已安裝的 npm 套件版本。如果兩者不一致,瀏覽器不會在 Playwright 預期的地方。
  • 先複製 package.json,再複製程式碼,使 COPY . . 不會使 install 層失效。
  • 在 runtime 中,如果啟動腳本使用 pnpm/npm,則需要它們 → 使用 corepack enable。
  • 如果 node_modules 從 build 階段複製,且權限不符,pnpm 會在每次啟動時嘗試「修復」它並失敗。可透過在映像檔的 .npmrc 中設定 verify-deps-before-run=false 來停用。
  • runtime 使用非 root 使用者。Playwright 映像檔已包含 pwuser。

pnpm-lock.yaml 必須提交,且 build 階段的 install 應使用 --frozen-lockfile。


5. 透過現有代理發布

如果已有 Caddy 發布多個網域,不要啟動另一個代理或開啟另一個公開埠號:只需在現有的 Caddyfile 中新增一個區塊,並連接到 Docker 網路。

services:
  api:
    networks: [edge_proxy]        # 外部網路,已建立
networks:
  edge_proxy:
    external: true
    name: edge_proxy

找到網路和 Caddyfile 的位置:

docker inspect <proxy> --format '{{range .Mounts}}{{.Source}} -> {{.Destination}} rw={{.RW}}{{println}}{{end}}'

在主機上編輯檔案:如果 bind mount 為 rw=false,在容器內寫入會失敗(Resource busy)或在重啟後遺失。

安全流程(切勿盲目編輯):

cp "$CADDYFILE" "$CADDYFILE.bak-$(date +%Y%m%d-%H%M%S)"   # 1. 備份
# 2. 新增區塊
docker exec <proxy> caddy validate --config /etc/caddy/Caddyfile  # 3. 驗證
docker exec <proxy> caddy reload  --config /etc/caddy/Caddyfile   # 4. 重新載入(無停機時間)

如果 validate 失敗,則還原備份。重新載入不會中斷現有連線。

Basic Auth

prospector.ejemplo.dev {
	basic_auth {
		leo $2a$14$...        # bcrypt,絕不能使用明文
	}
	reverse_proxy api:3000 {
		flush_interval -1     # SSE 必須設定
	}
	encode zstd gzip
	header {
		X-Frame-Options DENY
		-Server
	}
}
  • 在伺服器上產生 hash:docker exec <proxy> caddy hash-password --plaintext '...'。
  • 僅將明文密碼儲存在 root 檔案中(/root/<app>-password.txt,權限 600),以便日後恢復或輪換,絕不能提交到儲存庫。
  • 如果應用程式使用 SSE,不要在前端使用 Cloudflare 代理:100 秒超時和緩衝會中斷進度。僅使用 DNS(灰雲)讓來源伺服器提供真實憑證。
  • basic_auth 僅保護通過代理的流量。如果應用程式也透過其他埠號暴露,則不受保護:請記錄此點。

6. 從 Windows 部署

Windows 沒有 rsync。可行的跨平台方法是透過 SSH 管道傳輸 tar:

const tar = spawn('tar', ['-cf', '-', '--exclude', './node_modules', ...excludes, '.'], { cwd: ROOT });
const ssh = spawn('ssh', [HOST, `mkdir -p ${DIR} && tar -xf - -C ${DIR}`], { stdio: ['pipe', 'inherit', 'inherit'] });
tar.stdout.pipe(ssh.stdin);

在 Windows 上,spawn 必須設定 shell: true 才能解析 pnpm(它是 .cmd 檔案),但這樣做無法傳遞檔案 handle 來重新導向輸出:請使用環境變數 LOG_FILE,讓應用程式自行寫入日誌。


7. 操作共享伺服器前

  • 記錄 docker ps -q | wc -l,以便最後進行比較
  • 釋放後有足夠的可用磁碟空間
  • 所有新服務均已設定 mem_limit
  • 繁重程序位於自己的容器中,且無埠號
  • 健康檢查僅設定在提供 HTTP 服務的容器上
  • 宣告使用代理的外部網路,而非建立新網路
  • 備份 Caddyfile,並在重新載入前執行 caddy validate
  • 日誌寫入檔案(程序將無控制台)
  • 部署腳本具備冪等性:check / push / build / up / logs / stats / backup
  • 備份的 volume 已測試且已還原,而不僅僅是建立

快速診斷

docker stats --no-stream --format '{{.Name}} {{.MemUsage}} {{.CPUPerc}}' <api> <worker>
docker logs --tail 80 <worker>
docker exec <api> node -e "fetch('http://127.0.0.1:3000/api/health').then(r=>r.text()).then(console.log)"
free -m && df -h /

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

相關技能