CapRover is pleasant when you are clicking around or running commands by hand.

It gets a lot less pleasant when you try to automate it and the CLI decides it still wants a human in the loop.

That was the failure mode here. The target workflow was simple: create or update an app non-interactively from an agent run. Instead, npx caprover deploy and even npx caprover api started dying with a readline crash:

Error [ERR_USE_AFTER_CLOSE]: readline was closed

Once that happens, the important question is not “how do I coax this exact CLI invocation into working?”

It is: what is the most reliable path that still respects the deployment contract?

In this case, the answer was to stop relying on the CapRover CLI wrapper and use the underlying CapRover API directly, including a manual multipart upload to the app-data endpoint.

That path worked cleanly.


The symptom to recognize early

The key symptom was not a normal deploy failure.

It was an automation-hostile failure mode:

  • CapRover CLI commands were present
  • auth was available
  • the target server was reachable
  • but non-interactive runs still crashed inside CLI prompt handling

That is a different class of problem.

When the failure is in the wrapper layer, repeatedly tweaking deploy flags is often wasted motion.


Why the raw API path is the right fallback

CapRover’s CLI is convenient, but it is still just a client around the platform API.

So when the CLI becomes unreliable in automation, the best fallback is usually:

  1. authenticate directly against the CapRover API,
  2. create or inspect the app using the API,
  3. upload the deploy artifact yourself,
  4. let CapRover handle the actual build/deploy from there.

That keeps you inside the supported control plane.

It is also easier to reason about in agents and CI because you remove prompt logic, TTY assumptions, and CLI UX code from the critical path.


The two API operations that mattered

The practical recovery path had two parts.

1) Log in and get a captain auth token

The API login is straightforward:

  • send the CapRover password to the login endpoint
  • extract the returned token
  • use that token on follow-up requests

Once you have the token, app management gets much more deterministic.

2) Upload the app tarball directly

The deploy step was the important one.

Instead of asking the CLI to manage the whole deploy flow, we uploaded the packaged app ourselves to:

/api/v2/user/apps/appData/<app>?detached=1

The key detail there is the detached multipart upload.

That gives CapRover the build context it needs without forcing the higher-level CLI workflow to stay alive and interactive.


Why multipart upload is the right mental model

A lot of people think of CapRover deploys as “run the deploy command.”

For automation, the better model is:

  • create a correct build context,
  • send that build context to CapRover,
  • let the platform build and release it.

Once you see it that way, multipart upload stops feeling like a hack.

It is just a lower-level, more reliable expression of the same deploy contract.


The artifact still matters more than the transport

The API fallback only works well if the uploaded bundle is right.

In our case, the build context was a CapRover tarball that:

  • built the frontend app with Bun,
  • packaged the needed files cleanly,
  • and deployed a runtime image that served the built SPA correctly.

That is important because API fallback does not save a bad deploy artifact. It only removes flaky control-plane tooling from the path.


What I would standardize after hitting this once

If you run CapRover in agent or CI automation, I would not treat raw API fallback as an emergency-only trick.

I would treat it as a standard recovery path and document it up front.

Specifically:

1) Keep login + token retrieval scripted

Do not depend on interactive auth once automation matters.

2) Keep app-definition reads separate from deploy uploads

Read state first. Then mutate intentionally. Do not mix discovery and deploy logic into one opaque shell blob.

3) Make artifact creation reproducible

If you have to fall back from the CLI, the deploy bundle should still be identical in spirit to what your normal path would ship.

4) Prefer API-level logs and status checks after upload

Once the upload is detached, your next job is validation:

  • did CapRover accept it,
  • did the build start,
  • did the app come up healthy,
  • did the public URL behave correctly?

When not to bother fighting the CLI

There is a time to keep debugging the wrapper, and a time to move on.

I would switch to the API path quickly when all of these are true:

  • the server is reachable,
  • auth is valid,
  • the CLI crash is clearly in prompt/readline handling,
  • the workflow needs to be reliable inside agents or CI,
  • and the browser dashboard is not the right operational path.

At that point, the wrapper is the unstable layer.

Do not let it stay on the critical path just because it is the friendlier interface.


A practical operator policy

If your deployment tool has both:

  • a nice human CLI, and
  • an underlying API,

then your automation policy should be explicit:

  • use the CLI when it is stable and cheap,
  • but always keep a direct API path ready for unattended runs.

That is not redundancy for its own sake. It is what keeps operations boring when the wrapper gets weird.


The takeaway

The real lesson here was not “CapRover CLI is bad.”

It was narrower and more useful:

when CapRover CLI breaks in non-interactive automation, stop debugging prompts and fall back to the raw API plus detached multipart upload.

That path is cleaner, more automatable, and closer to the actual deployment boundary anyway.

If you self-host seriously, this is the kind of fallback worth writing down before you need it.