先花五分鐘建立正確的心智模型,再一步步把舊的 Codex Docker 目錄搬移、改名為 codex-claude,用同一份 Dockerfile 裝好兩套 CLI,讓 Codex 走 7681、Claude 走 7682,各自保留登入狀態,並用一個受限的小服務安全地控制其他專案。
1. 這篇文章要幫你完成什麼
你原本已經有一個能用瀏覽器操作 Codex CLI 的 Docker 環境(放在 NAS 的 /volume1/docker/codex)。現在你想在同一套環境裡再加上 Claude Code,而且:
- 兩套 CLI 各自有獨立的網頁終端,不會互搶畫面;
- 各自的登入狀態、設定、對話紀錄互不干擾;
- 維護成本低——升級系統套件或 CLI 時只要改一個地方;
- 順便把舊目錄改成更貼切的名字
codex-claude,並留好備份可隨時回復。
2. 先建立正確的心智模型
初學者最常見的誤解是:「一個 Docker 跑兩個 CLI」是把 Codex 和 Claude 塞進同一個終端輪流用。不是這樣。
正確的理解是——一份 Dockerfile(食譜)→ 一個 Docker 映像(做好的半成品)→ 由 Compose 啟動成多個容器(服務),每個容器只跑一件事。
Node.js 22+git+tmux
ttyd+ripgrep
Codex CLI+Claude Code
同一份映像,啟動成 3 個容器
codex-web
- Port 7681
- tmux:codex
- 設定:
.codex
http://NAS:7681claude-web
- Port 7682
- tmux:claude
- 設定:
.claude
http://NAS:7682project-helper
- 無對外 Port
- 只有它能碰
docker.sock
project-control三個重點:
- 映像只有一份。Codex 和 Claude 裝在同一份映像裡,所以「升級」只發生在一個 Dockerfile。
- 容器彼此隔離。
codex-web和claude-web是兩個獨立行程,各自綁不同的連接埠、不同的 tmux session、不同的設定資料夾。它們天生不會衝突。 - Docker 控制權只給一個「守門員」。AI CLI 不應該直接碰
docker.sock(等於整台主機的 root)。我們把控制權交給一個只會做五件事、只認白名單專案的小服務project-helper。這是進階但很重要的一環,第 9 節會說明;剛入門可以先跳過。
3. 名詞速查表
看到不懂的字先回來這裡查,不用死背。
- Docker 映像 (image)
- 一個「做好的環境快照」,裡面已經裝好作業系統套件和程式。由
Dockerfile建置而來。 - 容器 (container)
- 把映像實際跑起來的「一個執行中的行程」。同一份映像可以同時跑很多容器。
- Docker Compose
- 用一個
compose.yaml檔描述「我要哪幾個容器、各自的連接埠與資料夾」,然後一鍵啟動全部。文中每個容器稱為一個服務 (service)。 - Volume(掛載)
- 把 NAS 上的一個資料夾「接進」容器裡的某個路徑。容器重建後資料還在 NAS,所以登入狀態不會不見。寫法是
主機路徑:容器內路徑。 - Port(連接埠)對應
- 寫法
7682:7682,左邊是「你用瀏覽器連的主機埠」,右邊是「容器裡程式監聽的埠」。左邊可以隨意改,右邊我們固定。 - ttyd
- 一個小程式,把「一個終端指令」變成網頁。打開網址就等於打開那個終端。
ttyd -W-W= 允許在網頁裡「輸入」(可寫)。它不是密碼保護,只是打開鍵盤輸入權限。- tmux
- 終端多工器。這裡的用途是:即使瀏覽器斷線,CLI 仍在 tmux 裡繼續活著,重新整理網頁就接回原本的畫面。
- UID
- Linux 使用者的數字編號。NAS 上資料夾的擁有者 UID 必須和容器裡的使用者 UID 一致,容器才寫得進去。本文用
1026。
4. 最終的目錄結構長什麼樣
先看終點,過程就不會迷路。全部檔案都在 NAS 的 /volume1/docker/codex-claude/:
codex-claude/
├── Dockerfile # 一份食譜,裝 Codex + Claude + 系統工具
├── compose.yaml # 描述 codex-web / claude-web / project-helper 三個服務
├── .env # 連接埠、UID、helper 的密鑰(不進 Git)
├── .gitignore # 忽略 .env 與兩個設定目錄
├── project-control # 容器內的友善指令(project-restart blog 等)
├── home/ # Codex 的設定與登入(掛到容器的 ~/.codex)
│ └── .gitkeep
├── claude-home/ # Claude 的設定與登入(掛到容器的 ~/.claude)
│ └── .gitkeep
└── project-helper/ # 受限的 Docker 控制服務
├── Dockerfile
└── app.py
| 項目 | Codex | Claude |
|---|---|---|
| Compose 服務名稱 | codex-web |
claude-web |
| 容器內 ttyd Port(固定) | 7681 | 7682 |
| 主機 Port(可在 .env 改) | TTYD_PORT,預設 7681 |
CLAUDE_TTYD_PORT,預設 7682 |
| tmux session 名稱 | codex |
claude |
| NAS 上的設定目錄 | ./home |
./claude-home |
| 容器內設定路徑 | /home/codexuser/.codex |
/home/codexuser/.claude |
| 登入資料檔 | auth.json |
.credentials.json |
5. 步驟 1搬移前:備份舊 Codex 目錄
以下都以 Synology NAS 為例:舊目錄 /volume1/docker/codex,新目錄 /volume1/docker/codex-claude。先看看目前狀態、把展開後的設定存一份,方便之後比對:
cd /volume1/docker/codex
sudo docker compose ps
sudo docker compose config > /tmp/codex-compose-resolved.yaml
接著做一份完整、命名清楚的備份:
cd /volume1/docker
sudo cp -a codex codex-backup-before-claude
cp -a 會連同 home/ 裡的登入 token 一起複製。這份備份不要放進任何公開 Git 倉庫,也不要上傳到雲端相簿之類的地方。先用 df -h /volume1 確認空間夠。6. 步驟 2停止服務、搬移並改名
一定要先在舊目錄把 Compose 停掉,再改名。否則執行中的容器還記著舊路徑,會出現各種奇怪錯誤。
cd /volume1/docker/codex
sudo docker compose down
cd /volume1/docker
sudo mv codex codex-claude
cd /volume1/docker/codex-claude
改名後,設定檔裡可能還殘留舊的絕對路徑。用 rg(ripgrep)搜出來,但先排除兩個設定目錄,避免被登入紀錄干擾:
rg -n '/volume1/docker/codex([/" ]|$)' . \
--glob '!home/**' --glob '!claude-home/**'
把「確定是專案路徑」的那幾行改成 /volume1/docker/codex-claude。
@openai/codex、容器裡的 .codex 設定目錄、tmux session 名稱 codex ——這些字裡有 codex,但不是舊專案路徑,動到就壞了。改名後必做:刪除舊容器,再用新路徑重新建立
Docker 在建立容器當下就把 bind mount 的主機來源路徑記進容器設定。資料夾從 codex 改名為 codex-claude 後,單純執行 docker restart 只會重啟同一個舊容器,不會把掛載來源自動改成新路徑。因此舊容器必須刪除,再由新目錄裡的 Compose 建立新容器。
前面的 docker compose down 正常情況下已經會刪除該 Compose 專案的容器。為避免舊版曾用不同專案名稱、手動 docker run,或先前只做過 stop 而留下殘骸,改名後再檢查一次:
# 先列出可能殘留的舊容器;確認名稱後才刪除
sudo docker ps -a --filter name=codex-web \
--filter name=codex-cli \
--filter name=codex-project-helper
# 只刪除確認屬於舊架構的容器;不存在時出現 No such container 可忽略
sudo docker rm -f codex-web codex-cli codex-project-helper
docker rm -f 會強制停止並刪除指定容器,但不會刪除本文使用的主機資料夾。若你的容器名稱不同,請以 docker ps -a 顯示的舊容器名稱為準,不要照抄不相符的名稱。完成 Compose 與路徑修改後,要用 --force-recreate 從新目錄建立容器,讓 bind mount 真正指向 /volume1/docker/codex-claude:
cd /volume1/docker/codex-claude
sudo docker compose config --quiet
sudo docker compose up -d --build --force-recreate
最後直接查看新容器記錄的掛載來源,不只確認網頁「看起來能開」:
sudo docker inspect codex-web \
--format '{{range .Mounts}}{{println .Source "→" .Destination}}{{end}}'
sudo docker inspect codex-claude-claude-web \
--format '{{range .Mounts}}{{println .Source "→" .Destination}}{{end}}'
輸出中的專案來源應是 /volume1/docker/codex-claude,不能再出現舊的 /volume1/docker/codex。
7. 步驟 3一份 Dockerfile 裝兩套 CLI
整個共存架構的核心,就是這份 Dockerfile 的 npm 安裝段——同時裝 OpenAI Codex CLI 與 Anthropic Claude Code:
FROM node:22-bookworm-slim
ARG CODEX_UID=1026
ENV DEBIAN_FRONTEND=noninteractive \
LANG=C.UTF-8 LC_ALL=C.UTF-8 \
HOME=/home/codexuser TERM=xterm-256color
# 1) 系統工具:兩套 CLI 都會用到
RUN apt-get update && apt-get install -y --no-install-recommends \
bash bubblewrap ca-certificates curl git less \
openssh-client procps ripgrep tmux \
&& rm -rf /var/lib/apt/lists/*
# 2) 一次裝好兩套 AI CLI(本教學用最新版;正式環境建議釘版本)
RUN npm install -g \
@openai/codex \
@anthropic-ai/claude-code \
&& npm cache clean --force
# 3) ttyd:把終端變成網頁
RUN curl -fsSL \
https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.x86_64 \
-o /usr/local/bin/ttyd \
&& chmod 0755 /usr/local/bin/ttyd
# 4) 建立與 NAS 目錄同 UID 的非 root 使用者
RUN useradd --uid "${CODEX_UID}" --create-home --shell /bin/bash codexuser \
&& mkdir -p /home/codexuser/.codex /workspace \
&& chown -R codexuser:codexuser /home/codexuser /workspace
USER codexuser
WORKDIR /workspace
CMD ["bash"]
因為兩個服務都 build 自這份 Dockerfile,升級系統工具或任一 CLI 只需要改這一處,再重新 build。這就是「一份映像」帶來的維護紅利。
@openai/codex、@anthropic-ai/claude-code 釘在測試過的版本號,確認沒問題後再升版重 build。project-control:若你要做完整版,Dockerfile 尾端還會 COPY project-control 進映像並建立幾個別名(project-restart 等)。第 9 節會補上這段。8. 步驟 4最小可行版:Compose 兩個服務
先把「能用」做出來。這個版本只有 codex-web 和 claude-web,沒有 project-helper。對大多數人這樣就夠了。
name: codex-claude
services:
codex-web:
build:
context: .
dockerfile: Dockerfile
args:
CODEX_UID: "${CODEX_UID:-1026}"
container_name: codex-web
restart: unless-stopped
init: true
stdin_open: true
tty: true
working_dir: /workspace
environment:
LANG: C.UTF-8
LC_ALL: C.UTF-8
TERM: xterm-256color
HOME: /home/codexuser
ports:
- "${TTYD_PORT:-7681}:7681"
volumes:
- ./home:/home/codexuser/.codex
- /volume1/docker/codex-claude:/workspace/codex-claude
entrypoint:
- ttyd
- -W
- -p
- "7681"
- -t
- "fontSize=18"
- -t
- "rendererType=canvas"
- bash
- -lc
- 'exec tmux new-session -A -s codex "codex"'
healthcheck:
test: ["CMD-SHELL", "curl -sS -o /dev/null http://127.0.0.1:7681 || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
claude-web:
build:
context: .
dockerfile: Dockerfile
args:
CODEX_UID: "${CODEX_UID:-1026}"
container_name: codex-claude-claude-web
restart: unless-stopped
init: true
stdin_open: true
tty: true
working_dir: /workspace
environment:
LANG: C.UTF-8
LC_ALL: C.UTF-8
TERM: xterm-256color
HOME: /home/codexuser
ports:
- "${CLAUDE_TTYD_PORT:-7682}:7682"
volumes:
- ./claude-home:/home/codexuser/.claude
- /volume1/docker/codex-claude:/workspace/codex-claude
entrypoint:
- ttyd
- -W
- -p
- "7682"
- -t
- "fontSize=18"
- -t
- "rendererType=canvas"
- bash
- -lc
- 'exec tmux new-session -A -s claude "claude"'
healthcheck:
test: ["CMD-SHELL", "curl -sS -o /dev/null http://127.0.0.1:7682 || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
逐段看懂這份設定
build.args.CODEX_UID- 把
.env的 UID 傳進 Dockerfile,讓容器使用者和 NAS 資料夾擁有者一致。 init: true/stdin_open/tty- 互動式 CLI 需要一個「真的終端」。這三個讓容器提供 TTY 並正確處理 Ctrl+C。
ports- 兩個服務左邊主機埠不同(7681 / 7682),右邊容器埠也不同,所以同時開兩個網頁不會撞。
volumes第一行- 關鍵的隔離:Codex 掛
./home→.codex,Claude 掛./claude-home→.claude。各自獨立,這是雙 CLI 能穩定共存的根本原因。 volumes第二行- 把專案本身掛進
/workspace/codex-claude,方便你用 AI 直接改這套設定。要讓兩套 CLI 看到更多程式碼,就在兩個服務都各加一行明確的掛載,例如- /volume1/docker/blog:/workspace/blog。 entrypoint最後一行tmux new-session -A -s codex "codex":開一個叫codex的 tmux session 跑codex;-A代表「已存在就接回去」。所以瀏覽器重整或短暫斷線後,CLI 不會消失。healthcheck- Docker 每 30 秒 curl 一次自己的 ttyd 埠,失敗就把容器標記為 unhealthy,方便你用
docker compose ps一眼看出哪個服務掛了。
volumes 明確列出。把 /volume1 或 / 直接交給容器,等於把整台 NAS 的讀寫權交給 AI CLI。9. 步驟 4+完整版:加入受限的 project-helper
如果你想在 Codex / Claude 裡直接下指令去重啟或重建其他 Compose 專案(例如「幫我 rebuild blog」),那 CLI 就需要 Docker 控制權。直接把 /var/run/docker.sock 掛進 AI 容器很危險——那等於給了整台主機的 root。
解法:多一個只會做五件事、只認白名單的守門員服務。它不對外開埠,只有 Compose 內部網路上的 codex-web / claude-web 連得到。
9-1 project-helper/app.py(守門員本體)
一支極小的 Flask 程式:驗證 Bearer token → 檢查專案在白名單 → 只允許 status / restart / up / rebuild / logs → 在該專案目錄執行對應的 docker compose 指令。
import os, subprocess
from flask import Flask, request, jsonify
app = Flask(__name__)
TOKEN = os.environ["PROJECT_HELPER_TOKEN"]
PROJECTS = {
"blog": "/projects/blog",
"umami": "/projects/umami",
"codex-claude": "/projects/codex-claude",
# …只放你允許被控制的專案
}
ACTIONS = {
"restart": ["docker", "compose", "restart"],
"up": ["docker", "compose", "up", "-d"],
"rebuild": ["docker", "compose", "up", "-d", "--build"],
"status": ["docker", "compose", "ps"],
"logs": ["docker", "compose", "logs", "--tail", "200"],
}
@app.post("/action")
def action():
if request.headers.get("Authorization", "") != f"Bearer {TOKEN}":
return jsonify(error="unauthorized"), 401
data = request.get_json(silent=True) or {}
project, name = data.get("project", ""), data.get("action", "")
if project not in PROJECTS:
return jsonify(error="project not allowed", allowed=sorted(PROJECTS)), 400
if name not in ACTIONS:
return jsonify(error="action not allowed", allowed=sorted(ACTIONS)), 400
p = subprocess.run(ACTIONS[name], cwd=PROJECTS[project],
text=True, capture_output=True,
timeout=1800 if name in {"rebuild", "up"} else 120)
return jsonify(ok=(p.returncode == 0), project=project, action=name,
stdout=p.stdout[-20000:], stderr=p.stderr[-20000:]), \
(200 if p.returncode == 0 else 500)
9-2 project-helper/Dockerfile
FROM docker:27-cli
RUN apk add --no-cache python3 py3-pip \
&& pip3 install --break-system-packages --no-cache-dir flask gunicorn
WORKDIR /app
COPY app.py /app/app.py
EXPOSE 8080
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "1", "--threads", "4", "app:app"]
9-3 project-control(放進主 Dockerfile 的友善指令)
在第 7 節的 Dockerfile USER codexuser 之前加入:
COPY project-control /usr/local/bin/project-control
RUN chmod 0755 /usr/local/bin/project-control \
&& for a in restart up rebuild status logs; do \
ln -s /usr/local/bin/project-control /usr/local/bin/project-$a; \
done
project-control 本身是一支小 shell script,把 project-restart blog 轉成一個帶 token 的 HTTP 請求送給 helper:
#!/bin/sh
set -eu
PROG="$(basename "$0")"
case "$PROG" in
project-restart) set -- restart "${1:-}" ;;
project-up) set -- up "${1:-}" ;;
project-rebuild) set -- rebuild "${1:-}" ;;
project-status) set -- status "${1:-}" ;;
project-logs) set -- logs "${1:-}" ;;
esac
ACTION="${1:-}"; PROJECT="${2:-}"
URL="${PROJECT_HELPER_URL:-http://project-helper:8080}"
TOKEN="${PROJECT_HELPER_TOKEN:?PROJECT_HELPER_TOKEN is not set}"
curl -fsS -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data "{\"action\":\"$ACTION\",\"project\":\"$PROJECT\"}" \
"$URL/action"
echo
9-4 在 compose.yaml 加入 helper,並讓兩個 CLI 服務認得它
在 codex-web 和 claude-web 的 environment 各加兩行,並加上 depends_on:
environment:
# …原本的 LANG / TERM / HOME …
PROJECT_HELPER_URL: http://project-helper:8080
PROJECT_HELPER_TOKEN: "${PROJECT_HELPER_TOKEN}"
depends_on:
- project-helper
然後新增第三個服務:
project-helper:
build:
context: ./project-helper
dockerfile: Dockerfile
container_name: codex-project-helper
restart: unless-stopped
environment:
PROJECT_HELPER_TOKEN: "${PROJECT_HELPER_TOKEN}"
volumes:
# 只有守門員拿到 Docker 控制權
- /var/run/docker.sock:/var/run/docker.sock
# 只掛你允許被控制的 Compose 專案目錄
- /volume1/docker/blog:/projects/blog
- /volume1/docker/umami:/projects/umami
- /volume1/docker/codex-claude:/projects/codex-claude
# 不對外開 port:只有這個 Compose 網路上的服務連得到
expose:
- "8080"
/var/run/docker.sock 掛進 codex-web 或 claude-web。整個設計的意義就是:Docker 控制權只存在於 project-helper 這一個容器,而且它被 token、白名單專案、白名單動作三重限制。10. 步驟 5Port 設計與 .env
容器裡的 ttyd 埠固定是 7681 / 7682;你能自由更動的是「主機端」的埠。全部集中寫在 .env:
CODEX_UID=1026
TTYD_PORT=7681
CLAUDE_TTYD_PORT=7682
# 用 openssl rand -hex 32 產生一長串隨機值貼在這裡(完整版才需要)
PROJECT_HELPER_TOKEN=請換成你自己的隨機字串
假設 NAS 上 7682 已經被別的服務佔用,你只要把 CLAUDE_TTYD_PORT 改成沒被用的埠(例如 8765),然後 docker compose up -d。容器內的 ttyd 仍然是 7682,不用動 compose.yaml。
產生 helper 密鑰:
openssl rand -hex 32
.env 一定要進 .gitignore。裡面的 PROJECT_HELPER_TOKEN 一旦外洩,任何能連到 helper 的人都能重建你的專案。11. 步驟 6登入資料分離與持久化
雙 CLI 能穩定共存的第二個要點:兩套認證資料夾完全分開。
./home → /home/codexuser/.codex (Codex:auth.json)
./claude-home → /home/codexuser/.claude (Claude:.credentials.json)
先建好目錄,並把擁有者設成和 CODEX_UID 一致,容器才寫得進去:
cd /volume1/docker/codex-claude
mkdir -p home claude-home
sudo chown -R 1026:1026 home claude-home
touch home/.gitkeep claude-home/.gitkeep
對應的 .gitignore(兩個設定目錄都要忽略):
.env
home/*
!home/.gitkeep
claude-home/*
!claude-home/.gitkeep
這次踩到的坑:Claude 的 ~/.claude.json 在 .claude/ 外面。Claude Code 會把「專案信任狀態、初次設定、MCP 設定」寫在 /home/codexuser/.claude.json,而我們只掛了 .claude/ 目錄。症狀是:每次 --build 重建後,Claude 又要你重新確認信任這個資料夾。若想連這個也保留,先建好檔案再多掛一行:
# 主機端先建立一個空 JSON,避免 Docker 把它當資料夾掛
touch /volume1/docker/codex-claude/claude-home/dot-claude.json
sudo chown 1026:1026 /volume1/docker/codex-claude/claude-home/dot-claude.json
然後在 claude-web 的 volumes 加:
- ./claude-home/dot-claude.json:/home/codexuser/.claude.json
Codex 對應的檔案(~/.codex/ 內的 config.toml、auth.json)都在掛載目錄裡,沒有這個問題。
home/、claude-home/、.env 都要在 .gitignore;也不要在文章、截圖或貼除錯輸出時露出 token。12. 步驟 7建置並啟動
先讓 Compose 檢查 YAML 語法,再 build 並啟動:
cd /volume1/docker/codex-claude
sudo docker compose config --quiet # 沒輸出就是語法正確
sudo docker compose up -d --build --force-recreate
sudo docker compose ps
第一次 build 會下載基礎映像和 npm 套件,需要幾分鐘。完成後開瀏覽器:
- Codex:
http://NAS-IP:7681 - Claude:
http://NAS-IP:7682(或你在.env改的埠)
docker compose ps 裡 codex-web、claude-web(有做完整版還有 codex-project-helper)狀態都是 Up,且前兩者是 (healthy)。兩個網址都能打開看到各自的 CLI 歡迎畫面。13. 驗證、首次登入與日常操作
確認兩套 CLI 都在
sudo docker compose exec codex-web codex --version
sudo docker compose exec claude-web claude --version
確認健康狀態與埠
sudo docker compose ps
curl -I http://127.0.0.1:7681
curl -I http://127.0.0.1:7682
首次登入
各自打開網頁終端,依 CLI 畫面完成官方登入/授權。登入結果會分別寫進 ./home/auth.json 與 ./claude-home/.credentials.json,之後重建容器不用重登(.claude.json 的例外見第 11 節)。

看日誌、重啟單一服務
sudo docker compose logs --tail 100 codex-web
sudo docker compose logs --tail 100 claude-web
# 只重啟 Claude,不打擾正在工作的 Codex
sudo docker compose restart claude-web
# 只重建 Claude(例如你改了 Dockerfile 想升級)
sudo docker compose up -d --build claude-web
完整版:在 CLI 裡控制其他專案
在 Codex 或 Claude 的終端裡:
project-status blog
project-logs blog
project-restart blog
project-rebuild blog
這些指令會透過 project-helper 執行,且只對白名單專案有效。
14. 安全性:ttyd -W 不等於登入保護
-W 只是「允許在網頁裡打字」,沒有任何密碼驗證。把 7681/7682 直接開到公網,等於把一個能執行任意指令的終端送給全世界。
- 只在可信任的 LAN、VPN 或 Tailscale 網路裡使用。
- 需要外網存取時,前面放反向代理,加上 HTTPS 與可靠的身分驗證(不是 ttyd 內建的 basic auth 就好,建議用代理層的 SSO 或至少強密碼)。
- 防火牆只放行必要來源,不要做無限制的 Port Forwarding。
- 容器用非 root 使用者(
codexuser),只掛兩套 CLI 真正需要的專案目錄。 - Docker 控制權只給
project-helper,並用 token+白名單專案+白名單動作三重把關。
bubblewrap 讓 Codex 能在容器內再做一層 sandbox。若你要放寬 Codex 的權限(例如 codex --sandbox danger-full-access),務必先確認上面的網路隔離都到位。15. 故障排除與安全回復
Claude 的網址(7682)打不開
sudo docker compose ps
sudo docker compose logs --tail 200 claude-web
sudo ss -lntp | grep ':7682'
若主機埠被別的程式佔用,改 .env 的 CLAUDE_TTYD_PORT 後 docker compose up -d。
設定目錄寫不進去 / 權限錯誤
幾乎都是 NAS 目錄擁有者和容器 UID 不一致。確認 .env 的 CODEX_UID,只針對兩個設定目錄修權限,不要對整個專案 chmod 777:
sudo chown -R 1026:1026 /volume1/docker/codex-claude/home \
/volume1/docker/codex-claude/claude-home
重建後 Claude 又要求信任資料夾
~/.claude.json 沒有被保留,見第 11 節的多掛一行做法。
搬移後容器掛到空目錄
檢查 compose.yaml 裡所有舊絕對路徑是否都更新成 codex-claude,再重建:
sudo docker compose down
sudo docker compose up -d --build
project-control 回 401 / 403
codex-web/claude-web 的 PROJECT_HELPER_TOKEN 和 project-helper 的不一致,或 .env 沒被讀到。三個服務要用同一個值;改完 .env 要 docker compose up -d 讓環境變數重新注入。
整組回復成原本的 Codex 環境
先停新服務,把有問題的新目錄留著除錯,再從備份還原:
cd /volume1/docker/codex-claude
sudo docker compose down
cd /volume1/docker
sudo mv codex-claude codex-claude-failed
sudo cp -a codex-backup-before-claude codex
cd codex
sudo docker compose up -d
codex 尚未存在。不要直接刪掉失敗版本——裡面可能有搬移後才產生、還沒備份的登入資料或設定變更。16. 完成檢查清單與架構總結
- 舊
codex目錄已備份為codex-backup-before-claude,並改名為codex-claude。 - Dockerfile 同時安裝
@openai/codex與@anthropic-ai/claude-code,兩服務共用這一份映像。 codex-web在 7681、claude-web在 7682,docker compose ps都是Up (healthy)。- Codex 用
./home、Claude 用./claude-home;重建容器後兩者都不用重新登入。 - (完整版)
project-helper沒有對外開埠,只有它掛docker.sock,且限定白名單專案與五個動作。 .env、home/、claude-home/都在.gitignore裡。- 外部存取由 VPN、防火牆或帶驗證的 HTTPS 反向代理保護,沒有裸奔到公網。
這套設計真正的重點不是「把兩個互動式 CLI 擠進一個終端」,而是:用同一份映像壓低維護成本,用服務隔離消除連接埠、tmux session 與認證的衝突,再用一個受限的守門員服務把危險的 Docker 控制權收斂到一處。日後要升級任一 CLI,只要改 Dockerfile、重新 build,再視需要重建單一服務即可——另一套完全不受影響。
這篇文章已有 22 次瀏覽
發佈留言