Files
ds2api/docs/DEPLOY.en.md
2026-05-02 12:19:09 +08:00

22 KiB

DS2API Deployment Guide

Language: 中文 | English

This guide covers all deployment methods for the current Go-based codebase.

Doc map: Index | Architecture | API | Testing


Table of Contents


Recommended order when choosing a deployment method:

  1. Download and run release binaries: the easiest path for most users because the artifacts are already built.
  2. Docker / GHCR image deployment: suitable for containerized, orchestrated, or cloud environments.
  3. Vercel deployment: suitable if you already use Vercel and accept its platform constraints.
  4. Run from source / build locally: suitable for development, debugging, or when you need to modify the code yourself.

0. Prerequisites

Dependency Minimum Version Notes
Go 1.26+ Build backend
Node.js 20.19+ or 22.12+ Only needed to build WebUI locally
npm Bundled with Node.js Install WebUI dependencies

Config source (choose one):

  • File: config.json (recommended for local/Docker)
  • Environment variable: DS2API_CONFIG_JSON (recommended for Vercel; supports raw JSON or Base64)

Unified recommendation (best practice):

cp config.example.json config.json
# Edit config.json

Use config.json as the single source of truth:

  • Local run: read config.json directly
  • Docker / Vercel: generate DS2API_CONFIG_JSON (Base64) from config.json and inject it

1. Download Release Binaries

Built-in GitHub Actions workflow: .github/workflows/release-artifacts.yml

  • Trigger: by default only on Release published; you can also run it manually via workflow_dispatch and pass release_tag to rerun / backfill
  • Outputs: multi-platform binary archives, Linux Docker image export tarballs, and sha256sums.txt
  • Container publishing: GHCR only (ghcr.io/cjackhwang/ds2api)
Platform Architecture Format
Linux amd64, arm64, armv7 .tar.gz
macOS amd64, arm64 .tar.gz
Windows amd64, arm64 .zip

Each archive includes:

  • ds2api executable (ds2api.exe on Windows)
  • static/admin/ (built WebUI assets)
  • config.example.json, .env.example
  • README.MD, README.en.md, LICENSE

Usage

# 1. Download the archive for your platform
# 2. Extract
tar -xzf ds2api_<tag>_linux_amd64.tar.gz
cd ds2api_<tag>_linux_amd64

# 3. Configure
cp config.example.json config.json
# Edit config.json

# 4. Start
./ds2api

Maintainer Release Flow

  1. Create and publish a GitHub Release (with tag, for example vX.Y.Z)
  2. Wait for the Release Artifacts workflow to complete
  3. Download the matching archive from Release Assets

2. Docker / GHCR Deployment

2.1 Basic Steps

# Pull prebuilt image
docker pull ghcr.io/cjackhwang/ds2api:latest

# Copy env template and config file
cp .env.example .env
cp config.example.json config.json

# Edit .env and set at least:
#   DS2API_ADMIN_KEY=your-admin-key
# Optionally set the host port:
#   DS2API_HOST_PORT=6011

# Start
docker-compose up -d

# View logs
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. The image pre-creates /data and grants it to the non-root ds2api user. If you bind-mount a single host file, make sure config.json is readable/writable by the container user, for example with chmod 644 config.json; otherwise Linux UID/GID mismatches can still cause open /data/config.json: permission denied. Compatibility note: when DS2API_CONFIG_PATH is unset and runtime base dir is /app, newer versions prefer /data/config.json; if that file is missing but legacy /app/config.json exists, DS2API automatically falls back to the legacy path to avoid post-upgrade config loss.

If you want a pinned version instead of latest, you can also pull a specific tag directly:

docker pull ghcr.io/cjackhwang/ds2api:v3.0.0

2.2 Update

docker-compose up -d --build

2.3 Docker Architecture

The Dockerfile now provides two image paths:

  1. Default local/dev path (runtime-from-source): a three-stage build (WebUI build + Go build + runtime).
  2. Release path (runtime-from-dist): the release workflow first creates tag-named release archives, then copies the Linux bundles to dist/docker-input/linux_amd64.tar.gz / linux_arm64.tar.gz; Docker consumes those prepared inputs directly, without rerunning npm build/go build.

