CloudishbyHal9
FeaturesFor agentsPricingCompareSKILL.mdHelp

SKILL.md, formatted for reading. Agents: fetch the source, https://cloudish.ai/skill.md. It's the same document, and the one to use.

Deploy container apps on Cloudish

Last updated 2026-11-01 (changelog).

Cloudish is an agent-first, API-key/credit, container-deployment platform: it builds images server-side, so no local Docker or docker build is needed — a local build is CPU/memory-heavy and often more than the machine you're 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. Everything from getting a key to deploying takes only the calls below: nothing to install, no account to create.

Never print API keys or secrets into chat, source code or logs. Never report a deploy as succeeded until you've verified it: the build reached succeeded, and the app's logs (section 7) show it started and is listening. Try its URL too if you can reach it, but some environments reach cloudish.ai but not the apps' own domain, so a URL you can't open isn't a failure by itself.

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 new keys or delegate credits from one you have, as fits: a sub-agent or container that needs its own credential gets a key under yours, funded by transfer (credits.md).

curl -X POST https://cloudish.ai/api/v1/keys -H "content-type: application/json" \
  -d '{"alias": "<alias>", "agent": "<agent>", "model": "<model>", "ref": "<ref>"}'

The key comes back in the response:

{ "apiKey": { "alias": "my-app", "balance": 0.1, ... }, "key": "cld_****", "claimUrl": "https://..." }

key authenticates every call below (Authorization: Bearer $CLOUDISH_API_KEY). If you already have a key for this work (from an earlier run, or one your environment provides), reuse it instead of minting another. Claiming is optional: claimUrl attaches this key to the Cloudish account of whoever opens it and signs in, for a dashboard and adding credits. It works once and expires in 30 minutes; GET /api/v1/credits/claim returns a new one (section 9).

alias is optional: a short name for what you're deploying (e.g. todo-app) makes a good one, or leave it out for a random one. You get that name if it's free, or a close variant (e.g. my-app-otter) if not, and it can be renamed later (PATCH /api/v1/me). Use the apiKey.alias that comes back, the {alias} in every project path below.

agent, model and ref are all optional; leave out any you don't know. agent is the product you run in (claude-code, claude, chatgpt, codex, cursor, grok, ...), model the model you are (claude-opus-5-5, gpt-5.5, ...), and ref the plugin, directory or link this document came from (claude-plugin, chatgpt-plugin, codex-plugin, mcp.so, producthunt, ...) — leave it out rather than guess. They only show Cloudish which agents and channels use it.

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:

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:1.0.0", "port": 8080, "alwaysOn": true}'
# -> { "project": { "path": "your-alias/my-app", ... }, "app": { "status": "registered", ... },
#      "subdomain": { "url": "https://<token>.cloudish.app/", ... } }

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:

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 "sleepAfterMinutes=30" -F "context=@context.tar.gz"
# -> { "project": { "path": "your-alias/my-app", ... }, "build": { "id": 123, "status": "pending" } }

Choose how it runs on every deploy, since the right answer depends on the app:

  • "alwaysOn": true runs it around the clock, billed the whole time, and starts it as soon as it's deployed: for a site or API that must answer at once, or a bot or worker that nothing sends requests to.
  • "sleepAfterMinutes": 30 (1 to 1440; 15 if you choose neither) puts it to sleep after that long without requests, billed only while awake; the next request wakes it in seconds. Add "wakeEveryMinutes": 60 (5 to 10080) to also wake it on a schedule: a bot that polls then runs every hour until it sleeps again.

Left out on a redeploy, each keeps the app's own. Change them without a redeploy: PATCH https://cloudish.ai/api/v1/projects/{owner}/{name} with the same fields. An always-on app's runway.alwaysOnHoursLeft says how long its balance keeps it running.

port defaults to 8080 and must be the port your server listens on, whatever the Dockerfile's EXPOSE says: an nginx image listens on 80, so send port=80 for one. Get it wrong and requests answer 503 with "nothing accepts connections on port …".

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:

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: env (JSON object of non-secret vars), volumeEnabled/volumeSizeGb/volumeMountPath (persistent storage), cpuCores/memoryGb (container size, distinct from the build's own), maxBodyMb (the largest request body the app accepts, 64 MB unless set, at most 1024; a bigger one gets 413 before it reaches the app, so raise it for large uploads). To run many isolated copies of the same app — one sandbox per user, task or agent — see https://cloudish.ai/replicas.md.

Left out, a new app gets the default size, shared: billed for 0.1 CPU and 0.2 GB, using up to 1 CPU and 1 GB when the machine has them spare, and the first to restart when it runs short. Apps that must not restart, run a database inside the container, or need their full memory all the time (a Next.js server, Streamlit) run better on small or larger. Left out on an existing app, its size stays as it is. The response's app.resources shows the size an app runs at and its creditsPerHour. Sizes and pricing: https://cloudish.ai/instances.md.

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. It's https://<name>.cloudish.app/ and keeps working whichever datacenter serves the app: save and share that one. See section 4 to swap the random URL for a memorable one, rotate it, or turn it off.

To take a deployment down, DELETE /api/v1/projects/{owner}/{name} stops the container and its billing, and deploying the same name again later starts fresh. A volume is deleted with the project, so its data is gone.

2. Environment variables and secrets

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

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:1.0.0", "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:

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-..."}'

For human login, OIDC authentication is available (section 5).

Several at once take the same shape as env: -d '{"OPENAI_API_KEY": "sk-...", "STRIPE_KEY": "sk_live_..."}'. Saving or deleting secrets restarts a running app with the new values; an idle one gets them when it next starts. GET …/secrets lists names (never values); DELETE …/secrets/NAME removes one.

3. Persistent storage

Request a volume when creating or updating the app:

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:1.0.0", "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 container, so an app with one can't run replicas.

4. Customize the subdomain, or use your own domains

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):

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.cloudish.app/", ... } }

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. Names are global: a 409 means another datacenter's app already has this one, so pick another. 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 (the default) or public project also answers with no credential at all, billed to its owner. 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. A sleeping app (section 1) starts on the next request.

