---
name: cloudish
description: Get an API key, build a Docker image from source (or point at an existing one), and deploy it as a running container app on Cloudish — no local Docker daemon needed. Use whenever a task is "deploy this to Cloudish", "containerize this and run it on Cloudish", or "get a Cloudish API key".
---

# Deploy container apps on Cloudish

Cloudish is an agent-first, API-key/credit, container-deployment platform: it
builds images server-side (no local Docker daemon or `docker build` needed,
no registry credentials to manage), hosts a private registry, and runs the
resulting containers with attached storage. There is no user-facing product
UX here — everything is this API.

## 0. Get an API key

Free, no auth needed. Starts with 0 credits — ask the platform admin to
grant some (they can do this by the alias below) before building or running
anything billable.

```bash
curl -X POST https://cloudish.ai/api/v1/keys
# -> { "apiKey": { "alias": "funny-monkey", ... }, "key": "cld_...", "runtimeKey": "cldr_..." }
```

Save `key` as `$CLOUDISH_API_KEY` — every call below authenticates with
`Authorization: Bearer $CLOUDISH_API_KEY`. `runtimeKey` (`$CLOUDISH_TOKEN`) is a
scoped-down credential meant for a *running* container to call back with
(building its own image, registering itself as a new app) — it cannot modify
or delete something that already exists. Optionally pass `{"alias": "..."}`
to choose your own alias instead of a random one; it's permanent and becomes
the `{alias}` in every project path below.

## 1. Create (or update) a project and deploy it — one call

`POST /api/v1/projects` creates the project if it doesn't exist yet (or
updates it if it does — same call either way, so a redeploy script can
always call this unconditionally), and optionally attaches an image in the
same request. Two ways to attach one, mutually exclusive:

**An image you already have** — registered immediately:

```bash
curl -X POST https://cloudish.ai/api/v1/projects \
  -H "Authorization: Bearer $CLOUDISH_API_KEY" -H "content-type: application/json" \
  -d '{"name": "my-app", "image": "ghcr.io/acme/my-app:latest", "port": 8080}'
# -> { "project": { "path": "your-alias/my-app", ... }, "app": { "status": "registered", ... } }
```

**Build from source** — upload a tar.gz build context (must contain a
`Dockerfile` at its root); a build runs server-side and, once it
succeeds, the project's docker app is registered automatically — no second
call needed:

```bash
tar -czf context.tar.gz .
curl -X POST https://cloudish.ai/api/v1/projects \
  -H "Authorization: Bearer $CLOUDISH_API_KEY" \
  -F "name=my-app" -F "port=8080" -F "context=@context.tar.gz"
# -> { "project": { "path": "your-alias/my-app", ... }, "build": { "id": 123, "status": "pending" } }
```

The build runs with `cpuCores: 1`/
`memoryGb: 4` by default. If it dies
with no error beyond `"Job has reached the specified backoff limit"` right
after a dependency-install step (`npm ci`, `pip install`, ...), that's
almost always an out-of-memory kill during the build's filesystem snapshot, not
a Dockerfile problem — retry with more of both as extra form fields, one of
`0.5`, `1`, `2`, `3` cpu cores
(`buildCpuCores`) and `1`, `2`, `4`, `5`
GB memory (`buildMemoryGb`).

Poll until it settles:

```bash
curl https://cloudish.ai/api/v1/images/builds/123 -H "Authorization: Bearer $CLOUDISH_API_KEY"
# -> { "build": { "status": "running", "logs": "<build output so far>" }, "image": null }
```

`build.logs` is the current tail of what the build is doing — it grows on every
poll while the build runs, so print only the lines you haven't shown yet.
Stop once `status` is `"succeeded"` or `"failed"`; on `"failed"`, show both
`build.error` and the tail of `build.logs`. Once `"succeeded"`, the project's
docker app is already registered — no follow-up call needed.