The release path keeps Docker images aligned with release archives and reduces duplicate build work.

Container entry command: /usr/local/bin/ds2api, default exposed port: 5001.

2.4 Development Mode

docker-compose -f docker-compose.dev.yml up

Development features:

  • Source code mounted (live changes)
  • LOG_LEVEL=DEBUG
  • No auto-restart

2.5 Health Check

Docker Compose includes a built-in health check:

healthcheck:
  test: ["CMD", "/usr/local/bin/busybox", "wget", "-qO-", "http://localhost:${PORT:-5001}/healthz"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 10s

2.6 Docker Troubleshooting

If container logs look normal but the admin panel is unreachable, check these first:

  1. Port alignment: when PORT is not 5001, use the same port in your URL (for example http://localhost:8080/admin).
  2. WebUI assets in dev compose: docker-compose.dev.yml runs go run in a dev image and does not auto-install Node.js inside the container; if static/admin is missing in your repo, /admin will return 404. Build once on host: ./scripts/build-webui.sh.

2.7 Zeabur One-Click (Dockerfile)

This repo includes a zeabur.yaml template for one-click deployment on Zeabur:

Deploy 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. 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);
    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).

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.
  1. Expose HTTP port 5001. The health check path can be /healthz.
  2. 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, Dockerfile deployment, and Volumes docs.


3. Vercel Deployment

3.1 Steps

  1. Fork the repo to your GitHub account
  2. Import the project on Vercel
  3. Set environment variables (minimum required: one variable):
Variable Description
DS2API_ADMIN_KEY Admin key (required)
DS2API_CONFIG_JSON Config content, raw JSON or Base64 (optional, recommended)
  1. Deploy

If you prefer faster one-click bootstrap, you can leave DS2API_CONFIG_JSON empty first, then open /admin after deployment, import config, and sync it back to Vercel env vars from the "Vercel Sync" page.

Recommended: in repo root, copy the template first and fill your real accounts:

cp config.example.json config.json
# Edit config.json

Do not hand-edit large JSON directly in Vercel. Generate Base64 locally and paste it:

# Run in repo root
DS2API_CONFIG_JSON="$(base64 < config.json | tr -d '\n')"
echo "$DS2API_CONFIG_JSON"

If you choose to preconfigure before first deploy, set these vars in Vercel Project Settings -> Environment Variables:

DS2API_ADMIN_KEY=replace-with-a-strong-secret
DS2API_CONFIG_JSON=<the single-line Base64 output above>

Optional but recommended (for WebUI one-click Vercel sync):

VERCEL_TOKEN=your-vercel-token
VERCEL_PROJECT_ID=prj_xxxxxxxxxxxx
VERCEL_TEAM_ID=team_xxxxxxxxxxxx   # optional for personal accounts

3.2 Optional Environment Variables

Variable Description Default
DS2API_ACCOUNT_MAX_INFLIGHT Per-account inflight limit 2
DS2API_ACCOUNT_MAX_QUEUE Waiting queue limit recommended_concurrency
DS2API_GLOBAL_MAX_INFLIGHT Global inflight limit recommended_concurrency
DS2API_ENV_WRITEBACK When DS2API_CONFIG_JSON is present, auto-write to DS2API_CONFIG_PATH and switch to file-backed mode after success (1/true/yes/on) Disabled
DS2API_VERCEL_INTERNAL_SECRET Hybrid streaming internal auth Falls back to DS2API_ADMIN_KEY
DS2API_VERCEL_STREAM_LEASE_TTL_SECONDS Stream lease TTL 900
DS2API_RAW_STREAM_SAMPLE_ROOT Raw stream sample root for saving/reading samples tests/raw_stream_samples
VERCEL_TOKEN Vercel sync token
VERCEL_PROJECT_ID Vercel project ID
VERCEL_TEAM_ID Vercel team ID
DS2API_CHAT_HISTORY_PATH Chat history storage path (must be set to /tmp/chat_history.json on Vercel, otherwise unavailable due to read-only filesystem) data/chat_history.json
DS2API_VERCEL_PROTECTION_BYPASS Deployment protection bypass for internal Node→Go calls

