---
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, so skip installing Docker and running `docker
build` locally — that's CPU/memory-heavy and often more than the machine
you're running on can spare, and there's no registry to manage either way.
Cloudish 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

Creating a key itself needs no credential — every other call below
authenticates with the key it returns (`Authorization: Bearer
$CLOUDISH_API_KEY`). New keys include 0.1 credits to start, subject to availability. Mint as many keys as needed, the same way — a runtime child that wants its
own credential rather than reusing yours just calls this again, no auth
required either.

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

Save `key` as `$CLOUDISH_API_KEY` — every call below authenticates with it.
If `./.env` already has a `CLOUDISH_API_KEY`, reuse that instead of minting
a new one. Optionally pass `{"alias": "<project-folder-name>"}` — you get
that name if it's free, or a close variant (e.g. `<name>-otter`) if not;
use whatever `apiKey.alias` comes back, which becomes the `{alias}` in
every project path below.

Persist it somewhere durable before doing anything else — losing it means
starting over with a new key and balance. A local `.env` file works well
(`echo "CLOUDISH_API_KEY=$CLOUDISH_API_KEY" >> .env`); just make sure that
file is gitignored (`echo .env >> .gitignore`) so it never ends up
committed by accident.

## 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://anton.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", ... },
#      "subdomain": { "url": "https://<token>.<run-domain>/" } }
```

**Build from source** — upload a tar.gz build context (must contain a
`Dockerfile` at its root). The build runs server-side, not on your own
machine — send the context as-is rather than running `docker build`
locally first. Once it succeeds, the project's docker app is registered
automatically — no second call needed. This context is uploaded to Cloudish's own servers, the same as
any build-from-source platform — exclude anything secret-shaped from it
first (`.env` files, credentials, `.git`) and use section 2's encrypted
secrets endpoint for real credentials instead, never a file baked into the
context:

```bash
tar --exclude='.env*' --exclude='.git' -czf context.tar.gz .
curl -X POST https://anton.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: 3`/
`memoryGb: 5` by default. If it dies
with no error beyond `"Job has reached the specified backoff limit"`, that's
usually resource exhaustion rather than 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://anton.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 and its subdomain already provisioned — fetch
`GET https://anton.cloudish.ai/api/v1/projects/{owner}/{name}` for the resulting
`subdomain.url`, no separate subdomain 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).

Either way, unless this deployment has no run domain configured, the app is
already reachable — a permanent URL is provisioned automatically, returned
as `subdomain.url` above. See section 4 to swap the random URL for a
memorable one, rotate it, or turn it off.

## 2. Environment variables and secrets

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

```bash
curl -X POST https://anton.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 — API keys, tokens, passwords — is set separately here,
encrypted at rest, and merged into the container's environment
automatically. Never put these in `env` above, and never bundle them into
the build context in section 1 either:

```bash
curl -X PUT https://anton.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://anton.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. Customize the subdomain

Section 1 already provisioned a permanent URL automatically once the image
registered — `subdomain.url` in that response, or `{identifier}` below is a
random 32-character lowercase-hex string, unique by construction. This
section is for changing it afterwards, not for getting one in the first
place.

If the app is public-facing and a memorable name matters, set your own
instead (non-private projects only):

```bash
curl -X PUT https://anton.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 this call means this
deployment has no wildcard run domain configured. Leaked the URL? `POST
.../subdomain/rotate` issues a fresh one. Want the app unreachable from the
outside entirely (API-only)? `DELETE .../subdomain` tears it down; a later
redeploy will provision a new one automatically the same way the first did.

A request to the subdomain 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. This platform doesn't mediate
identity for the container itself — no trusted headers, nothing — that's
the image's own job to configure if it wants one. 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 to an out-of-credit project returns `402` (JSON) instead of
starting the container — see https://anton.cloudish.ai/credits.md for adding credits, especially across
several projects. Ask the platform admin to grant more via `POST /admin/v1/credits/grant`,
or get a hand from any other key that already has a balance — its owner
transfers some of it to yours by alias, authenticating with their own key,
no admin needed:
`curl -X POST https://anton.cloudish.ai/api/v1/credits/transfer -H "Authorization: Bearer
$THEIR_API_KEY" -H "content-type: application/json" -d
'{"to":"<your-alias>","amount":1}'`.

## 5. Building containers — any language, any framework

Cloudish doesn't care what's inside the image — it just runs whatever
listens on `port` and reverse-proxies to it. Bring any Dockerfile:

- **JavaScript/TypeScript** — Express, Fastify, NestJS, or a Next.js app
  in standalone/server mode.
- **Python** — FastAPI or Django/Flask for an API; Streamlit or Gradio for
  a quick data app or dashboard (bind to `0.0.0.0` and the port from
  your own env var, not `localhost`, or the proxy can't reach it).
- **Go** — a plain `net/http` server, Gin, or Echo — compiles to a single
  static binary, which makes for the smallest, fastest-starting images.

Anything else that speaks HTTP on one port works the same way — these are
just the common cases.

### A real database, without a managed database service

Cloudish has no managed Postgres/MySQL offering — attach a volume (section
3) and run the database *inside your own container* instead:

- **SQLite** — simplest option for most apps: point your app at a file
  under the mounted volume (e.g. `$DATA_DIR/app.db`). No extra process.
- **Postgres** — install `postgresql` in your image, and on container
  start (not at build time): run `initdb` into a subdirectory of the
  mounted volume if it isn't initialized yet, start `pg_ctl`, then
  idempotently create your role/database if missing (this runs on every
  boot, so it has to be a no-op after the first). Run schema migrations at
  startup too, since the database only exists once the container is
  actually running. Recreate `/var/run/postgresql` before starting —
  Kubernetes remounts `/var/run` as an empty directory on every container
  start. Generate any long-lived secret (session signing key, etc.) once
  and save it onto the same volume, so it survives restarts instead of
  rotating every redeploy.

## 6. Manage images already in the registry

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

## 7. Troubleshoot a deployment

Fetch the running container's own stdout/stderr on demand — pulled fresh
from the pod on every call, never persisted server-side, so polling it while
diagnosing an issue doesn't grow anything:

```bash
curl https://anton.cloudish.ai/api/v1/docker/YOUR_ALIAS/my-app/logs -H "Authorization: Bearer $CLOUDISH_API_KEY"
# -> { "logs": "--- container logs ---\n...\n--- events ---\n..." }
```

Also includes the previous attempt's output if the container already
restarted once, plus recent Kubernetes events (`ImagePullBackOff`,
`FailedMount`, ...) — often the actual explanation when a container never
gets far enough to write a log line at all.

## 8. Server info

Deployed version, uptime and database status, for troubleshooting:

```bash
curl https://anton.cloudish.ai/health
# -> { "ok": true, "service": "cloudish-server", "env": "prod", "version": "<deployed commit sha>", "uptimeSeconds": ..., "database": "ok", "runtime": {...} }
```

This instance is currently running in the "anton" datacenter — also directly reachable at https://anton.cloudish.ai, in case https://anton.cloudish.ai is mid-cutover to another datacenter or you specifically want to pin a request to this one.