Other docker-app fields available on either path: `replicas`, `env` (JSON
object of non-secret vars), `volumeEnabled`/`volumeSizeGb`/`volumeMountPath`
(persistent storage), `cpuCores`/`memoryGb` (container resources, distinct
from the build's own), `requireAuth` (see section 4).

Once it's running, the app itself is reachable at `https://cloudish.ai/{alias}/{name}/`
— the container starts on first request if it isn't already, and scales back
down after inactivity (60/300/900/3600/28800/86400 seconds; default is
the deployment's own, override per-key with
`PATCH /api/v1/me {"settings": {"idleTimeoutSeconds": <one of those>}}`).
A request there carries the same `Authorization: Bearer $CLOUDISH_API_KEY` as
everything else in this doc; an `open`-access project also answers with no
credential at all. See section 5 if the app can't run under a path prefix.

## 2. Environment variables and secrets

Non-secret config goes in `env` on the same call as above:

```bash
curl -X POST https://cloudish.ai/api/v1/projects \
  -H "Authorization: Bearer $CLOUDISH_API_KEY" -H "content-type: application/json" \
  -d '{"name": "my-app", "image": "ghcr.io/acme/my-app:latest", "port": 8080,
       "env": {"LOG_LEVEL": "debug"}}'
```

Anything sensitive is set separately, encrypted at rest, and merged into the
container's environment automatically — never put it in `env` above:

```bash
curl -X PUT https://cloudish.ai/api/v1/projects/YOUR_ALIAS/my-app/secrets \
  -H "Authorization: Bearer $CLOUDISH_API_KEY" -H "content-type: application/json" \
  -d '{"name": "OPENAI_API_KEY", "value": "sk-..."}'
```

## 3. Persistent storage

Request a volume when creating or updating the app:

```bash
curl -X POST https://cloudish.ai/api/v1/projects \
  -H "Authorization: Bearer $CLOUDISH_API_KEY" -H "content-type: application/json" \
  -d '{"name": "my-app", "image": "ghcr.io/acme/my-app:latest", "port": 8080,
       "volumeEnabled": true, "volumeSizeGb": 5, "volumeMountPath": "/data"}'
```

`volumeSizeGb` must be one of 1, 5 or 20; `volumeMountPath` defaults to
`/data`. A volume can only attach to one pod, so an app with one enabled
always runs single-replica regardless of any `replicas` value sent.

## 4. Header-based authentication

Set `"requireAuth": true` on the registration to require a real Cloudish
identity before a request reaches the container — without it, a public/
unlisted project lets anonymous requests through with no identity at all.
Cloudish then injects trusted headers into every request it forwards to the app:

- `X-Cloudish-User-Email`
- `X-Cloudish-User-Role` (`admin` for the project's owner, `user` otherwise)

Configure the app itself (via its own env vars, see section 2) to trust these
headers from the reverse proxy in front of it — the container never sees a
Cloudish auth token. For example, OpenWebUI reads
`WEBUI_AUTH_TRUSTED_EMAIL_HEADER=X-Cloudish-User-Email` (and `_ROLE_HEADER`).

## 5. If the app assumes it's deployed at "/"

By default the app runs under a path prefix, not at the root of its own
origin. That's transparent to an app that only ever uses relative paths or
reads its base path from its own environment — most frameworks. It breaks
one specific kind of app: an SPA built with root-absolute asset paths baked
in at build time (no configurable base path), which 404s on its own JS/CSS
once it's not served from `/`. OpenWebUI is the motivating example.

Prefer fixing the app to respect a base path/relative URLs if you can. Only
reach for a subdomain when that's not an option:

```bash
curl -X POST https://cloudish.ai/api/v1/projects/YOUR_ALIAS/my-app/subdomain \
  -H "Authorization: Bearer $CLOUDISH_API_KEY"
# -> { "subdomain": { "url": "https://{identifier}.<run-domain>/" } }
```

`{identifier}` is a random 32-character lowercase-hex string, unique by
construction. If the app is public-facing and a memorable name matters, set
your own instead (non-private projects only):

```bash
curl -X PUT https://cloudish.ai/api/v1/projects/YOUR_ALIAS/my-app/subdomain \
  -H "Authorization: Bearer $CLOUDISH_API_KEY" -H "content-type: application/json" \
  -d '{"label": "anamazingapp"}'
# -> { "subdomain": { "url": "https://anamazingapp.<run-domain>/" } }
```

`label` must be 1-63 characters: lowercase letters, digits and hyphens, not
starting or ending with a hyphen. A `503` on either call means this
deployment has no wildcard run domain configured — the app must work under
the path prefix instead.

## 6. Manage images already in the registry

```bash
curl https://cloudish.ai/api/v1/images/registry -H "Authorization: Bearer $CLOUDISH_API_KEY"
curl -X DELETE "https://cloudish.ai/api/v1/images/registry/tags?repo=builds/123&tag=v1" \
  -H "Authorization: Bearer $CLOUDISH_API_KEY"
```