3.4 Vercel Architecture

Request ──────┐
              │
              ▼
         vercel.json routing
              │
        ┌─────┴─────┐
        │           │
        ▼           ▼
  api/index.go   api/chat-stream.js
  (Go Runtime)   (Node Runtime)
  • Go entry: api/index.go (Serverless Go)
  • Stream entry: api/chat-stream.js (Node Runtime for real-time SSE)
  • Routing: vercel.json
  • Build command: npm ci --prefix webui && npm run build --prefix webui (automatic)

Streaming Pipeline

Vercel Go Runtime applies platform-level response buffering, so this project uses a hybrid "Go prepare + Node stream" path on Vercel:

  1. api/chat-stream.js receives /v1/chat/completions request
  2. Node calls Go internal prepare endpoint (?__stream_prepare=1) for session ID, PoW, token
  3. Go prepare creates a stream lease, locking the account
  4. Node connects directly to DeepSeek upstream, relays SSE in real-time to client (including OpenAI chunk framing and tools anti-leak sieve)
  5. After stream ends, Node calls Go release endpoint (?__stream_release=1) to free the account

This adaptation is Vercel-only; local and Docker remain pure Go.

Non-Stream Fallback and Tool Call Handling

  • api/chat-stream.js falls back to Go entry (?__go=1) for non-stream requests only
  • Streaming requests (including requests with tools) stay on the Node path and use Go-aligned tool-call anti-leak handling
  • The Node stream path also mirrors Go finalization semantics: empty visible output returns the same shaped error SSE, and empty content_filter returns a content_filter error
  • WebUI non-stream test calls ?__go=1 directly to avoid Node hop timeout on long requests

Function Duration

vercel.json sets maxDuration: 300 for both api/chat-stream.js and api/index.go (subject to your Vercel plan limits).

3.5 Vercel Troubleshooting

Go Build Failure

Error: Command failed: go build -ldflags -s -w -o .../bootstrap ...

Cause: Invalid Go build flag settings in Vercel (-ldflags not passed as a single argument).

