mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-04 00:15:28 +08:00
Document Zeabur manual deployment
This commit is contained in:
@@ -254,6 +254,10 @@ docker-compose logs -f
|
||||
2. 部署完成后访问 `/admin`,使用 Zeabur 环境变量/模板指引中的 `DS2API_ADMIN_KEY` 登录。
|
||||
3. 在管理台导入/编辑配置(会写入并持久化到 `/data/config.json`)。
|
||||
|
||||
Zeabur 首次空卷启动时可以没有 `/data/config.json`;DS2API 会先使用空的文件模式配置启动,并在管理台首次保存时创建该文件。
|
||||
|
||||
不依赖模板手动部署时,在 Zeabur 中选择 GitHub 仓库服务,Root Directory 保持 `/`,使用仓库根目录 `Dockerfile` 构建;添加持久卷 `/data`,设置 `PORT=5001`、`DS2API_ADMIN_KEY=你的强密钥`、`DS2API_CONFIG_PATH=/data/config.json`,然后暴露 HTTP 端口 `5001`。更完整步骤见 [docs/DEPLOY.md](docs/DEPLOY.md#不使用模板手动部署)。
|
||||
|
||||
说明:Zeabur 使用仓库内 `Dockerfile` 直接构建时,不需要额外传入 `BUILD_VERSION`;镜像会优先读取该构建参数,未提供时自动回退到仓库根目录的 `VERSION` 文件。
|
||||
|
||||
### 方式三:Vercel 部署
|
||||
|
||||
@@ -243,6 +243,10 @@ Rebuild after updates: `docker-compose up -d --build`
|
||||
2. After deployment, open `/admin` and login with `DS2API_ADMIN_KEY` shown in Zeabur env/template instructions.
|
||||
3. Import / edit config in Admin UI (it will be written and persisted to `/data/config.json`).
|
||||
|
||||
Fresh Zeabur volumes can start without `/data/config.json`; DS2API will boot with an empty file-backed config and create the file on the first Admin UI save.
|
||||
|
||||
For manual deployment without the template, create a Zeabur GitHub service, keep Root Directory as `/`, build with the repo-root `Dockerfile`, mount a persistent volume at `/data`, set `PORT=5001`, `DS2API_ADMIN_KEY=your-strong-secret`, and `DS2API_CONFIG_PATH=/data/config.json`, then expose HTTP port `5001`. See [docs/DEPLOY.en.md](docs/DEPLOY.en.md#manual-deployment-without-the-template) for the full guide.
|
||||
|
||||
Note: when Zeabur builds directly from the repo `Dockerfile`, you do not need to pass `BUILD_VERSION`. The image prefers that build arg when provided, and automatically falls back to the repo-root `VERSION` file when it is absent.
|
||||
|
||||
### Option 3: Vercel
|
||||
|
||||
@@ -196,7 +196,7 @@ This repo includes a `zeabur.yaml` template for one-click deployment on Zeabur:
|
||||
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.
|
||||
- **Persistent config**: the template mounts `/data` and sets `DS2API_CONFIG_PATH=/data/config.json`. On a fresh volume, DS2API starts with an empty file-backed config; 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`);
|
||||
@@ -205,6 +205,37 @@ Notes:
|
||||
- **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).
|
||||
|
||||
#### Manual Deployment Without The Template
|
||||
|
||||
If you do not want to use the `zeabur.yaml` one-click template, deploy directly from the repo root with Zeabur's GitHub integration:
|
||||
|
||||
1. Fork this repo, or push the code to your own GitHub repository.
|
||||
2. In Zeabur Dashboard, create a Project, add a Service, then choose a GitHub/Git repository source.
|
||||
3. Select the repository and branch. Keep Root Directory as `/`.
|
||||
4. Use the Dockerfile build path. Zeabur auto-detects the repo-root `Dockerfile`; do not set `ZBPACK_IGNORE_DOCKERFILE=true`. If the UI asks for a Dockerfile name, enter `Dockerfile`.
|
||||
5. Add a persistent volume in the Service settings and mount it at `/data`.
|
||||
6. Configure environment variables:
|
||||
|
||||
| Variable | Recommended value | Description |
|
||||
| --- | --- | --- |
|
||||
| `PORT` | `5001` | Service listen port; keep it aligned with the exposed Zeabur HTTP port. |
|
||||
| `DS2API_ADMIN_KEY` | Strong random string | Required admin login key. |
|
||||
| `DS2API_CONFIG_PATH` | `/data/config.json` | Recommended persistent config path. |
|
||||
| `LOG_LEVEL` | `INFO` | Optional log level. |
|
||||
| `DS2API_CONFIG_JSON` | Raw JSON or Base64 JSON | Optional config bootstrap from env. |
|
||||
| `DS2API_ENV_WRITEBACK` | `1` | Optional; enable only when using `DS2API_CONFIG_JSON` and you want the initial config written to `/data/config.json`. |
|
||||
|
||||
7. Expose HTTP port `5001`. The health check path can be `/healthz`.
|
||||
8. After deployment, open `/admin`, login with `DS2API_ADMIN_KEY`, then import or edit config in Admin UI. A fresh volume does not need `/data/config.json` up front; the service boots first and creates the file on the first save.
|
||||
|
||||
Troubleshooting:
|
||||
|
||||
- **Startup log says `open /data/config.json: no such file or directory`**: make sure you deployed a version that includes the fresh-volume bootstrap fix, then redeploy the latest code.
|
||||
- **`open /app/config.json: permission denied`**: the config path still points at the read-only image directory; mount `/data` and set `DS2API_CONFIG_PATH=/data/config.json`.
|
||||
- **Config disappears after restart**: check that the `/data` persistent volume is mounted on this service. If you use `DS2API_CONFIG_JSON` but want Admin UI saves persisted, enable `DS2API_ENV_WRITEBACK=1`.
|
||||
|
||||
References: Zeabur's official [GitHub/Git integration](https://zeabur.com/docs/en-US/deploy/github), [Dockerfile deployment](https://zeabur.com/docs/en-US/deploy/dockerfile), and [Volumes](https://zeabur.com/docs/data-management/volumes) docs.
|
||||
|
||||
---
|
||||
|
||||
## 3. Vercel Deployment
|
||||
|
||||
@@ -196,7 +196,7 @@ healthcheck:
|
||||
部署要点:
|
||||
|
||||
- **端口**:服务默认监听 `5001`,模板会固定设置 `PORT=5001`。
|
||||
- **配置持久化**:模板挂载卷 `/data`,并设置 `DS2API_CONFIG_PATH=/data/config.json`;在管理台导入配置后,会写入并持久化到该路径。
|
||||
- **配置持久化**:模板挂载卷 `/data`,并设置 `DS2API_CONFIG_PATH=/data/config.json`;首次空卷启动时会先使用空的文件模式配置,在管理台导入配置后,会写入并持久化到该路径。
|
||||
- **`open /app/config.json: permission denied`**:说明当前实例在尝试把运行时 token 持久化到只读路径(常见于镜像内 `/app`)。
|
||||
处理建议:
|
||||
1. 显式设置可写路径:`DS2API_CONFIG_PATH=/data/config.json`(并挂载持久卷到 `/data`);
|
||||
@@ -205,6 +205,37 @@ healthcheck:
|
||||
- **构建版本号**:Zeabur / 普通 `docker build` 默认不需要传 `BUILD_VERSION`;镜像会优先使用该构建参数,未提供时自动回退到仓库根目录的 `VERSION` 文件。
|
||||
- **首次登录**:部署完成后访问 `/admin`,使用 Zeabur 环境变量/模板指引中的 `DS2API_ADMIN_KEY` 登录(建议首次登录后自行更换为强密码)。
|
||||
|
||||
#### 不使用模板手动部署
|
||||
|
||||
如果你不想使用 `zeabur.yaml` 一键模板,可以直接用 Zeabur 的 GitHub 集成从仓库根目录构建:
|
||||
|
||||
1. Fork 本仓库,或把代码推送到你自己的 GitHub 仓库。
|
||||
2. 在 Zeabur Dashboard 中创建 Project,然后添加 Service,选择 GitHub/Git 仓库来源。
|
||||
3. 选择仓库与分支,Root Directory 保持 `/`。
|
||||
4. 构建方式使用 Dockerfile。Zeabur 会自动检测仓库根目录的 `Dockerfile`;不要设置 `ZBPACK_IGNORE_DOCKERFILE=true`。如果界面要求填写 Dockerfile 名称,填写 `Dockerfile`。
|
||||
5. 在 Service 配置中添加持久卷,挂载目录填写 `/data`。
|
||||
6. 配置环境变量:
|
||||
|
||||
| 变量 | 推荐值 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `PORT` | `5001` | 服务监听端口,需要和 Zeabur 暴露的 HTTP 端口一致。 |
|
||||
| `DS2API_ADMIN_KEY` | 强随机字符串 | 管理台登录密钥,必填。 |
|
||||
| `DS2API_CONFIG_PATH` | `/data/config.json` | 配置持久化路径,建议必填。 |
|
||||
| `LOG_LEVEL` | `INFO` | 可选,日志级别。 |
|
||||
| `DS2API_CONFIG_JSON` | 原始 JSON 或 Base64 JSON | 可选,用于用环境变量初始化配置。 |
|
||||
| `DS2API_ENV_WRITEBACK` | `1` | 可选;当设置了 `DS2API_CONFIG_JSON` 且希望首次启动后写入 `/data/config.json` 时再启用。 |
|
||||
|
||||
7. 暴露 HTTP 端口 `5001`,健康检查路径可填 `/healthz`。
|
||||
8. 部署完成后访问 `/admin`,用 `DS2API_ADMIN_KEY` 登录,然后在管理台导入或编辑配置。首次空卷可以没有 `/data/config.json`,服务会先启动,第一次保存时自动创建该文件。
|
||||
|
||||
常见问题:
|
||||
|
||||
- **启动日志出现 `open /data/config.json: no such file or directory`**:请确认已经部署包含“首次空卷启动”修复的版本,并重新部署最新代码。
|
||||
- **出现 `open /app/config.json: permission denied`**:说明配置路径仍指向镜像内只读目录;设置持久卷 `/data`,并确认 `DS2API_CONFIG_PATH=/data/config.json`。
|
||||
- **管理台保存后重启配置丢失**:检查 `/data` 持久卷是否已挂载到当前服务;如果使用了 `DS2API_CONFIG_JSON`,但想让管理台保存落盘,请启用 `DS2API_ENV_WRITEBACK=1`。
|
||||
|
||||
参考:Zeabur 官方文档的 [GitHub/Git 集成](https://zeabur.com/docs/en-US/deploy/github)、[Dockerfile 部署](https://zeabur.com/docs/zh-CN/deploy/dockerfile) 与 [Volumes](https://zeabur.com/docs/data-management/volumes)。
|
||||
|
||||
---
|
||||
|
||||
## 三、Vercel 部署
|
||||
|
||||
@@ -144,6 +144,44 @@ func TestLoadStoreIgnoresLegacyConfigJSONEnv(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExplicitMissingConfigPathBootstrapsEmptyFileBackedStore(t *testing.T) {
|
||||
path := t.TempDir() + "/config.json"
|
||||
|
||||
t.Setenv("DS2API_CONFIG_JSON", "")
|
||||
t.Setenv("DS2API_CONFIG_PATH", path)
|
||||
|
||||
store, err := LoadStoreWithError()
|
||||
if err != nil {
|
||||
t.Fatalf("expected missing explicit config path to bootstrap, got: %v", err)
|
||||
}
|
||||
if store.IsEnvBacked() {
|
||||
t.Fatal("expected bootstrap store to be file-backed")
|
||||
}
|
||||
if store.ConfigPath() != path {
|
||||
t.Fatalf("ConfigPath() = %q, want %q", store.ConfigPath(), path)
|
||||
}
|
||||
if len(store.Keys()) != 0 || len(store.Accounts()) != 0 {
|
||||
t.Fatalf("expected empty bootstrap config, got keys=%d accounts=%d", len(store.Keys()), len(store.Accounts()))
|
||||
}
|
||||
if _, statErr := os.Stat(path); !errors.Is(statErr, os.ErrNotExist) {
|
||||
t.Fatalf("expected bootstrap not to create config until first save, stat err=%v", statErr)
|
||||
}
|
||||
|
||||
if err := store.Update(func(c *Config) error {
|
||||
c.Keys = []string{"first-key"}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatalf("update should persist bootstrap config: %v", err)
|
||||
}
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("expected first update to write config: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(content), "first-key") {
|
||||
t.Fatalf("expected saved config to contain first key, got: %s", content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvBackedStoreWritebackBootstrapsMissingConfigFile(t *testing.T) {
|
||||
tmp, err := os.CreateTemp(t.TempDir(), "config-*.json")
|
||||
if err != nil {
|
||||
|
||||
@@ -52,11 +52,12 @@ func loadStore() (*Store, error) {
|
||||
|
||||
func loadConfig() (Config, bool, error) {
|
||||
rawCfg := strings.TrimSpace(os.Getenv("DS2API_CONFIG_JSON"))
|
||||
path := ConfigPath()
|
||||
if rawCfg != "" {
|
||||
cfg, err := parseConfigString(rawCfg)
|
||||
if err != nil {
|
||||
if !IsVercel() && envWritebackEnabled() {
|
||||
if fileCfg, fileErr := loadConfigFromFile(ConfigPath()); fileErr == nil {
|
||||
if fileCfg, fileErr := loadConfigFromFile(path); fileErr == nil {
|
||||
return fileCfg, false, nil
|
||||
}
|
||||
}
|
||||
@@ -67,7 +68,7 @@ func loadConfig() (Config, bool, error) {
|
||||
if IsVercel() || !envWritebackEnabled() {
|
||||
return cfg, true, err
|
||||
}
|
||||
content, fileErr := os.ReadFile(ConfigPath())
|
||||
content, fileErr := os.ReadFile(path)
|
||||
if fileErr == nil {
|
||||
var fileCfg Config
|
||||
if unmarshalErr := json.Unmarshal(content, &fileCfg); unmarshalErr == nil {
|
||||
@@ -79,7 +80,7 @@ func loadConfig() (Config, bool, error) {
|
||||
if validateErr := ValidateConfig(cfg); validateErr != nil {
|
||||
return cfg, true, validateErr
|
||||
}
|
||||
if writeErr := writeConfigFile(ConfigPath(), cfg.Clone()); writeErr == nil {
|
||||
if writeErr := writeConfigFile(path, cfg.Clone()); writeErr == nil {
|
||||
return cfg, false, err
|
||||
} else {
|
||||
Logger.Warn("[config] env writeback bootstrap failed", "error", writeErr)
|
||||
@@ -87,7 +88,7 @@ func loadConfig() (Config, bool, error) {
|
||||
}
|
||||
return cfg, true, err
|
||||
}
|
||||
cfg, err := loadConfigFromFile(ConfigPath())
|
||||
cfg, err := loadConfigFromFile(path)
|
||||
if err != nil {
|
||||
if shouldTryLegacyContainerConfigPath() {
|
||||
legacyPath := legacyContainerConfigPath()
|
||||
@@ -100,6 +101,10 @@ func loadConfig() (Config, bool, error) {
|
||||
// Vercel may start without writable/present config; keep in-memory bootstrap config.
|
||||
return Config{}, true, nil
|
||||
}
|
||||
if shouldBootstrapMissingConfigFile(err) {
|
||||
Logger.Warn("[config] config file missing; starting with empty file-backed config", "path", path)
|
||||
return Config{}, false, nil
|
||||
}
|
||||
return Config{}, false, err
|
||||
}
|
||||
if IsVercel() {
|
||||
@@ -109,6 +114,10 @@ func loadConfig() (Config, bool, error) {
|
||||
return cfg, false, nil
|
||||
}
|
||||
|
||||
func shouldBootstrapMissingConfigFile(err error) bool {
|
||||
return errors.Is(err, os.ErrNotExist) && strings.TrimSpace(os.Getenv("DS2API_CONFIG_PATH")) != ""
|
||||
}
|
||||
|
||||
func loadConfigFromFile(path string) (Config, error) {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
|
||||
@@ -25,6 +25,7 @@ spec:
|
||||
1. Open your service URL, then visit `/admin`
|
||||
2. Login with `DS2API_ADMIN_KEY` (shown in Zeabur env/instructions)
|
||||
3. Import / edit config in Admin UI (saved to `/data/config.json`)
|
||||
4. On a fresh volume, DS2API starts with an empty config and creates `/data/config.json` on the first save
|
||||
|
||||
services:
|
||||
- name: ds2api
|
||||
|
||||
Reference in New Issue
Block a user