---
name: uncloud
description: Operate the uncloud cloud platform from the terminal — sign in, deploy apps and monorepos, attach custom domains, set environment and scale, provision databases, read connection strings into .env, watch rollouts, and reach the rest of the control plane (droplets, volumes, buckets, pipelines, IAM) through the CLI or its REST API. Use when the user asks to deploy their app, ship to production, deploy all apps in a monorepo, point a domain at an app, add or attach a database, get database credentials or a DATABASE_URL, check what is deployed, sign in to uncloud, or manage uncloud infrastructure.
---

# Uncloud

`uncloud` is one binary over the control plane's REST API. Everything the
console can do, the CLI can do; anything it does not model is one
`uncloud api` call away.

## How to run this

**Diagnose and fix before you report.** Every failure below has a command that
identifies it and a command that fixes it. Run them. Do not hand a raw error
back to the user and stop — the only failures worth escalating are the four
listed under *Stop and ask* at the end.

**Branch on the exit code, not the message text.** Wording changes; codes do
not.

| Code | Meaning | Do this |
|---|---|---|
| 0 | worked | continue |
| 1 | generic failure | read stderr; it names the cause |
| 2 | no valid credential | *Not signed in* below |
| 3 | control plane unreachable | *Cannot reach the API* below |
| 4 | build failed | *Build failures* below — the fix is in the log |
| 5 | built, but the app is not serving | *Runtime failures* below |
| 6 | command cannot apply here | run `uncloud doctor` in the project dir |

Every failing command also prints `next: <command>` to stderr. That is the
single next thing to try.

**Read machine output.** `-json` on any command. `uncloud doctor -json` returns
every preflight finding with a level (`ok`/`fixed`/`warn`/`fail`), plus
`exit_code` and `next` when something is wrong. Parse that, not the tables.

## First contact

```bash
command -v uncloud || npm i -g uncloud-cli
uncloud whoami           # who this token acts as, and in which project
```

`whoami` is the cheapest way to answer "am I set up". It prints the email, user
id, project, and whether the token is scoped to that project or falling back to
the account default.

## Deploying

```bash
cd <app directory>
uncloud deploy
```

`deploy` checks the things that break deploys *before* uploading anything,
fixes what is safe to fix, builds, and waits until the URL actually answers. It
writes a Dockerfile and a `.dockerignore` if the directory has none — those are
normal files, so read them and commit them.

`uncloud doctor` runs the same checks and deploys nothing. Run it first when
you want to know what the deploy will do.

For a monorepo, put one path per line in `.uncloud-apps` at the repo root and
run `uncloud deploy all`.

## Recovering

### Not signed in — exit 2

```bash
uncloud whoami            # exit 2 confirms it
```

Signing in needs a human, but **not a browser on this machine**. On a server,
in a container, in CI, or under an agent, `uncloud login` prints a short code:

```
  Open   https://uncloud.club/cli/device
  Enter  V94R-TUT6
```

The user opens that on any device they have, types the code, and the terminal
collects the token by itself. It falls back to this automatically when no
browser can be opened; force it with `uncloud login -device`. **Never tell the
user sign-in is impossible on a headless box — run it and hand them the code.**

What to check before you even ask them:

1. Check it is genuinely an auth problem and not a server fault. Exit code 2
   means the API refused the token. A 500 is not an auth problem — it will
   surface as exit 1, and the token is probably fine.
2. Check whether another saved account works: `uncloud accounts list`, then
   `uncloud accounts use <name>`. Re-run `uncloud whoami`.
3. If a token exists but is scoped to the wrong project, that is exit 1 with
   "token is scoped to another project" — mint one for the right project with
   `uncloud tokens create name=<x>` rather than logging in again.
4. Fully unattended (CI, no human at all): `UNCLOUD_API_URL` and
   `UNCLOUD_API_TOKEN`. Mint with `uncloud tokens create name=ci -json`.

Only after those, run `uncloud login` and give the user the code it prints.

### Cannot reach the API — exit 3

```bash
uncloud api GET /health
```

- Wrong host in `~/.uncloud/config` → `uncloud login -api https://api.uncloud.club`,
  or point at a self-hosted control plane.
- Health is fine but commands fail → not a network problem; re-read the error.

### Build failures — exit 4

The build log names the cause and `deploy` prints the one line that matters
under it. Fix the cause and deploy again. The common ones and their fixes:

