This is an agents-first platform — please see /skill.md for the full API reference. For convenience, here are its contents:
--- 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://cloudish.ai/api/v1/keys # -> { "apiKey": { "alias": "funny-monkey", "balance": 0.1, ... }, "key": "cld_...", "claimUrl": "https://..." } ``` 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://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://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://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://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 size, distinct from the build's own). Leave `cpuCores`/`memoryGb` out unless your human asks about cost or performance; sizes and pricing are in https://cloudish.ai/instances.md for then. 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://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://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. 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://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 — a plain browser page load gets redirected to a top-up page instead — see https://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://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. ### Sign-in for your users (OIDC) If your app needs to know who's visiting it, point it at Cloudish's own OpenID Connect provider instead of running your own identity system — a standard discovery document, Authorization Code + PKCE, no client secret to manage: ``` GET https://www.cloudish.ai/.well-known/openid-configuration ``` Many off-the-shelf apps take this directly (e.g. a generic `OIDC_ISSUER`/`OPENID_PROVIDER_URL` setting) with zero code. If you're writing the login flow yourself: redirect a signed-out visitor to `https://www.cloudish.ai/oauth/authorize?client_id=<anything>&redirect_uri=<your subdomain>/callback&response_type=code&code_challenge=<S256 of a random verifier>&code_challenge_method=S256&state=...`, then exchange the `code` your callback receives at `POST https://www.cloudish.ai/oauth/token` (with the PKCE verifier) for an `id_token` carrying `sub`/`email`/`name`. Requires your project to have a subdomain enabled (section 4) — `redirect_uri` must be under it. ## 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" ``` ## 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://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://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 "do" datacenter — also directly reachable at https://do.cloudish.ai, in case https://cloudish.ai is mid-cutover to another datacenter or you specifically want to pin a request to this one. ## 9. Claiming and adding credits Read https://cloudish.ai/credits.md when you perform credit operations — claiming, transfers, or funding more than one project. A key works fine unclaimed — claiming attaches it to a human's own account, which unlocks a dashboard for it (usage, projects, balance) and a way to add credits beyond the free grant. Whether and when that's worth doing is situational: some deployments want a large balance up front, some are fine running on the free grant for a while, some never need more. Bring it up when it's relevant to what you and your human are doing, not on a fixed schedule. `GET /api/v1/credits/claim` (same auth) mints a fresh, single-use link a human can open to attach this key to their own account and add credits to it. Run it yourself and give your human the resulting URL — not the `curl` command for them to run, which they'd need the key from `.env` for: ```bash curl https://cloudish.ai/api/v1/credits/claim -H "Authorization: Bearer $CLOUDISH_API_KEY" # -> { "url": "https://www.cloudish.ai/claim?challenge=..." } # -> tell your human: "To add credits to <alias>, open: https://www.cloudish.ai/claim?challenge=..." ``` The link works once and expires 30 minutes after it's minted, so mint it at the moment you hand it over (the `claimUrl` from `POST /api/v1/keys` has the same lifetime), and mention that it expires. If your human comes back later, or the page says the link expired, just mint a new one — minting never invalidates an earlier link that's still within its own lifetime. Never hand over `$CLOUDISH_API_KEY` itself — the link alone is what proves the claim, the raw key never needs to leave this session.