Production Guide
Run cli2api as a hardened, always-on service — systemd, slow models, persistence, reverse proxy.
A complete walkthrough for running cli2api as a real, always-on service: a
dedicated user, a muk- key, systemd auto-start, persistent jobs, tuning for
slow models like gpt-image-2, a TLS reverse proxy, and inbound auth.
The defaults are tuned for this — the server listens on 51222 (not the
crowded 8080), retries transient upstream blips, and persists jobs when you
give it a database.
1. Install the binary
# Pick your platform from the latest release.
sudo curl -sSL -o /usr/local/bin/cli2api \
https://github.com/yeagoo/MuleRunCLI2API/releases/latest/download/cli2api-linux-amd64
sudo chmod +x /usr/local/bin/cli2api
cli2api --version # cli2api v0.1.22. Get your muk- API key (once)
cli2api authenticates with a muk- API key — stable and long-lived per
account. For a long-running systemd service we recommend pinning a static
muk- key (rather than the cache-based flow) — one fewer dependency on
disk, no JWT refresh churn, and trivial to rotate. Capture it once:
export MULERUN_TOKEN=$(curl -fsSL \
https://raw.githubusercontent.com/yeagoo/MuleRunCLI2API/master/scripts/get-muk-key.sh \
| bash)(Prerequisite: mulerun CLI installed and mulerun login done. See
Troubleshooting for
two zero-config alternatives if you'd rather not bake a static secret.)
Store it where only root can read it — never commit it:
sudo install -d -m 750 /etc/cli2api
printf 'MULERUN_TOKEN=muk-xxxxxxxx\n' | sudo tee /etc/cli2api/env >/dev/null
sudo chmod 640 /etc/cli2api/envWant auto-refreshing creds under systemd instead?
The hardened systemd unit below sets ProtectHome=true + User=cli2api, so
~/.config/mulerun/oauth_cache.json is unreachable from the service. To use
the OAuth cache flow (Option A in Troubleshooting), copy the cache to a
writable spot and point cli2api at it:
sudo install -o cli2api -g cli2api -m 600 \
/home/$YOU/.config/mulerun/oauth_cache.json \
/var/lib/cli2api/oauth_cache.json
echo "CLI2API_TOKEN_CACHE=/var/lib/cli2api/oauth_cache.json" \
| sudo tee -a /etc/cli2api/envcli2api will refresh the JWT on its own and persist the exchanged muk- back
into that file. Re-copy or re-run mulerun login → install ... after a long
absence in case the refresh_token chain rotated out of sync.
3. Create a dedicated user + data dir
Don't run network services as root.
sudo useradd --system --no-create-home --shell /usr/sbin/nologin cli2api
sudo install -d -o cli2api -g cli2api -m 750 /var/lib/cli2api
sudo chown root:cli2api /etc/cli2api/env # group-readable by the service4. Inbound API keys (auth)
cli2api has no auth by default — fine for localhost, dangerous if exposed. Generate one or more inbound keys and add them to the env file:
echo "CLI2API_API_KEYS=$(openssl rand -hex 24)" | sudo tee -a /etc/cli2api/envClients then send Authorization: Bearer <key> (OpenAI) or x-api-key: <key>
(Anthropic). Multiple keys are comma-separated.
5. Persist jobs (libsql)
Video/music jobs live in memory by default and vanish on restart. Point the store at a libsql file so job IDs survive restarts and deploys:
echo "CLI2API_JOBSTORE_DSN=file:/var/lib/cli2api/jobs.db" | sudo tee -a /etc/cli2api/env(Or a remote Turso/sqld: libsql://your-db.turso.io?authToken=.... The auth
part is redacted in logs.)
6. Tune for slow models (gpt-image-2, video, music)
Some models are slow. gpt-image-2 routinely takes 2–7 minutes; video and
music can run several minutes. cli2api already retries transient upstream
disconnects during polling, but make sure the timeouts give slow models room:
# in /etc/cli2api/env
CLI2API_IMAGE_TIMEOUT=15m # sync image/speech max wait (default 5m)
CLI2API_POLL_INTERVAL=2s # initial upstream poll cadence
CLI2API_POLL_MAX_INTERVAL=15s # backoff ceilinggpt-image-2 is synchronous and slow
/v1/images/generations blocks until the image is ready. For gpt-image-2
that can be minutes — your HTTP client and any reverse proxy must allow a
long read timeout (see §8). If a client gives up early, the upstream job still
finishes; just call again. Prefer wan2.6-t2i (~15 s) when latency matters.
Job retention (for the async video/music store):
CLI2API_JOB_RETENTION=168h # keep finished jobs 7 days (0 = never expire)
CLI2API_REAPER_INTERVAL=1h # cleanup sweep cadence (0 = disabled)7. systemd unit (auto-start on boot)
sudo tee /etc/systemd/system/cli2api.service >/dev/null <<'EOF'
[Unit]
Description=cli2api — OpenAI/Anthropic-compatible MuleRun proxy
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=cli2api
Group=cli2api
EnvironmentFile=/etc/cli2api/env
ExecStart=/usr/local/bin/cli2api
Restart=on-failure
RestartSec=3
# Hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/cli2api
StateDirectory=cli2api
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now cli2apiVerify:
systemctl status cli2api --no-pager
journalctl -u cli2api -f # follow logs
curl -s localhost:51222/healthz # -> ok
curl -s localhost:51222/v1/images/generations \
-H "Authorization: Bearer $YOUR_KEY" -H "Content-Type: application/json" \
-d '{"model":"wan2.6-t2i","prompt":"a test","size":"1024x1024"}'The startup log line shows version, token_source, jobstore, and the port.
If you see looks like an OAuth JWT — your MULERUN_TOKEN is the wrong kind;
re-do §2.
8. TLS reverse proxy (nginx)
Terminate TLS and forward to 127.0.0.1:51222. Streaming and slow models
need buffering off and a long read timeout:
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
location /v1/ {
proxy_pass http://127.0.0.1:51222;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_buffering off; # SSE + audio streaming
proxy_read_timeout 900s; # gpt-image-2 / video can run minutes
client_max_body_size 64M; # multipart image edits
}
location = /healthz {
proxy_pass http://127.0.0.1:51222;
access_log off;
}
}Caddy equivalent: reverse_proxy 127.0.0.1:51222 plus
flush_interval -1 (disable buffering) inside the route.
9. Update / rollback
sudo curl -sSL -o /usr/local/bin/cli2api \
https://github.com/yeagoo/MuleRunCLI2API/releases/latest/download/cli2api-linux-amd64
sudo chmod +x /usr/local/bin/cli2api
sudo systemctl restart cli2apiPin a version by downloading cli2api-v0.1.2-linux-amd64 instead of the
latest stable name. Job state in libsql survives the restart.
10. Watch your MuleRun balance
cli2api relays upstream 402 Payment Required as 502 upstream HTTP 402 when
your MuleRun account runs out of credits — and it drains silently (premium
models like gpt-image-2 cost the most). A simple hourly alert beats finding
out via a failed request:
# /usr/local/bin/cli2api-balance-check.sh
#!/bin/bash
bal=$(mulerun user balance -o json 2>/dev/null | python3 -c 'import json,sys; print(json.load(sys.stdin)["data"]["balance"])')
awk -v b="$bal" 'BEGIN{ if (b+0 < 100) exit 1 }' \
&& logger -t cli2api "MuleRun balance OK: $bal" \
|| logger -t cli2api -p user.warning "MuleRun balance LOW: $bal — top up at mulerun.com"Drive it with a systemd timer (hourly) and watch journalctl -t cli2api. To
switch to a funded account, see
Troubleshooting → switching accounts.
11. Operational checklist
-
MULERUN_TOKENis amuk-key (no JWT warning at startup) -
CLI2API_API_KEYSset if reachable beyond localhost -
CLI2API_JOBSTORE_DSNset to a file/remote db (jobs survive restart) -
CLI2API_IMAGE_TIMEOUT≥ 15m if you usegpt-image-2 - reverse proxy:
proxy_buffering off+proxy_read_timeout 900s - systemd
Restart=on-failure+enable(survives crashes and reboots) - secrets in
/etc/cli2api/env(mode 640, groupcli2api), never in git -
journalctl -u cli2apireviewed for warnings on first boot - MuleRun balance monitored (alert before it hits 0 → silent 402s)