cli2api

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.2

2. 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/env

Want 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/env

cli2api 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 service

4. 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/env

Clients 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 ceiling

gpt-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 cli2api

Verify:

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 cli2api

Pin 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_TOKEN is a muk- key (no JWT warning at startup)
  • CLI2API_API_KEYS set if reachable beyond localhost
  • CLI2API_JOBSTORE_DSN set to a file/remote db (jobs survive restart)
  • CLI2API_IMAGE_TIMEOUT ≥ 15m if you use gpt-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, group cli2api), never in git
  • journalctl -u cli2api reviewed for warnings on first boot
  • MuleRun balance monitored (alert before it hits 0 → silent 402s)

On this page