Your own domains

A project can also answer at any number of domains you control. Add each one (foobar.com, www.foobar.com, ...) to the same project:

curl -X POST https://cloudish.ai/api/v1/projects/YOUR_ALIAS/my-app/domains \
  -H "Authorization: Bearer $CLOUDISH_API_KEY" -H "content-type: application/json" \
  -d '{"host": "foobar.com"}'
# -> { "domain": { "host": "foobar.com", "status": "pending", "redirect": false,
#      "txtRecord": { "name": "_cloudish-challenge.foobar.com", "value": "cloudish-verify=..." } } }
  1. Add the returned TXT record at your DNS provider. It's checked every minute; once it matches, the domain turns active and stays that way. It's only checked once, so the record can be removed afterwards.
  2. Point the domain at do.cloudish.ai: a CNAME for a subdomain like www; an apex (foobar.com) can't be a CNAME at most providers, so use an A record to that host's IP address (dig +short do.cloudish.ai), or your provider's CNAME flattening/ALIAS if it has one. The certificate issues once both steps are done.
  3. To have one domain send visitors to the other instead of serving the app twice, add it with "redirect": true (or PATCH .../domains/www.foobar.com {"redirect": true} later). It 308s to the project's primary domain: its oldest active domain that doesn't redirect, or its subdomain if there's none.

GET .../domains lists them; DELETE .../domains/{host} removes one. Each active domain costs 0.01 credits a day. Adding one needs a positive balance; at most 10 can be added per hour and 20 be pending at once. A 409 means another project already holds that host.

A request to an out-of-credit project returns 402 (JSON) instead of starting the container — a plain browser page load gets a 503 status 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, for a sub-agent's key, get a hand from the parent key that minted it — the parent transfers some of its balance to yours by alias, authenticating with its own key, no admin needed: curl -X POST https://cloudish.ai/api/v1/credits/transfer -H "Authorization: Bearer $PARENT_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, adding no CORS headers of its own: set them in your app if a browser on another origin calls it. How containers are isolated: https://cloudish.ai/container.md. 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 — add the postgresql package to your image. On container start (not at build time), initialize a subdirectory of the mounted volume with initdb if it isn't initialized yet, start the server with 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.

Authentication: user sign-in and admin panels (OIDC)

If your app needs to know who's visiting it — an end-user login, or a single admin panel — 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. redirect_uri must be under your project's subdomain or one of its active custom domains (section 4).

Authenticating the visitor is not the same as authorizing them: any Cloudish user can complete this flow, so an admin panel still needs its own allowlist of email/sub claims and must reject everyone else. The platform adds no trusted headers to the request (section 4) — verify the id_token in your app.

Build redirect_uri from the host the request came in on, not from a fixed URL. An app can answer on more than one hostname (apps created before their URLs moved to cloudish.app also keep their older *.on.<datacenter>.cloudish.ai one), and a sign-in has to come back to the hostname it started on, where your app set its state cookie.

Your app's "Sign out" only ends your app's own session. To also let the visitor sign out of Cloudish (so their next sign-in can pick another account), send them afterwards to https://www.cloudish.ai/oauth/logout?id_token_hint=<their id_token>&post_logout_redirect_uri=<your subdomain>/&state=... — Cloudish asks them, then returns them to post_logout_redirect_uri (same rule as redirect_uri).

6. Manage images already in the registry

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:

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. Datacenter

This instance is currently running in the "do" datacenter — also directly reachable at https://do.cloudish.ai. We are planning expansion to other datacenters.

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.

Credits are spent only while an app runs: one that sleeps (section 1) costs nothing until it wakes, and an always-on one spends around the clock. More can be added at any time: through the claim link below (for a key already in an account, from that account), or by a transfer from another key. Deploy, /me and credits responses carry runway: the balance and the creditsPerHour being spent.

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:

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.


Every change to this document is listed in changelog.md.

CloudishbyHal9

The Cloud for Agents. Get a key, deploy a Dockerfile, get a URL.

FeaturesFor agentsPricingCompareLogoDashboardHelp
SKILL.mdInstancesCreditsReplicasIsolationChangelog
© 2026 CloudishTermsPrivacyCreated by Hal9 Inc, Redmond, WA