Fix:

  1. Open Vercel Project Settings → Build and Development Settings
  2. Clear custom Go Build Flags / Build Command (recommended)
  3. If ldflags must be used, set -ldflags="-s -w" (ensure it's one argument)
  4. Verify go.mod uses a supported version (currently go 1.26.0)
  5. Redeploy (recommended: clear cache)

Internal Package Import Error

use of internal package ds2api/internal/server not allowed

Cause: Vercel Go entrypoint directly imports internal/....

Fix: This repo uses a public bridge package: api/index.gods2api/appinternal/server.

Output Directory Error

No Output Directory named "public" found after the Build completed.

Fix: This repo uses static as output directory ("outputDirectory": "static" in vercel.json). If you manually changed Output Directory in Project Settings, set it to static or clear it.

Deployment Protection Blocking

If API responses return Vercel HTML Authentication Required:

  • Option A: Disable Deployment Protection for that environment (recommended for public APIs)
  • Option B: Add x-vercel-protection-bypass header to requests
  • Option C: Set VERCEL_AUTOMATION_BYPASS_SECRET (or DS2API_VERCEL_PROTECTION_BYPASS) for internal Node→Go calls

Chat History Unavailable (read-only file system)

create chat history dir: mkdir /var/task/data: read-only file system

Cause: Vercel Serverless functions have a read-only filesystem (/var/task). Chat history fails because it cannot create directories there.

Fix: Add the following in Vercel Project Settings → Environment Variables:

DS2API_CHAT_HISTORY_PATH=/tmp/chat_history.json

/tmp is the only writable directory in Vercel Serverless. Data is ephemeral (not persisted across cold starts), but the feature works within a single instance lifetime.

3.6 Build Artifacts Not Committed

  • static/admin directory is not in Git
  • Vercel / Docker automatically generate WebUI assets during build

4. Local Run from Source

4.1 Basic Steps

# Clone
git clone https://github.com/CJackHwang/ds2api.git
cd ds2api

# Copy and edit config
cp config.example.json config.json
# Open config.json and fill in:
#   - keys: your API access keys
#   - accounts: DeepSeek accounts (email or mobile + password)

# Start
go run ./cmd/ds2api

Default local access URL: http://127.0.0.1:5001; the server actually binds to 0.0.0.0:5001 (override with PORT).

4.2 WebUI Build

On first local startup, if static/admin/ is missing, DS2API will automatically attempt to build the WebUI (requires Node.js/npm; when dependencies are missing it runs npm ci first, then npm run build -- --outDir static/admin --emptyOutDir).

Manual build:

./scripts/build-webui.sh

Or step by step:

cd webui
npm ci
npm run build
# Output goes to static/admin/

Control auto-build via environment variable:

# Disable auto-build
DS2API_AUTO_BUILD_WEBUI=false go run ./cmd/ds2api

# Force enable auto-build
DS2API_AUTO_BUILD_WEBUI=true go run ./cmd/ds2api

4.3 Compile to Binary

go build -o ds2api ./cmd/ds2api
./ds2api

5. Reverse Proxy (Nginx)

When deploying behind Nginx, you must disable buffering for SSE streaming to work:

location / {
    proxy_pass http://127.0.0.1:5001;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_buffering off;
    proxy_cache off;
    chunked_transfer_encoding on;
    tcp_nodelay on;
}

For HTTPS, add SSL at the Nginx layer:

server {
    listen 443 ssl;
    server_name api.example.com;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    location / {
        proxy_pass http://127.0.0.1:5001;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_buffering off;
        proxy_cache off;
        chunked_transfer_encoding on;
        tcp_nodelay on;
    }
}

6. Linux systemd Service

6.1 Installation

# Copy compiled binary and related files to target directory
sudo mkdir -p /opt/ds2api
sudo cp ds2api config.json /opt/ds2api/
sudo cp -r static/admin /opt/ds2api/static/admin

6.2 Create systemd Service File

# /etc/systemd/system/ds2api.service

[Unit]
Description=DS2API (Go)
After=network.target

[Service]
Type=simple
WorkingDirectory=/opt/ds2api
Environment=PORT=5001
Environment=DS2API_CONFIG_PATH=/opt/ds2api/config.json
Environment=DS2API_ADMIN_KEY=your-admin-key-here
ExecStart=/opt/ds2api/ds2api
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

6.3 Common Commands

# Reload service config
sudo systemctl daemon-reload

# Enable on boot
sudo systemctl enable ds2api

# Start
sudo systemctl start ds2api

# Check status
sudo systemctl status ds2api

# View logs
sudo journalctl -u ds2api -f

# Restart
sudo systemctl restart ds2api

# Stop
sudo systemctl stop ds2api

7. Post-Deploy Checks

After deployment (any method), verify in order:

# 1. Liveness probe
curl -s http://127.0.0.1:5001/healthz
# Expected: {"status":"ok"}

# 2. Readiness probe
curl -s http://127.0.0.1:5001/readyz
# Expected: {"status":"ready"}

# 3. Model list
curl -s http://127.0.0.1:5001/v1/models
# Expected: {"object":"list","data":[...]} (including `*-nothinking` variants)

# 4. Admin panel (if WebUI is built)
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:5001/admin
# Expected: 200

# 5. Test API call
curl http://127.0.0.1:5001/v1/chat/completions \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"hello"}]}'

8. Pre-Release Local Regression

Run the full live testsuite before release (real account tests):

./tests/scripts/run-live.sh

With custom flags:

go run ./cmd/ds2api-tests \
  --config config.json \
  --admin-key admin \
  --out artifacts/testsuite \
  --timeout 120 \
  --retries 2

The testsuite automatically performs:

  • Preflight checks (syntax/build/unit tests)
  • Isolated config copy startup (no mutation to your original config.json)
  • Live scenario verification (OpenAI/Claude/Admin/concurrency/toolcall/streaming)
  • Full request/response artifact logging for debugging

For detailed testsuite documentation, see TESTING.md. The fixed local PR gates are listed in TESTING.md.