From 2c8409dcbb8593b2c6a76793875987a5a662d69e Mon Sep 17 00:00:00 2001 From: "CJACK." <155826701+CJackHwang@users.noreply.github.com> Date: Wed, 29 Apr 2026 13:46:22 +0800 Subject: [PATCH] fix docker defaults to writable /data config path and align docs --- API.en.md | 5 ++++- API.md | 5 ++++- README.MD | 1 + README.en.md | 1 + docker-compose.yml | 3 ++- docs/DEPLOY.en.md | 6 ++++++ docs/DEPLOY.md | 6 ++++++ internal/config/paths.go | 5 +++++ .../httpapi/admin/accounts/handler_accounts_testing.go | 10 ++++++---- 9 files changed, 35 insertions(+), 7 deletions(-) diff --git a/API.en.md b/API.en.md index ee1b73b..f2a9ad4 100644 --- a/API.en.md +++ b/API.en.md @@ -917,12 +917,15 @@ Updates proxy binding for a specific account. "message": "API test successful (session creation only)", "model": "deepseek-v4-flash", "session_count": 0, - "config_writable": true + "config_writable": true, + "config_warning": "" } ``` If a `message` is provided, `thinking` may also be included when the upstream response carries reasoning text. +When the configured file path is not writable (for example, read-only `/app/config.json` inside some containers), login/session testing still proceeds; `config_warning` is returned to indicate token persistence failed and the token is memory-only until restart. + ### `POST /admin/accounts/test-all` Optional request field: `model`. diff --git a/API.md b/API.md index 8fb77ba..9449e63 100644 --- a/API.md +++ b/API.md @@ -934,12 +934,15 @@ data: {"type":"message_stop"} "message": "API 测试成功(仅会话创建)", "model": "deepseek-v4-flash", "session_count": 0, - "config_writable": true + "config_writable": true, + "config_warning": "" } ``` 如果传入 `message`,还会附带 `thinking`(当上游返回思考内容时)。 +当部署环境配置文件路径不可写(例如容器内默认 `/app/config.json` 只读)时,登录与会话测试仍可继续;此时会返回 `config_warning` 提示 token 仅保存在内存、重启后丢失。 + ### `POST /admin/accounts/test-all` 可选请求字段:`model` diff --git a/README.MD b/README.MD index 7c20a7a..a958209 100644 --- a/README.MD +++ b/README.MD @@ -245,6 +245,7 @@ docker-compose logs -f ``` 默认 `docker-compose.yml` 会把宿主机 `6011` 映射到容器内的 `5001`。如果你希望直接对外暴露 `5001`,请设置 `DS2API_HOST_PORT=5001`(或者手动调整 `ports` 配置)。 +同时默认把 `./config.json` 挂载到容器 `/data/config.json`,并设置 `DS2API_CONFIG_PATH=/data/config.json`,用于避免 `/app` 只读导致运行时 token 持久化失败。 更新镜像:`docker-compose up -d --build` diff --git a/README.en.md b/README.en.md index 4484fcf..b404dcf 100644 --- a/README.en.md +++ b/README.en.md @@ -233,6 +233,7 @@ docker-compose up -d ``` The default `docker-compose.yml` uses `ghcr.io/cjackhwang/ds2api:latest` and maps host port `6011` to container port `5001`. If you want `5001` exposed directly, set `DS2API_HOST_PORT=5001` (or adjust the `ports` mapping). +It also mounts `./config.json` to `/data/config.json` and sets `DS2API_CONFIG_PATH=/data/config.json` by default, which avoids runtime token persistence failures caused by read-only `/app`. Rebuild after updates: `docker-compose up -d --build` diff --git a/docker-compose.yml b/docker-compose.yml index 9398fdc..571829a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,8 +9,9 @@ services: # Host port is configurable via DS2API_HOST_PORT; container port stays fixed at 5001. - "${DS2API_HOST_PORT:-6011}:5001" volumes: - - ./config.json:/app/config.json # 配置文件 + - ./config.json:/data/config.json # 配置文件(持久化推荐路径) environment: - TZ=Asia/Shanghai - LOG_LEVEL=INFO - DS2API_ADMIN_KEY=${DS2API_ADMIN_KEY:-ds2api} + - DS2API_CONFIG_PATH=/data/config.json diff --git a/docs/DEPLOY.en.md b/docs/DEPLOY.en.md index f81de01..e4d0734 100644 --- a/docs/DEPLOY.en.md +++ b/docs/DEPLOY.en.md @@ -130,6 +130,7 @@ docker-compose logs -f ``` The default `docker-compose.yml` directly uses `ghcr.io/cjackhwang/ds2api:latest` and maps host port `6011` to container port `5001`. If you want `5001` exposed directly, set `DS2API_HOST_PORT=5001` (or adjust the `ports` mapping). +The compose template also defaults to `DS2API_CONFIG_PATH=/data/config.json` with `./config.json:/data/config.json` mounted, so deployments avoid read-only `/app` persistence issues by default. If you want a pinned version instead of `latest`, you can also pull a specific tag directly: @@ -195,6 +196,11 @@ Notes: - **Port**: DS2API listens on `5001` by default; the template sets `PORT=5001`. - **Persistent config**: the template mounts `/data` and sets `DS2API_CONFIG_PATH=/data/config.json`. After importing config in Admin UI, it will be written and persisted to this path. +- **`open /app/config.json: permission denied`**: this means the instance is trying to persist runtime tokens to a read-only path (commonly `/app` inside the image). + Recommended handling: + 1. Set a writable path explicitly: `DS2API_CONFIG_PATH=/data/config.json` (and mount a persistent volume at `/data`); + 2. If you bootstrap with `DS2API_CONFIG_JSON` and do not need runtime writeback, keep env-backed mode (`DS2API_ENV_WRITEBACK` disabled); + 3. In current versions, login/session tests continue even if persistence fails; Admin API returns a warning that token persistence failed and token is memory-only until restart. - **Build version**: Zeabur / regular `docker build` does not require `BUILD_VERSION` by default. The image prefers that build arg when provided, and automatically falls back to the repo-root `VERSION` file when it is absent. - **First login**: after deployment, open `/admin` and login with `DS2API_ADMIN_KEY` shown in Zeabur env/template instructions (recommended: rotate to a strong secret after first login). diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 0f91fdf..e2efe61 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -130,6 +130,7 @@ docker-compose logs -f ``` 默认 `docker-compose.yml` 直接使用 `ghcr.io/cjackhwang/ds2api:latest`,并把宿主机 `6011` 映射到容器内的 `5001`。如果你希望直接对外暴露 `5001`,请设置 `DS2API_HOST_PORT=5001`(或者手动调整 `ports` 配置)。 +Compose 模板还会默认设置 `DS2API_CONFIG_PATH=/data/config.json` 并挂载 `./config.json:/data/config.json`,优先避免 `/app` 只读带来的配置持久化问题。 如需固定版本,也可以直接拉取指定 tag: @@ -195,6 +196,11 @@ healthcheck: - **端口**:服务默认监听 `5001`,模板会固定设置 `PORT=5001`。 - **配置持久化**:模板挂载卷 `/data`,并设置 `DS2API_CONFIG_PATH=/data/config.json`;在管理台导入配置后,会写入并持久化到该路径。 +- **`open /app/config.json: permission denied`**:说明当前实例在尝试把运行时 token 持久化到只读路径(常见于镜像内 `/app`)。 + 处理建议: + 1. 显式设置可写路径:`DS2API_CONFIG_PATH=/data/config.json`(并挂载持久卷到 `/data`); + 2. 若你使用 `DS2API_CONFIG_JSON` 启动且不需要运行时落盘,可保持环境变量模式(`DS2API_ENV_WRITEBACK` 关闭); + 3. 最新版本中,即使持久化失败,登录/会话测试仍会继续,仅提示“token 未持久化(重启后丢失)”。 - **构建版本号**:Zeabur / 普通 `docker build` 默认不需要传 `BUILD_VERSION`;镜像会优先使用该构建参数,未提供时自动回退到仓库根目录的 `VERSION` 文件。 - **首次登录**:部署完成后访问 `/admin`,使用 Zeabur 环境变量/模板指引中的 `DS2API_ADMIN_KEY` 登录(建议首次登录后自行更换为强密码)。 diff --git a/internal/config/paths.go b/internal/config/paths.go index e3cc249..5df2759 100644 --- a/internal/config/paths.go +++ b/internal/config/paths.go @@ -30,6 +30,11 @@ func ResolvePath(envKey, defaultRel string) string { } func ConfigPath() string { + if strings.TrimSpace(os.Getenv("DS2API_CONFIG_PATH")) == "" && BaseDir() == "/app" { + // Official container images commonly run from /app where filesystem may be read-only. + // Prefer /data default so deployments can persist config/token state by mounting a volume. + return "/data/config.json" + } return ResolvePath("DS2API_CONFIG_PATH", "config.json") } diff --git a/internal/httpapi/admin/accounts/handler_accounts_testing.go b/internal/httpapi/admin/accounts/handler_accounts_testing.go index 3b41c60..d92c1dc 100644 --- a/internal/httpapi/admin/accounts/handler_accounts_testing.go +++ b/internal/httpapi/admin/accounts/handler_accounts_testing.go @@ -107,6 +107,7 @@ func (h *Handler) testAccount(ctx context.Context, acc config.Account, model, me "model": model, "session_count": 0, "config_writable": !h.Store.IsEnvBacked(), + "config_warning": "", } defer func() { status := "failed" @@ -121,8 +122,7 @@ func (h *Handler) testAccount(ctx context.Context, acc config.Account, model, me return result } if err := h.Store.UpdateAccountToken(acc.Identifier(), token); err != nil { - result["message"] = "登录成功但写入运行时 token 失败: " + err.Error() - return result + result["config_warning"] = "登录成功,但 token 持久化失败(仅保存在内存,重启后会丢失): " + err.Error() } authCtx := &authn.RequestAuth{UseConfigToken: false, DeepSeekToken: token, AccountID: identifier, Account: acc} proxyCtx := authn.WithAuth(ctx, authCtx) @@ -136,8 +136,7 @@ func (h *Handler) testAccount(ctx context.Context, acc config.Account, model, me token = newToken authCtx.DeepSeekToken = token if err := h.Store.UpdateAccountToken(acc.Identifier(), token); err != nil { - result["message"] = "刷新 token 成功但写入运行时 token 失败: " + err.Error() - return result + result["config_warning"] = "刷新 token 成功,但 token 持久化失败(仅保存在内存,重启后会丢失): " + err.Error() } sessionID, err = h.DS.CreateSession(proxyCtx, authCtx, 1) if err != nil { @@ -155,6 +154,9 @@ func (h *Handler) testAccount(ctx context.Context, acc config.Account, model, me if strings.TrimSpace(message) == "" { result["success"] = true result["message"] = "Token 刷新成功(登录与会话创建成功)" + if warning, _ := result["config_warning"].(string); strings.TrimSpace(warning) != "" { + result["message"] = result["message"].(string) + ";" + warning + } result["response_time"] = int(time.Since(start).Milliseconds()) return result }