This guide covers all deployment methods for the current Go-based codebase.
- Prerequisites
- 1. Local Run
- 2. Docker Deployment
- 3. Vercel Deployment
- 4. Download Release Binaries
- 5. Reverse Proxy (Nginx)
- 6. Linux systemd Service
- 7. Post-Deploy Checks
- 8. Pre-Release Local Regression
| Dependency | Minimum Version | Notes |
|---|---|---|
| Go | 1.24+ | Build backend |
| Node.js | 20+ | 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) - Compatibility note:
CONFIG_JSONis the legacy fallback variable;DS2API_CONFIG_JSONmay also contain raw JSON directly
Unified recommendation (best practice):
cp config.example.json config.json
# Edit config.jsonUse config.json as the single source of truth:
- Local run: read
config.jsondirectly - Docker / Vercel: generate
DS2API_CONFIG_JSON(Base64) fromconfig.jsonand inject it
# 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/ds2apiDefault address: http://0.0.0.0:5001 (override with PORT).
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.shOr step by step:
cd webui
npm install
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/ds2apigo build -o ds2api ./cmd/ds2api
./ds2api# Copy env template
cp .env.example .env
# Edit .env and set at least:
# DS2API_ADMIN_KEY=your-admin-key
# Start
docker-compose up -d
# View logs
docker-compose logs -fThe default docker-compose.yml maps host port 6011 to container port 5001. If you want 5001 exposed directly, adjust the ports mapping.
docker-compose up -d --buildThe Dockerfile now provides two image paths:
- Default local/dev path (
runtime-from-source): a three-stage build (WebUI build + Go build + runtime). - Release path (
runtime-from-dist): CI first createsdist/ds2api_<tag>_linux_<arch>.tar.gz, then Docker directly reuses the binary andstatic/adminassets from those release archives, without runningnpm build/go buildagain.
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.
docker-compose -f docker-compose.dev.yml upDevelopment features:
- Source code mounted (live changes)
LOG_LEVEL=DEBUG- No auto-restart
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: 10sIf container logs look normal but the admin panel is unreachable, check these first:
- Port alignment: when
PORTis not5001, use the same port in your URL (for examplehttp://localhost:8080/admin). - WebUI assets in dev compose:
docker-compose.dev.ymlrunsgo runin a dev image and does not auto-install Node.js inside the container; ifstatic/adminis missing in your repo,/adminwill return 404. Build once on host:./scripts/build-webui.sh.
This repo includes a zeabur.yaml template for one-click deployment on Zeabur:
Notes:
- Port: DS2API listens on
5001by default; the template setsPORT=5001. - Persistent config: the template mounts
/dataand setsDS2API_CONFIG_PATH=/data/config.json. After importing config in Admin UI, it will be written and persisted to this path. - Build version: Zeabur / regular
docker builddoes not requireBUILD_VERSIONby default. The image prefers that build arg when provided, and automatically falls back to the repo-rootVERSIONfile when it is absent. - First login: after deployment, open
/adminand login withDS2API_ADMIN_KEYshown in Zeabur env/template instructions (recommended: rotate to a strong secret after first login).
-
Fork the repo to your GitHub account
-
Import the project on Vercel
-
Set environment variables (minimum required: one variable):
Variable Description DS2API_ADMIN_KEYAdmin key (required) DS2API_CONFIG_JSONConfig content, raw JSON or Base64 (optional, recommended) -
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.jsonDo 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
| Variable | Description | Default |
|---|---|---|
DS2API_ACCOUNT_MAX_INFLIGHT |
Per-account inflight limit | 2 |
DS2API_ACCOUNT_CONCURRENCY |
Alias (legacy compat) | — |
DS2API_ACCOUNT_MAX_QUEUE |
Waiting queue limit | recommended_concurrency |
DS2API_ACCOUNT_QUEUE_SIZE |
Alias (legacy compat) | — |
DS2API_GLOBAL_MAX_INFLIGHT |
Global inflight limit | recommended_concurrency |
DS2API_MAX_INFLIGHT |
Alias (legacy compat) | — |
DS2API_VERCEL_INTERNAL_SECRET |
Hybrid streaming internal auth | Falls back to DS2API_ADMIN_KEY |
DS2API_VERCEL_STREAM_LEASE_TTL_SECONDS |
Stream lease TTL | 900 |
VERCEL_TOKEN |
Vercel sync token | — |
VERCEL_PROJECT_ID |
Vercel project ID | — |
VERCEL_TEAM_ID |
Vercel team ID | — |
DS2API_VERCEL_PROTECTION_BYPASS |
Deployment protection bypass for internal Node→Go calls | — |
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)
Vercel Go Runtime applies platform-level response buffering, so this project uses a hybrid "Go prepare + Node stream" path on Vercel:
api/chat-stream.jsreceives/v1/chat/completionsrequest- Node calls Go internal prepare endpoint (
?__stream_prepare=1) for session ID, PoW, token - Go prepare creates a stream lease, locking the account
- Node connects directly to DeepSeek upstream, relays SSE in real-time to client (including OpenAI chunk framing and tools anti-leak sieve)
- 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.
api/chat-stream.jsfalls 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 - WebUI non-stream test calls
?__go=1directly to avoid Node hop timeout on long requests
vercel.json sets maxDuration: 300 for both api/chat-stream.js and api/index.go (subject to your Vercel plan limits).
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:
- Open Vercel Project Settings → Build and Development Settings
- Clear custom Go Build Flags / Build Command (recommended)
- If ldflags must be used, set
-ldflags="-s -w"(ensure it's one argument) - Verify
go.moduses a supported version (currentlygo 1.24) - Redeploy (recommended: clear cache)
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.go → ds2api/app → internal/server.
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.
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-bypassheader to requests - Option C: Set
VERCEL_AUTOMATION_BYPASS_SECRET(orDS2API_VERCEL_PROTECTION_BYPASS) for internal Node→Go calls
static/admindirectory is not in Git- Vercel / Docker automatically generate WebUI assets during build
Built-in GitHub Actions workflow: .github/workflows/release-artifacts.yml
- Trigger: only on Release
published(no build on normal push) - Outputs: multi-platform binary archives +
sha256sums.txt - Container publishing: GHCR only (
ghcr.io/cjackhwang/ds2api)
| Platform | Architecture | Format |
|---|---|---|
| Linux | amd64, arm64 | .tar.gz |
| macOS | amd64, arm64 | .tar.gz |
| Windows | amd64 | .zip |
Each archive includes:
ds2apiexecutable (ds2api.exeon Windows)static/admin/(built WebUI assets)sha3_wasm_bg.7b9ca65ddd.wasm(optional; binary has embedded fallback)config.example.json,.env.exampleREADME.MD,README.en.md,LICENSE
# 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- Create and publish a GitHub Release (with tag, for example
vX.Y.Z) - Wait for the
Release Artifactsworkflow to complete - Download the matching archive from Release Assets
# latest
docker pull ghcr.io/cjackhwang/ds2api:latest
# specific version (example)
docker pull ghcr.io/cjackhwang/ds2api:v2.1.2When 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;
}
}# Copy compiled binary and related files to target directory
sudo mkdir -p /opt/ds2api
sudo cp ds2api config.json /opt/ds2api/
# Optional: if you want to use an external WASM file (override embedded one)
# sudo cp sha3_wasm_bg.7b9ca65ddd.wasm /opt/ds2api/
sudo cp -r static/admin /opt/ds2api/static/admin# /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# 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 ds2apiAfter 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":[...]}
# 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-chat","messages":[{"role":"user","content":"hello"}]}'Run the full live testsuite before release (real account tests):
./tests/scripts/run-live.shWith custom flags:
go run ./cmd/ds2api-tests \
--config config.json \
--admin-key admin \
--out artifacts/testsuite \
--timeout 120 \
--retries 2The 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.