# 指令成功了，版本沒動：symlink 才是那個真正的入口

- URL: https://justfly.idv.tw/%e6%8c%87%e4%bb%a4%e6%88%90%e5%8a%9f%e4%ba%86%ef%bc%8c%e7%89%88%e6%9c%ac%e6%b2%92%e5%8b%95%ef%bc%9asymlink-%e6%89%8d%e6%98%af%e9%82%a3%e5%80%8b%e7%9c%9f%e6%ad%a3%e7%9a%84%e5%85%a5%e5%8f%a3/
- 日期: 2026-05-22
- 分類: WEB&amp;RIA
- 標籤: Linux, server, 神島

![指令成功了，版本沒動：symlink 才是那個真正的入口]

十台機器，九台跑升級指令，幾秒內版本號更新，確認完收工。第十台一樣的指令、一樣的回傳訊息，進去確認，版本還是舊的。

##### 技術環境

Node.js CLI 工具（`openclaw`），以 Docker 容器部署於 Linux 節點。Shell 命令解析機制：執行 `openclaw` 時，shell 依 `$PATH` 找到 `/usr/local/bin/openclaw`，這是一個在 Docker image build 時建立的 symlink，指向 image 內的 `/app/openclaw.mjs`。升級操作 `npm install -g` 修改的是 `/usr/local/lib/node_modules/` 路徑層，與 symlink 指向的 `/app/` 路徑完全平行，互不干涉。問題的本質與語言或框架無關，任何以 image 內建 symlink 作為命令解析入口的容器化部署方式，都會複現相同行為。

就像在超商的取件 App 上更新了聯絡資訊，但包裹在出廠時就貼好舊標籤了。App 改了沒用，要回到貼標籤的那一關才算數。這台機器的問題就是這個結構。

##### symlink 是入口，不是捷徑

進容器查，`/usr/local/bin/` 底下那支執行檔不是普通的二進位。是一個 symlink，靜靜指向 `/app/openclaw.mjs`，一支在 Docker image build 時就打包進去的 `.mjs` 檔。

`npm install -g` 確實把新版本裝進了 `/usr/local/lib/node_modules/`。`package.json` 顯示新版，指令回傳成功。但 shell 在解析指令時，先找到的是 `/usr/local/bin/openclaw`，那個 symlink 還指著 `/app/`，新裝的版本根本沒有被執行的機會。

容器從 image 啟動，in-container 裝的東西不持久。就算再跑一次 `force-recreate`，容器回到 image 的狀態，`npm install` 的結果一起消失。問題不在指令，在 image 本身。

##### 升級路徑分叉（時序）

```
Engineer               Shell               npm Layer            Image Layer
   |                     |                     |                     |
   |-- npm install -g -->|                     |                     |
   |                     |-- writes node_mods ->|                    |
   |                     | /app/openclaw.mjs (image 版)
   ||                     |                     |
   |                     |-- 容器回 image 狀態->-- node_modules 清除 x

npm layer: 更新成功 v  /  shell 解析入口: 仍是 image 內建版本 x
```

關鍵分叉點：`npm install -g` 的成功訊息來自 npm 層，但命令解析走的是 symlink 層，兩者在容器化環境中是互不知情的平行軌道。

##### 容易誤判的節點

其他九台裸機的升級流程是對的，`npm install -g` 改的就是 shell 實際會找到的那支二進位，路徑一致，沒有 symlink 隔層。這台容器化的機器表面行為完全相同，回傳訊息一樣乾淨，所以第一時間不會懷疑路徑結構有差異。

版本驗證腳本如果查的是 `package.json` 而不是執行 `--version`，也會回傳正確版號，問題繼續被掩蓋。過去有段時間升級一直是「假成功」的狀態，`npm` 世界裡版本確實更新了，image 世界毫無反應，兩條線平行跑，直到某次 `container recreate` 把 `npm` 裝的東西洗掉，才暴露出來。

##### 確認方式

進容器，跑 `ls -la /usr/local/bin/openclaw`。如果輸出是 `symlink → /app/openclaw.mjs`，這台機器的升級路徑就跟其他裸機不同。再跑 `openclaw --version` 跟 `cat /app/openclaw.mjs | head -1` 比對版本號，一旦對不上，原因就確認了。

正確路徑是：checkout 目標版本 tag、重新 build Docker image、`force-recreate` 容器。每次程式本體有變動，都要走這條路。沒有捷徑，也不應該有。

##### Code 對照：修法前後

**修法前：in-container 升級（假成功）**

```
# 進容器執行——看似成功，實際指向 image 舊版
docker exec -it openclaw-node bash
npm install -g openclaw@latest   #  /usr/local/bin/openclaw -> /app/openclaw.mjs  — symlink 仍指向 image 版本
```

**修法後：重建 image（正確路徑）**

```
# 在宿主機執行，不進容器
git checkout v1.2.3                    # |                     |                     |
   |                     |-- writes node_mods ->|                    |
   |                     | /app/openclaw.mjs (image ver)
   ||                     |                     |
   |                     |-- reset to image -->-- node_modules wiped x

npm layer: updated v  /  shell resolution entry: still image-baked version x
```

The critical fork: `npm install -g` succeeds in the npm layer. But command resolution travels through the symlink layer. In a containerized environment, these two layers have no awareness of each other.

##### Why it’s easy to miss

The nine bare-metal machines use the same upgrade flow, and it’s correct for them — `npm install -g` replaces exactly the binary the shell will find. No symlink indirection. This container looks identical from the outside: same command, same clean return code, same apparent success.

If your verification script checks `package.json` instead of running `--version`, it returns the right version number and the problem stays hidden. For a stretch, upgrades were silently succeeding in the npm layer while the image layer sat untouched — two parallel worlds, until a container recreate wiped the npm state and the discrepancy finally surfaced.

##### How to confirm it

Inside the container: `ls -la /usr/local/bin/openclaw`. If the output shows `symlink → /app/openclaw.mjs`, this machine has a different upgrade path from every bare-metal node. Cross-check `openclaw --version` against the version string in `/app/openclaw.mjs`. If they diverge, the diagnosis is confirmed.

The correct upgrade path: checkout the target version tag, rebuild the Docker image, `force-recreate` the container. Every time the program itself changes, that’s the path. No shortcut exists, and the environment doesn’t forgive assuming one does.

##### Code Diff: Before and After

**Before: in-container upgrade (silent failure)**

```
# Run inside the container -- looks like success, symlink unchanged
docker exec -it openclaw-node bash
npm install -g openclaw@latest   #  /usr/local/bin/openclaw -> /app/openclaw.mjs  -- symlink still points at image version
```

**After: rebuild the image (correct path)**

```
# Run on the host, not inside the container
git checkout v1.2.3                    #