| In the log | Fix |
|---|---|
| `npm ci` … `package-lock.json` | commit the lockfile, or use `npm install` |
| `ERR_PNPM_NO_LOCKFILE` | commit `pnpm-lock.yaml`, or `--no-frozen-lockfile` |
| `COPY failed` / `not found in build context` | the path is wrong, or `.dockerignore` excludes it |
| `pull access denied` / `manifest unknown` | the `FROM` tag does not exist |
| `Cannot find module` | dependency is in devDependencies, or install ran `--omit=dev` |
| `JavaScript heap out of memory` | `NODE_OPTIONS=--max-old-space-size=2048` |
| `error TS…` | a real type error — fix it |
| `no space left on device` | the build host is full; tell the user |

To re-read a log later: `uncloud api GET /api/v1/apps/<id>/build-log`.

### Runtime failures — exit 5

It built and deployed but does not serve.

```bash
uncloud logs             # the container says why
uncloud status           # replicas, deployment state, events
```

| In the logs | Fix |
|---|---|
| listening on `127.0.0.1` | bind `0.0.0.0` — `next start -H 0.0.0.0`, `app.listen(port,'0.0.0.0')` |
| `EADDRINUSE` | two processes on one port; start only the server |
| `Cannot find module` | move the package to `dependencies` |
| `ECONNREFUSED` at boot | a database it needs is unreachable — check `uncloud env` |
| `exec format error` | image built for the wrong CPU architecture |
| a missing env var | `uncloud env KEY=value` then `uncloud redeploy` |

**A 502 with a healthy container is almost always the port.** The app must
listen on the port the Dockerfile `EXPOSE`s. `uncloud doctor` compares the two
and offers to fix the app side.

### Deployed but the URL 404s or shows the wrong thing

```bash
uncloud open             # the app's real public URL
uncloud apps list        # every app in the project
```

A brand-new app serves a placeholder page while the image is pulled. That is
not a failure; wait and re-check.

## Everyday operations

```bash
uncloud status                   # watch this app
uncloud logs                     # container logs
uncloud env                      # read config
uncloud env KEY=value            # set, then `uncloud redeploy`
uncloud env KEY-                 # unset
uncloud scale 3                  # replicas, rolled out
uncloud open                     # print and open the public URL
uncloud redeploy                 # roll out the current spec
```

## Domains

```bash
uncloud domain add app.example.com
```

Prints the DNS record to create. TLS is issued automatically once the name
resolves to the ingress — an ACME failure right after adding a domain usually
means DNS has not propagated yet. Re-check rather than re-adding.

## Databases

```bash
uncloud db add -workload dev          # or production
uncloud db env >> .env                # DATABASE_URL for the app
```

## Accounts

```bash
uncloud whoami                   # active account, project, scope
uncloud accounts list            # every saved account, active marked
uncloud accounts use work        # switch
uncloud accounts add personal    # sign in and save under a name
```

Saved in `~/.uncloud/accounts.json` (0600). The active account is mirrored to
`~/.uncloud/config`, which is what every command reads.

## Everything else

Every control-plane resource takes the same five verbs:

```bash
uncloud <resource> list|get|create|update|delete
```

`apps databases droplets volumes buckets clusters pipelines backups webhooks
tokens ssh-keys projects users roles groups cronjobs templates loadbalancers
security-groups plans regions images nodes events audit`

Run `uncloud <resource>` with no verb for its own commands.

### The escape hatch

```bash
uncloud api GET /api/v1/apps
uncloud api POST /api/v1/apps name=api image_url=nginx:alpine
```

Same token, same REST surface. If a capability exists in the API, this reaches
it — so "the CLI has no command for that" is never the end of the road.

## Rules that matter

- **`-y` for anything destructive**, and only when the user asked for it. A
  delete without `-y` refuses rather than prompting when nothing can answer.
- **Never print a token.** `uncloud tokens create -json` returns it once; write
  it where it is needed and do not echo it into the transcript.
- **Do not edit `~/.uncloud/config` by hand.** Use `login`, `accounts use`, or
  the env vars.
- **Verify before reporting success.** `deploy` already waits for HTTP; if you
  did something else, confirm with `uncloud status` or `uncloud open`.
- **Retry once, then stop.** Re-running a failed deploy after a real fix is
  correct. Re-running it unchanged is not.

## Stop and ask

Escalate to the user only for these:

1. **`uncloud login` is required** — it needs a browser and their approval.
2. **A destructive action they did not ask for** — deleting an app, database,
   volume or token.
3. **Spending or capacity** — the build host is out of disk, or a plan limit is
   reached.
4. **A code fix you cannot make safely** — the build fails on their source and
   the fix is a product decision, not a typo.

Everything else: diagnose it, fix it, and carry on.
