# Cloider documentation > Cloider documentation: serverless Kubernetes, apps, managed PostgreSQL, MySQL, MongoDB and Redis, S3 object storage, container registry, domains and DNS. Guides and examples for the dashboard, CLI, Terraform, REST API and MCP. This file contains all documentation content in a single document following the llmstxt.org standard. ## What is Cloider? Cloider is a cloud platform where you build, deploy and scale applications without managing servers. It gives you serverless Kubernetes clusters, an App Engine that turns a git repository or a folder into a live HTTPS URL, managed PostgreSQL, MySQL, MongoDB and Redis, S3 compatible object storage, a private container registry, and domains with authoritative DNS. You pay only for what your resources use, and you can drive all of it from the dashboard, the `cloider` CLI, Terraform, a REST API or an AI agent over MCP. These docs explain every product and every tool, from your first deploy to the exact field of an API response. Each how-to shows the same task in every tool, so you can follow along in the one you prefer. ### At a glance | | | | --- | --- | | **What it is** | An all-in-one cloud platform: compute, data, storage and networking on one account and one bill | | **Pricing** | True pay-as-you-go, metered on what you actually use. Every account starts with a 14 day free trial and $25 in free credit. See [pricing](/reference/pricing) | | **Products** | [Apps](/apps/overview), [Kubernetes](/kubernetes/overview), [Databases](/databases/overview), [Object Storage](/object-storage/overview), [Container Registry](/container-registry/overview), [Domains and DNS](/domains/overview) | | **Tools** | [Dashboard](/get-started/dashboard-tour), [CLI](/cli/overview), [Terraform](/terraform/overview), [REST API](/api/overview), [MCP server](/mcp/overview) | | **Standards** | Standard Kubernetes API, S3 compatible storage, OCI container images, plain PostgreSQL, MySQL, MongoDB and Redis wire protocols. No lock-in and no egress fees | | **Who it is for** | Developers and teams who want production infrastructure without a DevOps team, and coding agents that deploy on their behalf | ### Products Push code, get a live HTTPS URL. Cloider detects your stack and builds it with your Dockerfile or buildpacks, and can redeploy on every push. Fully managed clusters ready in a few minutes. No nodes to size or patch, billed on the CPU and memory your workloads use. PostgreSQL, MySQL, MongoDB and Redis with automatic backups, SSL and trusted source allowlists. S3 compatible buckets for files, backups, static assets and static websites, with versioning and lifecycle rules. Private images at registry.cloider.com, with scoped tokens, retention policies and immutable tags. Free subdomains or your own domain, authoritative DNS, and automatic HTTPS for apps, cluster services and bucket websites. ### Tools Everything you can do in the dashboard you can also do from code. Pick the tool that fits the job, or mix them: a resource created in one shows up in all of them. The web console at cloider.com/dashboard. The fastest way to start and to see everything at once. The `cloider` command for your terminal, scripts and CI. One command deploys a folder. The `cloider/cloider` provider for Terraform and OpenTofu. Infrastructure as code, reviewed in pull requests. JSON over HTTPS with API tokens. Build your own integrations and automation. `cloider mcp` lets Claude, Cursor, Copilot and other AI agents manage your infrastructure, with read only modes. End to end tutorials: a full stack app, WordPress on Kubernetes, CI/CD, Terraform and more. ### How Cloider fits together An account holds every resource you create. Members of the account sign in to the dashboard, while the CLI, Terraform, the API and the MCP server act with an API token that belongs to the account. Apps and clusters are reached through domains, and they talk to databases, buckets and the registry over the network like any other client. ```mermaid flowchart LR subgraph You D[Dashboard] C[CLI] T[Terraform] A[REST API] M[MCP / AI agent] end subgraph Account["Your Cloider account"] APP[Apps] K8S[Kubernetes clusters] DB[(Databases)] S3[(Buckets)] REG[(Container registry)] DNS[Domains and DNS] end D & C & T & A & M --> Account DNS --> APP DNS --> K8S APP --> DB K8S --> DB APP --> S3 K8S --> S3 REG --> K8S REG --> APP ``` Read [how Cloider works](/concepts/how-cloider-works) for the full picture, and [the billing model](/concepts/billing-model) for how usage turns into an invoice. ### Where to start - **New to Cloider?** Follow the [quick start](/quick-start). In about five minutes you sign up, deploy an app to a live URL and, if you like, create a Kubernetes cluster. - **Moving an existing project?** Read [migrate to Cloider](/guides/migrate-to-cloider). - **Automating?** Install the [CLI](/cli/install), create an [API token](/account/api-tokens), then use [Terraform](/terraform/get-started) or the [API](/api/overview). - **Working with an AI coding agent?** Connect it with the [MCP server](/mcp/setup) and point it at [llms.txt](/mcp/ai-coding-agents). ### How to read these docs - **Interface tabs.** Tasks are shown in tabs for the Dashboard, CLI, Terraform, API and MCP. Pick one and every page remembers your choice. - **Beginner friendly.** Concepts are explained the first time they appear, and the [glossary](/reference/glossary) defines every term. - **Copy and paste.** Every example is complete. Names like `blog`, `staging` or `orders-db` are examples you can replace with your own. - **Built for AI.** Every page has a Markdown version at the same URL plus `.md`, the whole site is in [`/llms.txt`](pathname:///llms.txt) and [`/llms-full.txt`](pathname:///llms-full.txt), and the buttons under each title copy the page or open it in Claude or ChatGPT. --- ## Cloider quick start, deploy an app in 5 minutes This quick start takes you from no account to a live app with its own HTTPS address, and then (optionally) to a serverless Kubernetes cluster you control with `kubectl`. It takes about five minutes, and every step works from the dashboard or from the `cloider` command line tool. **At a glance** | | | | --- | --- | | **You will** | Sign up, deploy a small web app, open it at `https://.cloider.app`, optionally create a Kubernetes cluster and run `kubectl get nodes` | | **Time** | About 5 minutes, plus a few minutes while the cluster is created | | **Cost** | New accounts start on a 14-day free trial with $25 of free credit (see [the billing model](/concepts/billing-model)) | | **You need** | A Google or GitHub account, and a web browser. For the CLI path, a terminal on macOS, Linux or Windows | ### Before you begin - You need a **Google** or **GitHub** account. Cloider has no separate password: you sign in with one of those two providers. - For the **CLI** path, you need a terminal with `curl` (macOS and Linux) or PowerShell (Windows), see [Install the CLI](/get-started/install-the-cli). - For the optional Kubernetes step, install [`kubectl`](https://kubernetes.io/docs/tasks/tools/), the standard Kubernetes command line tool. :::tip[Pick a tool and stick with it] Every step below is shown in tabs: **Dashboard**, **CLI**, **Terraform**, **API** and **MCP** (an AI agent). The tab you pick is remembered on every page of these docs. If you are not sure which to use, start with the Dashboard, or read [Choose a tool](/get-started/choose-a-tool). ::: ### Step 1: Create your Cloider account You create a Cloider account by signing in with Google or GitHub on [cloider.com](https://cloider.com). The first sign in creates the account, and later sign ins use the same button. 1. Go to [cloider.com](https://cloider.com) and click **Login / Register** in the top right corner. 2. In the **Login Or Register** dialog, click **Sign in with Google** or **Sign in with Github**. 3. Approve the request on the Google or GitHub page. You land on the Cloider dashboard. A new account is on the free trial. A banner at the top of the dashboard shows the date the trial ends. During the trial you can have one Kubernetes cluster, one database, one bucket and one registry project at a time. Read [Create an account](/get-started/create-an-account) for the details. ### Step 2: Install the CLI (CLI path only) Skip this step if you use the dashboard. The `cloider` CLI is a single binary for macOS, Linux and Windows. ```bash curl -fsSL https://cloider.com/install.sh | sh ``` ```text title="Output" Downloading cloider 1.4.0 for darwin/arm64 Installed cloider 1.4.0 to /usr/local/bin/cloider Get started with: cloider login ``` Then log in. `cloider login` opens your browser on the **Authorize Cloider CLI** page, where you pick the account and the access level (**Read and write** or **Read only**), then click **Authorize CLI**. ```bash cloider login ``` ```text title="Output" Opening the browser to log in. If it doesn't open, visit: https://cloider.com/cli/login?port=53682&state=... ✓ Logged in as jane@example.com in account jane@example.com (Read & Write) ``` Choose **Read and write** here: a read only token can't deploy anything. More options, such as `--no-browser` for a remote machine, are in [Install the CLI](/get-started/install-the-cli). ### Step 3: Deploy your first app An **app** is your code, built into a container image and run by Cloider on a free subdomain with HTTPS. You don't write a Dockerfile or configure servers: Cloider detects the language and framework, builds the code and starts it. Read [Apps overview](/apps/overview) for everything apps can do. Create a small Node.js app to deploy. Any folder with code works, including a folder with a single `index.html`, but this example shows a real server: ```bash mkdir hello && cd hello ``` ```json title="package.json" { "name": "hello", "version": "1.0.0", "scripts": { "start": "node server.js" } } ``` ```js title="server.js" const http = require('http'); // Cloider tells the app which port to listen on in the PORT variable const port = process.env.PORT || 8080; http .createServer((req, res) => { res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('Hello from Cloider!\n'); }) .listen(port, () => console.log(`Listening on ${port}`)); ``` Now deploy it: **Dashboard** 1. In the dashboard sidebar, open **Apps** and click **Create App**. 2. On the **Bring your own code** step, drag the `hello` folder onto the box that says *Drag and drop your project folder, zip file, or a single HTML file to deploy instantly*, or click **choose a folder** and pick it. You can also import a repository from **GitHub**, **GitLab** or **Bitbucket** here instead (see [Deploy from Git](/apps/deploy-from-git)). 3. Cloider uploads and inspects the code. On the **Configure your app** step it shows what it found under *We had a look at your code*. 4. Check the **App Name** (for example `hello`) and the **Port** (`8080` for this example). 5. Leave **Give this app a free subdomain** switched on and pick an address in **Subdomain**, for example `hello-acme`. The form shows *hello-acme.cloider.app is available* when the name is free. 6. Click **Create App**. The app page opens and shows the build. When the status reaches **Ready**, the app's URL is live. **CLI** Run `cloider deploy` in the folder. The first deploy zips the folder, detects the language, framework and port, creates the app, claims a free subdomain and builds it: ```bash cloider deploy --name hello ``` ```text title="Output" Packaged 2 files (0.4 KB) Detected JavaScript ✓ Created app hello on port 8080 Linked . to hello in .cloider/app.json, later deploys from here update it. ... ✓ Deployed version 1 of hello https://hello-7kq2.cloider.app ``` Build output is printed to stderr and the app's URL to stdout, so `URL=$(cloider deploy)` captures just the URL in a script. The folder is now linked to the app in `.cloider/app.json`: run `cloider deploy` again after a change and it builds version 2 of the same app. Useful flags: `--subdomain hello-acme` picks the subdomain, `--env KEY=VALUE` sets a runtime variable, and `--detach` returns as soon as the build starts. See [Deploy from your computer](/apps/deploy-from-your-computer). **Terraform** The Terraform provider doesn't manage apps. Deploy the app with the **Dashboard** or **CLI** tab, and use Terraform for clusters, databases, buckets, registry projects, domains, DNS records and targets (see [Terraform overview](/terraform/overview)). **API** Deploying an app over the REST API is a sequence of calls, which `cloider deploy` makes for you: 1. `POST https://cloider.com/api/apps/source` with the archive's `name`, `type` and `size` answers with a `token` and a `presigned_url`. 2. `PUT` the zip of your code to the `presigned_url`. 3. `POST https://cloider.com/api/apps/source//detect` detects the language, framework and port. 4. `POST https://cloider.com/api/apps` with `app_name`, `port`, `subdomain` and `source_token` creates the app and starts the first build. Every call needs a Read & Write API token in the `Authorization: Bearer $CLOIDER_TOKEN` header. The full request and response shapes are in [Apps endpoints](/api/endpoints/apps). For a first deploy, the CLI is much quicker. **MCP** With the [Cloider MCP server](/mcp/overview) set up in your AI agent (it runs `cloider mcp`, so log in with the CLI first), ask: ```text title="Prompt" Deploy the folder /Users/jane/hello as a new app called hello and tell me its URL. ``` It calls the `deploy_app` tool: ```json title="Tool call: deploy_app" {"dir": "/Users/jane/hello", "name": "hello"} ``` The tool follows the build until the app is up and answers with the app's URL. #### Open your app Open the URL in a browser, or use `curl`: ```bash curl https://hello-7kq2.cloider.app ``` ```text title="Output" Hello from Cloider! ``` The first deploy takes longer than a page load, because Cloider builds a container image from your code before it starts it. If the build fails, the build log on the app's **Builds** tab (or `cloider app build logs hello`) says why; see [Apps troubleshooting](/apps/troubleshooting). ### Step 4 (optional): Create a Kubernetes cluster A Cloider **Kubernetes cluster** is a standard Kubernetes API that you reach with `kubectl`, Helm or any Kubernetes tool. It is serverless: you don't size or manage nodes, and you pay for the CPU and memory your workloads use. Read [Kubernetes overview](/kubernetes/overview) for how it works. **Dashboard** 1. Open **Kubernetes** in the sidebar and click **Create Cluster**. 2. Type a **Cluster Name**, for example `staging`. Use lowercase letters, digits and dashes, starting with a letter. 3. Leave **Cluster Version** on the newest version, or pick another. 4. Click **Create cluster**. 5. Wait until the cluster's status is **Ready**. It goes through **Creating** and **Starting** first, which takes a few minutes. 6. Click the menu button (three dots) at the end of the cluster's row and choose **Kubeconfig** to download its kubeconfig file (saved as `config`). Then point `kubectl` at the downloaded file: ```bash export KUBECONFIG=~/Downloads/config kubectl get nodes ``` **CLI** ```bash cloider cluster create staging --wait ``` ```text title="Output" ✓ Creating cluster staging (Kubernetes 1.34) ✓ Cluster staging is ready. Get its kubeconfig with `cloider cluster kubeconfig staging --merge`. ``` Merge the cluster into your kubeconfig and make it the current context: ```bash cloider cluster kubeconfig staging --merge --use kubectl get nodes ``` ```text title="Output" ✓ Added context cloider-staging to /Users/jane/.kube/config ``` `--merge` adds the cluster to `~/.kube/config` (or the first file in `$KUBECONFIG`) as the context `cloider-staging`. Without `--merge`, the command prints the kubeconfig so you can save it anywhere: `cloider cluster kubeconfig staging > staging.yaml`. **Terraform** ```hcl title="main.tf" terraform { required_providers { cloider = { source = "cloider/cloider" } } } # Uses CLOIDER_TOKEN, or the token `cloider login` saved provider "cloider" {} resource "cloider_cluster" "staging" { name = "staging" } # Writes a kubeconfig for kubectl resource "local_sensitive_file" "kubeconfig" { filename = "${path.module}/kubeconfig-staging.yaml" content = cloider_cluster.staging.kube_config.raw file_permission = "0600" } ``` ```bash terraform init terraform apply KUBECONFIG=./kubeconfig-staging.yaml kubectl get nodes ``` `terraform apply` waits until the cluster is `Ready` (up to 20 minutes by default). See [Terraform get started](/terraform/get-started). **API** Create the cluster: ```bash curl -X POST https://cloider.com/api/clusters \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"cluster_name": "staging", "cluster_version": "1.34"}' ``` ```json title="Response" {"hashed_id": "q7m2x"} ``` Poll its status until it is `Ready` (the answer is plain text), then download the kubeconfig: ```bash curl -H "Authorization: Bearer $CLOIDER_TOKEN" https://cloider.com/api/clusters/q7m2x/status curl -H "Authorization: Bearer $CLOIDER_TOKEN" https://cloider.com/api/clusters/q7m2x/kubeconfig -o staging.yaml KUBECONFIG=./staging.yaml kubectl get nodes ``` The versions you can pass as `cluster_version` are listed by `GET /api/options`. See [Clusters endpoints](/api/endpoints/clusters). **MCP** Ask your agent: ```text title="Prompt" Create a Kubernetes cluster called staging, wait until it is ready, and add it to my kubeconfig as the current context. ``` It calls `create_cluster`, then `get_cluster_kubeconfig`: ```json title="Tool call: create_cluster" {"name": "staging", "wait": true} ``` ```json title="Tool call: get_cluster_kubeconfig" {"cluster": "staging", "merge": true, "use": true} ``` When `kubectl get nodes` answers, `kubectl` is talking to your new cluster. You're ready to deploy: follow [Deploy a workload](/kubernetes/deploy-a-workload) for a first Deployment and Service, and [Expose services](/kubernetes/expose-services) to give it a public address. ### Step 5: Clean up (optional) Resources are billed while they exist, so delete what you created if you don't need it. Deleting is permanent. **Dashboard** - **App**: in **Apps**, click the menu button (three dots) at the end of the app's row and choose **Delete**. See [Pause, resume and delete an app](/apps/pause-resume-and-delete). - **Cluster**: in **Kubernetes**, click the menu button at the end of the cluster's row, choose **Delete** and type the cluster name to confirm. See [Delete a cluster](/kubernetes/delete-a-cluster). **CLI** ```bash cloider app delete hello cloider cluster delete staging --wait ``` Each command asks you to type the name to confirm. In a script, add `--yes` to skip the question. **Terraform** ```bash terraform destroy ``` The app was created outside Terraform, so delete it with the Dashboard or CLI tab. **API** ```bash curl -X DELETE -H "Authorization: Bearer $CLOIDER_TOKEN" https://cloider.com/api/clusters/q7m2x ``` Apps are deleted with `DELETE /api/apps/`; see [Apps endpoints](/api/endpoints/apps). **MCP** ```text title="Prompt" Delete the app hello and the cluster staging. ``` The agent calls `delete_app` and `delete_cluster`. Both tools are marked as deleting something, so your MCP client can ask you before it runs them. You can also **pause** a cluster or an app instead of deleting it. A paused cluster keeps its state and isn't billed for compute. See [Resource lifecycle](/concepts/resource-lifecycle). ### Next steps - [Deploy your first app](/get-started/deploy-your-first-app): the same app step by step, with environment variables, logs and redeploys. - [Create your first cluster](/get-started/create-your-first-cluster): a full walkthrough from cluster to a public service. - [Create a database](/databases/create-a-database): add managed PostgreSQL, MySQL, MongoDB or Redis to your app. - [Dashboard tour](/get-started/dashboard-tour) and [Choose a tool](/get-started/choose-a-tool): find your way around. --- ## Choose a tool, dashboard, CLI, Terraform, API or MCP You can manage Cloider from five tools: the **dashboard** in your browser, the **`cloider` CLI**, the **Terraform provider**, the **REST API** and the **MCP server** for AI agents. They all act on the same account and the same resources, so you can mix them freely: create a cluster in the dashboard, script it with the CLI and keep its database in Terraform. ### At a glance | | Dashboard | CLI | Terraform | REST API | MCP | | --- | --- | --- | --- | --- | --- | | **Best for** | Exploring, one-off tasks, billing, team management | Terminals, scripts, CI pipelines, deploying apps | Infrastructure as code, repeatable environments | Custom integrations in any language | Asking an AI agent to do the work | | **Signs in with** | Google or GitHub | An API token (`cloider login` creates one) | An API token | An API token as a bearer token | The CLI's API token | | **Install** | Nothing, open [cloider.com/dashboard](https://cloider.com/dashboard) | `curl -fsSL https://cloider.com/install.sh \| sh` | `source = "cloider/cloider"` in `required_providers` | Nothing, any HTTP client | The CLI, then add `cloider mcp` to your agent | | **Apps** | Yes | Yes | No | Yes | Yes | | **Clusters, databases, buckets, registry, domains, DNS, targets** | Yes | Yes | Yes | Yes | Yes | | **Support requests** | Yes | Yes | No | Yes | Yes | | **Usage and billing** | Yes | Yes (read) | No | Yes (read) | Yes (read) | | **Members and API tokens** | Yes | List only | No | List only | List only | | **Run commands in apps, clusters, databases** | Yes (browser terminal) | Yes (`shell`, `exec`) | No | `exec` endpoints | `exec` tools | | **Docs** | [Dashboard tour](/get-started/dashboard-tour) | [CLI](/cli/overview) | [Terraform](/terraform/overview) | [REST API](/api/overview) | [MCP](/mcp/overview) | :::info[Members and API tokens are dashboard only] Inviting members, changing roles and creating or deleting API tokens only work in a signed in dashboard session. The API refuses them for an API token with `This endpoint can't be used with an API token!`, so a leaked token can never give out more access. The CLI, the API and the MCP server can list members and tokens. ::: ### The same task in every tool Here is one small task, creating a Kubernetes cluster called `staging` and checking that it is ready, done in each tool. The tabs on every page of these docs follow the same order, and your choice is remembered. **Dashboard** 1. Open **Kubernetes** in the sidebar and click **Create Cluster**. 2. Type `staging` in **Cluster Name** and click **Create cluster**. 3. Watch the status in the list change from **Creating** to **Starting** to **Ready**. **CLI** ```bash cloider cluster create staging --wait cloider cluster list ``` ```text title="Output" ✓ Creating cluster staging (Kubernetes 1.34) ✓ Cluster staging is ready. Get its kubeconfig with `cloider cluster kubeconfig staging --merge`. NAME VERSION STATUS CREATED staging 1.34 Ready 2026-10-20 09:12 ``` Add `-o json` to any command for machine readable output. **Terraform** ```hcl title="main.tf" terraform { required_providers { cloider = { source = "cloider/cloider" } } } provider "cloider" {} resource "cloider_cluster" "staging" { name = "staging" } output "staging_status" { value = cloider_cluster.staging.status } ``` ```bash export CLOIDER_TOKEN=cldr_xxxxxxxxxxxxxxxxxxxxxxxx terraform init terraform apply ``` `terraform apply` returns once the cluster is `Ready`. **API** ```bash curl -X POST https://cloider.com/api/clusters \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"cluster_name": "staging", "cluster_version": "1.34"}' ``` ```json title="Response" {"hashed_id": "q7m2x"} ``` ```bash curl -H "Authorization: Bearer $CLOIDER_TOKEN" https://cloider.com/api/clusters/q7m2x/status ``` ```text title="Response" Ready ``` **MCP** Ask your agent: ```text title="Prompt" Create a Kubernetes cluster called staging and wait until it is ready. ``` It calls the `create_cluster` tool: ```json title="Tool call: create_cluster" {"name": "staging", "wait": true} ``` ### The dashboard The dashboard is the web interface at [cloider.com/dashboard](https://cloider.com/dashboard). You sign in with Google or GitHub, and nothing needs to be installed. **Use it when** you are exploring Cloider, doing something once, looking at charts (usage, insights), reading invoices, managing members and API tokens, or using the browser terminal. **It can't** be scripted. For repeatable work, use one of the other tools. Start with the [Dashboard tour](/get-started/dashboard-tour). ### The CLI The `cloider` CLI is a single binary for macOS, Linux and Windows that does from a terminal what the dashboard does. It authenticates with an API token, which `cloider login` creates for you through the browser. ```bash curl -fsSL https://cloider.com/install.sh | sh cloider login cloider deploy ``` **Use it when** you deploy apps from your computer (`cloider deploy`), work in a terminal, write shell scripts, or run Cloider commands in CI. It has `-o json`, `-o yaml` and `-q` output for scripts, `--wait` to block until a change is done, and documented exit codes. **It can't** invite members or create API tokens (it can list them), because the API keeps those to the dashboard. Read [Install the CLI](/get-started/install-the-cli), then the [CLI overview](/cli/overview). ### Terraform The `cloider/cloider` provider for Terraform and OpenTofu manages Cloider resources as code: Kubernetes clusters, databases, buckets and their access keys, registry projects and their tokens, domains, DNS records and targets. **Use it when** you want your infrastructure reviewed in pull requests, reproduced across environments (staging and production) and wired to other providers, such as the `kubernetes` and `helm` providers pointed at a Cloider cluster. **It can't** manage apps, support requests, members or API tokens. Deploy apps with the CLI or the dashboard. A **Read** token is enough for `terraform plan`, while `terraform apply` needs a **Read & Write** token. Read [Terraform get started](/terraform/get-started). ### The REST API Everything the dashboard and the CLI do goes through the same REST API at `https://cloider.com/api`. You call it with an API token in the `Authorization: Bearer` header. Requests and responses are JSON, and errors come back as: ```json title="Error response" {"errors": {"cluster_name": ["Cluster name can not be upper case"]}, "data": null} ``` **Use it when** you build your own integration in a language without a Cloider SDK, or need something the CLI doesn't expose. **Mind the rate limit**: 10 requests a second and 100 a minute per client. The CLI retries for you; your own code should back off on HTTP `429`. Read the [REST API overview](/api/overview) and [Rate limits](/api/rate-limits). ### MCP and AI agents The MCP server lets an AI agent (Claude Code, Claude Desktop, Cursor, VS Code, Windsurf and others) manage Cloider by calling tools. It is part of the CLI: `cloider mcp` serves one tool per CLI command, using the token the CLI is logged in with. ```bash cloider login claude mcp add cloider -- cloider mcp ``` **Use it when** you want to describe what you need in plain language ("deploy this folder", "why is my build failing?", "what did I spend this month?") and let the agent run the commands. **Keep it safe**: `cloider mcp --read-only` only offers tools that change nothing, and every tool says whether it reads, writes or deletes, so your client can ask before a delete. Read the [MCP overview](/mcp/overview) and [Safety and permissions](/mcp/safety-and-permissions). ### API tokens for the CLI, Terraform, API and MCP Every tool except the dashboard authenticates with an **API token**, a secret starting with `cldr_`. A token belongs to one account and has one of two access levels: | Access | Can do | | --- | --- | | **Read** | Only reads (`GET` requests). Writes fail with `This API token is read only!` | | **Read & Write** | Everything the member who created it can do, except managing members and tokens and connecting a Git provider, which need the dashboard | `cloider login` creates a token for you. For CI, Terraform and the API, create one in the dashboard under **Access Control**, **API Tokens**. Read [API tokens](/account/api-tokens). ### Next steps - [Install the CLI](/get-started/install-the-cli). - [Deploy your first app](/get-started/deploy-your-first-app). - [Manage Cloider with an AI agent](/guides/manage-cloider-with-an-ai-agent). - [Infrastructure as code with Terraform](/guides/infrastructure-as-code-with-terraform). --- ## Create a Cloider account You create a Cloider account by signing in with your Google or GitHub account on [cloider.com](https://cloider.com). There is no separate Cloider password and no sign up form: the first sign in creates your account and starts a 14-day free trial. ### At a glance | | | | --- | --- | | **Sign in with** | Google or GitHub (OAuth). Your email address from the provider becomes your Cloider identity | | **Trial** | 14 days, with $25 of free credit, as advertised on [cloider.com](https://cloider.com) | | **Trial limits** | One Kubernetes cluster, one database, one bucket and one container registry project at a time | | **After the trial** | The account owner upgrades to the paid plan, or the account is suspended and its resources are deleted | | **Session length** | A dashboard session lasts 24 hours, then you sign in again | ### Before you begin - A **Google** account or a **GitHub** account. Cloider uses the email address of that account. For GitHub, that is your primary email address, and it must be verified on GitHub. - A web browser. :::note[One email address, one Cloider user] Cloider identifies you by your email address, in lowercase. If your Google account and your GitHub account use the same email address, both sign you in to the same Cloider user and the same account. If they use different addresses, you get two separate Cloider users. ::: ### Sign up 1. Go to [cloider.com](https://cloider.com). 2. Click **Login / Register** in the top navigation bar. 3. In the **Login Or Register** dialog, click **Sign in with Google** or **Sign in with Github**. 4. On the Google or GitHub page, choose your account and approve the request. Cloider reads your name, email address and profile picture. 5. You return to cloider.com and land on the dashboard. By signing in you agree to the Terms and Conditions, the Privacy Policy and the Refund Policy linked under the buttons. ### Verify that your account works You are signed in when the dashboard opens and your name and picture appear in the top right corner. A banner at the top of the dashboard says that you are *currently on the limited trial period* and, within a day of signing up, on which date it ends. From the command line you can check the same thing after [installing the CLI](/get-started/install-the-cli): ```bash cloider whoami ``` ```text title="Output" User: jane@example.com Account: jane@example.com Role: Owner Access: Read & Write Token: Cloider CLI on janes-macbook Profile: default API: https://cloider.com/api Token from: /Users/jane/.cloider/token ``` ### Your user and your account Signing up creates two things that share your email address: - **Your user** is you, the person who signs in. - **Your account** is where resources live and where usage is billed. You are its **Owner**. You can later be invited to other people's accounts as a member, and you can invite others into yours. Each resource belongs to exactly one account. Read [Accounts and roles](/concepts/accounts-and-roles) for how users, accounts, members and roles fit together, and [Switch accounts](/account/switch-accounts) for moving between them. ### The free trial A new account is on the **Free** plan, which is the trial. The trial is 14 days long. Its end date appears in the banner within a day of signing up, 14 days from that day. #### What the trial includes During the trial you can use every product, with one limit: you can have **at most one of each** of these at a time. | Resource | Trial limit | Error when you try to create a second one | | --- | --- | --- | | Kubernetes cluster | 1 | `Free plan users can not create more than one cluster. UPGRADE NOW!` | | Database | 1 | `Free plan users can not create more than one database. UPGRADE NOW!` | | Bucket | 1 | `Free plan users can not create more than one bucket. UPGRADE NOW!` | | Container registry project | 1 | `Free plan users can not create more than one project. UPGRADE NOW!` | Deleted resources don't count toward the limit, so you can delete a cluster and create another. The registry project Cloider creates for your apps' images doesn't count either. Apps, domains and DNS records have no trial specific limit. The account wide limits that apply on every plan are listed in [Limits and quotas](/reference/limits-and-quotas). #### Trial reminders Cloider emails the account owner reminders during the trial, on days 4, 7, 10, 12, 13 and 14, with the subject *Action Required: Upgrade to a Paid Plan with Cloider*. On the trial end date you get *ACTION REQUIRED! Your Trial Ends Today*, which warns that the account is suspended 24 hours later. #### What happens when the trial ends If the account is still on the Free plan the day after the trial ends: 1. The account is **suspended**. The banner turns red and says *Your trial has expired and your account is suspended*. 2. The account's Kubernetes clusters are deleted, and its other resources can be permanently deleted as well. 3. The owner receives the email *Your Cloider account has been suspended*. A suspended account can't create clusters, databases, buckets, registry projects, apps, domains, DNS records, targets, access keys or registry tokens. Those requests fail with: ```text Your account is suspended. Upgrade to a paid plan to continue using our services. ``` :::warning[Upgrade before the trial ends to keep your resources] To keep what you built, the account owner clicks **UPGRADE NOW** in the trial banner before the trial ends. Members of the account see a message that the owner needs to upgrade. If your account was suspended by mistake, contact support@cloider.com. ::: ### Upgrade to the paid plan On the paid plan the one-of-each trial limit is gone and you pay monthly for what you use. Only the account owner can upgrade: 1. In the dashboard banner, click **UPGRADE NOW**. 2. Complete the upgrade request that opens in a new tab. How usage is metered and invoiced is explained in [Billing model](/concepts/billing-model). ### Troubleshooting #### I signed in with GitHub and see an empty account Your GitHub account probably uses a different email address than the Google account you used before, so Cloider created a second user. Sign out (account menu, **Logout**) and sign in with the provider you used originally. #### I see "Select an account" instead of the dashboard You are a member of at least one other account, so Cloider asks which account to open. Pick one. See [Switch accounts](/account/switch-accounts). #### "Login session lost!" Your session expired (sessions last 24 hours) or you signed out in another tab. Click **Reload page** and sign in again. See [Troubleshooting](/reference/troubleshooting) for more sign in problems. ### Next steps - [Dashboard tour](/get-started/dashboard-tour): learn your way around. - [Deploy your first app](/get-started/deploy-your-first-app). - [Invite your team](/account/access-control) to your account. --- ## Create your first Kubernetes cluster on Cloider This tutorial creates a Kubernetes cluster on Cloider, connects `kubectl` to it, deploys a small web server and makes it reachable on the internet at a free `cloider.app` address. It takes about fifteen minutes, most of it waiting for the cluster to start. A Cloider **Kubernetes cluster** gives you a standard Kubernetes API, so `kubectl`, Helm, Terraform and any other Kubernetes tool work with it. It is **serverless**: you don't choose machine sizes or manage nodes. You create the cluster, deploy workloads and pay for the CPU and memory they use, plus storage and load balancers. Read the [Kubernetes overview](/kubernetes/overview) for how it works. :::tip[Kubernetes or Apps?] If you just want to run a web app from source code, [Apps](/apps/overview) is simpler: no YAML, no `kubectl`. Choose a cluster when you need Kubernetes itself: several services, Helm charts, custom manifests, or a workload you already run on Kubernetes elsewhere. ::: ### Before you begin - A Cloider account. During the free trial you can have one cluster at a time. See [Create an account](/get-started/create-an-account). - [`kubectl`](https://kubernetes.io/docs/tasks/tools/) installed on your computer. - For the CLI path, the `cloider` CLI logged in with **Read and write** access. See [Install the CLI](/get-started/install-the-cli). ### Step 1: Create the cluster A cluster needs a name and a Kubernetes version. Names use lowercase letters, digits and `-`, start with a letter and are at least 2 characters long. This tutorial uses `staging`. **Dashboard** 1. Open **Kubernetes** in the sidebar and click **Create Cluster**. 2. In **Cluster Name**, type `staging`. 3. In **Cluster Version**, keep the newest version unless you need an older one. 4. Click **Create cluster**. The cluster appears in the list with the status **Creating**, then **Starting**, then **Ready**. The page updates by itself. **CLI** ```bash cloider cluster create staging --wait ``` ```text title="Output" ✓ Creating cluster staging (Kubernetes 1.34) ✓ Cluster staging is ready. Get its kubeconfig with `cloider cluster kubeconfig staging --merge`. ``` Without `--wait`, the command returns at once and `cloider cluster status staging` tells you when it is `Ready`. Pick a version with `--version 1.33`. **Terraform** ```hcl title="main.tf" terraform { required_providers { cloider = { source = "cloider/cloider" } } } provider "cloider" {} resource "cloider_cluster" "staging" { name = "staging" } ``` ```bash terraform init terraform apply ``` `terraform apply` waits until the cluster is `Ready`. See [cloider_cluster](/terraform/resources/cluster). **API** ```bash curl -X POST https://cloider.com/api/clusters \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"cluster_name": "staging", "cluster_version": "1.34"}' ``` ```json title="Response" {"hashed_id": "q7m2x"} ``` Then poll `GET /api/clusters/q7m2x/status` until it answers `Ready`. See [Clusters endpoints](/api/endpoints/clusters). **MCP** ```text title="Prompt" Create a Kubernetes cluster called staging and wait until it's ready. ``` ```json title="Tool call: create_cluster" {"name": "staging", "wait": true} ``` ### Step 2: Connect kubectl `kubectl` finds a cluster through a **kubeconfig** file, which holds the cluster's address and the credentials to reach it. Treat it like a password: anyone with the file has full access to the cluster. **Dashboard** 1. In **Kubernetes**, click the menu button (three dots) at the end of the `staging` row. 2. Click **Kubeconfig**. Your browser downloads a file called `config`. 3. Move it somewhere safe and point `kubectl` at it: ```bash mkdir -p ~/.kube mv ~/Downloads/config ~/.kube/staging.yaml export KUBECONFIG=~/.kube/staging.yaml ``` **CLI** Merge the cluster into your kubeconfig as the context `cloider-staging`, and switch to it: ```bash cloider cluster kubeconfig staging --merge --use ``` ```text title="Output" ✓ Added context cloider-staging to /Users/jane/.kube/config ``` Leave out `--use` to add the context without switching, and change contexts later with `kubectl config use-context cloider-staging`. To write a separate file instead, use `cloider cluster kubeconfig staging -f ~/.kube/staging.yaml`. **Terraform** The cluster's `kube_config` attribute holds the whole file in `raw`, and the parts the `kubernetes` and `helm` providers need in `host`, `cluster_ca_certificate`, `client_certificate` and `client_key`: ```hcl title="kubeconfig.tf" resource "local_sensitive_file" "kubeconfig" { filename = "${path.module}/kubeconfig-staging.yaml" content = cloider_cluster.staging.kube_config.raw file_permission = "0600" } ``` ```bash terraform apply export KUBECONFIG=$PWD/kubeconfig-staging.yaml ``` **API** ```bash curl -H "Authorization: Bearer $CLOIDER_TOKEN" \ https://cloider.com/api/clusters/q7m2x/kubeconfig -o ~/.kube/staging.yaml export KUBECONFIG=~/.kube/staging.yaml ``` **MCP** ```json title="Tool call: get_cluster_kubeconfig" {"cluster": "staging", "merge": true, "use": true} ``` Check the connection: ```bash kubectl get namespaces ``` ```text title="Output" NAME STATUS AGE default Active 4m kube-node-lease Active 4m kube-public Active 4m kube-system Active 4m ``` More options, such as several clusters and contexts, are in [Connect with kubectl](/kubernetes/connect-with-kubectl). ### Step 3: Deploy a workload Create a Deployment running two replicas of the NGINX web server, and a Service in front of it: ```yaml title="hello.yaml" apiVersion: apps/v1 kind: Deployment metadata: name: hello spec: replicas: 2 selector: matchLabels: app: hello template: metadata: labels: app: hello spec: containers: - name: web image: nginx:1.27 ports: - containerPort: 80 resources: requests: cpu: 50m memory: 64Mi --- apiVersion: v1 kind: Service metadata: name: hello spec: selector: app: hello ports: - port: 80 targetPort: 80 ``` ```bash kubectl apply -f hello.yaml kubectl get pods ``` ```text title="Output" NAME READY STATUS RESTARTS AGE hello-6d4b9c7f8d-2xk9p 1/1 Running 0 20s hello-6d4b9c7f8d-q8m4t 1/1 Running 0 20s ``` You pay for the CPU and memory the pods use, so small requests keep a test workload cheap. See [Deploy a workload](/kubernetes/deploy-a-workload). ### Step 4: Put it on the internet A Service inside the cluster isn't reachable from the internet yet. On Cloider you publish it by routing a **domain** to it with a **target**: a free `cloider.app` subdomain works and comes with HTTPS. Kubernetes Ingress objects in the cluster are not served, so use a target instead. **Dashboard** 1. Open **Domains** in the sidebar and claim a free subdomain, for example `hello-staging.cloider.app`. 2. On the domain's page, add a target that routes the path `/` to the cluster `staging`, the service `default/hello` and port `80`. The full walkthrough with screenshots is in [Expose services](/kubernetes/expose-services). **CLI** ```bash cloider domain create hello-staging.cloider.app cloider target create hello-staging.cloider.app --cluster staging --service default/hello --port 80 ``` ```text title="Output" ✓ Claimed hello-staging.cloider.app Route it with `cloider target create hello-staging.cloider.app --app `. ✓ Routed hello-staging.cloider.app/ to staging/default/hello:80 ``` `cloider cluster services staging` lists the services a target can route to. **Terraform** Use the [`cloider_domain`](/terraform/resources/domain) and [`cloider_target`](/terraform/resources/target) resources. Their attributes and a complete example are on those pages. **API** Claim the subdomain with `POST /api/domains` and create the target with `POST /api/targets`. See [Domains endpoints](/api/endpoints/domains) and [Targets endpoints](/api/endpoints/targets). **MCP** ```text title="Prompt" Claim hello-staging.cloider.app and route it to the hello service in the default namespace of my staging cluster, port 80. ``` The agent calls `create_domain`, then `create_domain_target`: ```json title="Tool call: create_domain_target" {"domain": "hello-staging.cloider.app", "cluster": "staging", "service": "default/hello", "port": 80} ``` Open it: ```bash curl -I https://hello-staging.cloider.app ``` ```text title="Output" HTTP/2 200 server: nginx/1.27.5 content-type: text/html ``` ### Step 5: Use the browser terminal (optional) Each cluster has a terminal in the dashboard with `kubectl` and `helm` already pointed at it, which is handy on a computer without `kubectl`. In **Kubernetes**, open the row menu of `staging` and click **Terminal**. From a terminal on your own computer, `cloider cluster shell staging` opens the same shell, and `cloider cluster exec staging kubectl get pods -A` runs one command. See [Web terminal and exec](/kubernetes/web-terminal-and-exec). ### Verify - The cluster's status is **Ready**. - `kubectl get pods` shows the `hello` pods as `Running`. - `https://hello-staging.cloider.app` answers with the NGINX welcome page. ### Stop paying for the cluster when you're done - **Pause** it to stop paying for compute while keeping everything in it: row menu, **Pause**, or `cloider cluster pause staging`. Resume it the same way. See [Pause and resume](/kubernetes/pause-and-resume). - **Delete** it to remove it and everything in it for good: row menu, **Delete**, or `cloider cluster delete staging`. See [Delete a cluster](/kubernetes/delete-a-cluster). How a paused cluster is billed is explained in [Billing model](/concepts/billing-model). ### Troubleshooting | Symptom | Likely cause and fix | | --- | --- | | `Free plan users can not create more than one cluster. UPGRADE NOW!` | You are on the trial and already have a cluster. Delete it or upgrade. | | **Kubeconfig** is greyed out in the row menu | The cluster isn't **Ready** yet. Wait for it, or check `cloider cluster status staging`. | | `kubectl` answers `Unable to connect to the server` | `kubectl` uses another kubeconfig or context. Check `echo $KUBECONFIG` and `kubectl config current-context`. A paused cluster doesn't answer either. | | `cluster staging has no services to route to yet` | The Service wasn't created. Run `kubectl get svc` and apply `hello.yaml` again. | | Pods stay `Pending` | The pods can't be scheduled. Run `kubectl describe pod ` and read the events. | More in [Kubernetes troubleshooting](/kubernetes/troubleshooting). ### Next steps - [Helm](/kubernetes/helm): install charts. - [Persistent volumes](/kubernetes/persistent-volumes) and [Load balancers](/kubernetes/load-balancers). - [Pull private images](/kubernetes/pull-private-images) from your Cloider registry project. - [WordPress on Kubernetes](/guides/wordpress-on-kubernetes): a complete example with a database and a volume. --- ## Cloider dashboard tour, sidebar, search and menus The Cloider dashboard at [cloider.com/dashboard](https://cloider.com/dashboard) is the web interface for everything in your account: apps, Kubernetes clusters, databases, buckets, registry projects, domains, usage, billing, support and access control. This page walks through its layout so you know where everything is. ### At a glance | Part | Where | What it does | | --- | --- | --- | | Sidebar | Left edge | One entry per product and account page | | Search | Top bar, center | Finds pages and resources by name. Shortcut: `⌘ K` on macOS, `Ctrl K` elsewhere | | Notifications | Bell icon, top right | Opens the Notifications page | | Help | Question mark icon, top right | Opens Support | | Account menu | Your account's picture and name, top right | **Switch account** and **Logout** | | Trial banner | Top of the page | Shows the trial end date while the account is on the trial | ### The landing page When you open the dashboard, it shows the **Apps** page, the first entry in the sidebar. The Cloider logo in the top left corner always brings you back there. ### The sidebar The sidebar lists every part of the dashboard, in this order: | Entry | Page | What you manage there | Docs | | --- | --- | --- | --- | | **Apps** | `/dashboard/apps` | Deployed applications, their builds, logs and environment | [Apps](/apps/overview) | | **Kubernetes** | `/dashboard/clusters` | Serverless Kubernetes clusters | [Kubernetes](/kubernetes/overview) | | **Registry** | `/dashboard/registry` | Container registry projects and images | [Container Registry](/container-registry/overview) | | **Databases** | `/dashboard/databases` | PostgreSQL, MySQL, MongoDB and Redis | [Databases](/databases/overview) | | **Buckets** | `/dashboard/buckets` | S3 compatible object storage | [Object Storage](/object-storage/overview) | | **Domains** | `/dashboard/domains` | Free subdomains, custom domains, DNS records and targets | [Domains and DNS](/domains/overview) | | **Usage** | `/dashboard/usage` | This month's usage and cost per resource | [Usage](/account/usage) | | **Billing** | `/dashboard/billing` | Monthly invoices. Shown to the account **Owner** only | [Billing](/account/billing) | | **Support** | `/dashboard/support` | Support requests to the Cloider team | [Support](/account/support) | | **Access Control** | `/dashboard/access-control` | Members, API tokens, S3 credentials and registry tokens | [Access control](/account/access-control) | Some entries carry a small badge such as **New** or **Beta**, which marks a recently added product. On a narrow screen, the hamburger button at the left end of the top bar (**Toggle menu**) collapses and expands the sidebar. ### Search The search box in the top bar finds dashboard pages and your resources by name. Click it, or press `⌘ K` (macOS) or `Ctrl K` (Windows and Linux), and start typing. Results are grouped in this order: **Pages**, **Apps**, **Clusters**, **Registry Projects**, **Databases**, **Buckets** and **Domains**. Use the arrow keys to move between results, `Enter` to open one and `Escape` to close the results. Search understands common words for each page, not only its title. For example: | You type | You find | | --- | --- | | `k8s`, `kubectl` or `kubeconfig` | **Kubernetes** | | `postgres`, `mysql`, `redis` or `mongo` | **Databases** | | `s3`, `files` or `access keys` | **Buckets** and **S3 credentials** | | `invoices` or `credit card` | **Billing** (Owner only) | | `api keys` or `bearer` | **API tokens** | | `docker login` or `pull secret` | **Registry tokens** | | `new cluster`, `create database`, `new bucket` | The matching create form | | `ticket` or `help` | **Support** and **New support request** | Resource results match the resource's name, so typing `orders` finds a database called `orders-db`. Results appear as you type, and a resource you created in the last half minute may not be listed yet. ### The account menu The button in the top right corner shows the picture and name of the **account you are working in**, which is not always your own. Click it to open the account menu: - **Switch account** appears when you are a member of at least one other account. It takes you to the **Select an account** screen. See [Switch accounts](/account/switch-accounts). - **Logout** signs you out of the dashboard. ### Notifications The bell icon opens the **Notifications** page. Notifications are announcements from Cloider: new features, offers, and warnings such as a budget that is about to be exceeded. When there is nothing new, the page says *You don't have any new notifications!*. Read [Notifications](/account/notifications) for the emails Cloider sends. ### The trial banner While the account is on the free trial, a banner across the top of every page says that you are on the limited trial period and when it ends. For the account owner it includes an **UPGRADE NOW!** link. If the trial has ended without an upgrade, the banner turns red and says the account is suspended. See [Create an account](/get-started/create-an-account#the-free-trial). ### How resource pages work Every product page follows the same pattern, so once you know one you know them all. 1. **The list page** (for example **Kubernetes**) shows your resources in a table with their status. A create button sits at the top, for example **Create Cluster** or **Create App**. 2. **The row menu**: the button with three dots at the end of each row opens actions for that resource, such as **Kubeconfig**, **Terminal**, **Pause**, **Resume** and **Delete** for a cluster. Actions that don't apply to the current status are greyed out. 3. **The detail page**: click a resource's name to open it. Detail pages have tabs, such as **Overview**, **Insights**, **Builds**, **Environment**, **Settings**, **Logs** and **Terminal** for an app. 4. **Statuses update by themselves**: while a resource is changing (for example **Creating** or **Pausing**), the page polls until it settles. You don't need to reload. What each status means is listed in [Status values](/reference/status-values). :::note[Members with the Restricted role] If your role in the account is **Restricted**, the dashboard hides or disables the buttons that change things, such as **Create Cluster** and **Delete**. You can still look at everything. See [Accounts and roles](/concepts/accounts-and-roles). ::: ### Sessions and signing out A dashboard session lasts 24 hours. When it expires, the dashboard shows **Login session lost!** with a **Reload page** button: reload and sign in again with Google or GitHub. **Logout** in the account menu ends the session straight away. Signing out of the dashboard doesn't affect API tokens or the CLI. Tokens keep working until they are deleted. See [API tokens](/account/api-tokens). ### Next steps - [Choose a tool](/get-started/choose-a-tool): when the CLI, Terraform, the API or an AI agent fits better than the dashboard. - [Deploy your first app](/get-started/deploy-your-first-app). - [Access control](/account/access-control): invite your team. --- ## Deploy your first app on Cloider This tutorial deploys a small web app to Cloider and then walks through the everyday tasks around it: watching the build, opening the app on its free HTTPS address, setting an environment variable, reading the logs and deploying a new version. It takes about ten minutes. An **app** on Cloider is your source code, built into a container image and run for you. You bring the code; Cloider detects the language, builds it (with your Dockerfile if the project has one, or with buildpacks if it doesn't), runs it and gives it a URL. You don't manage servers, clusters or certificates. Read the [Apps overview](/apps/overview) for the full picture. ### Before you begin - A Cloider account. See [Create an account](/get-started/create-an-account). - For the CLI path, the `cloider` CLI installed and logged in with **Read and write** access. See [Install the CLI](/get-started/install-the-cli). - Some code to deploy. This tutorial uses a minimal Node.js server, but any language Cloider supports works (see [Supported languages](/apps/supported-languages)), and so does a folder of static HTML. ### What happens when you deploy Every deploy creates a numbered **build** (version `1`, `2`, and so on). The app's status follows the build through these steps: ```mermaid flowchart LR A[Pending
source uploading] --> B[Queued] B --> C[Building
image is built] C --> D[Deploying
image is applied] D --> E[Starting
containers come up] E --> F[Ready
serving traffic] C -. build error .-> G[Failed] ``` | Status | What is happening | | --- | --- | | **Pending** | The source archive is still being uploaded | | **Queued** | The build is waiting for a builder | | **Building** | Cloider is building a container image from your code | | **Deploying** | The new image is being applied | | **Starting** | The new containers are pulling the image and starting up | | **Ready** | The new version is running and serving requests | | **Failed** | The build or start failed. The build log says why, and you can retry it | If an app already has a working version, a failed build doesn't take it down: the previous version keeps serving. ### Step 1: Prepare the code Create a folder called `hello` with two files: ```json title="package.json" { "name": "hello", "version": "1.0.0", "scripts": { "start": "node server.js" } } ``` ```js title="server.js" const http = require('http'); const port = process.env.PORT || 8080; const greeting = process.env.GREETING || 'Hello'; http .createServer((req, res) => { console.log(`${req.method} ${req.url}`); res.writeHead(200, {'Content-Type': 'text/plain'}); res.end(`${greeting} from Cloider!\n`); }) .listen(port, () => console.log(`Listening on ${port}`)); ``` Two things matter for any app you deploy: - **Listen on the port Cloider gives you.** An app built from source receives the port in the `PORT` environment variable. Use it, or make sure the port you set when you create the app is the one your code listens on. - **Start with one command.** Cloider uses the project's own start script (here `npm start`) or Procfile. You can also set a **Start Command** yourself. :::tip[Keep secrets and build output out of the upload] The CLI leaves out everything `.gitignore` lists, plus `.git` and `.cloider`. To leave out more without changing `.gitignore`, list it in a `.cloiderignore` file with the same syntax. An upload can hold up to 2000 files and 100 MB. ::: ### Step 2: Deploy **Dashboard** 1. Open **Apps** in the sidebar and click **Create App**. 2. Under **Application source**, drag the `hello` folder onto the drop zone, or click **choose a folder**. A zip file or a single HTML file works too. To deploy from a repository instead, click **GitHub**, **GitLab** or **Bitbucket** under **Import a Git repository** (see [Deploy from Git](/apps/deploy-from-git)), and to run an image you already built, click **Docker image** (see [Deploy a Docker image](/apps/deploy-an-image)). 3. Cloider uploads the code and looks at it. The **Configure your app** step opens with what it detected under *We had a look at your code*. 4. Fill in the form: - **App Name**: `hello`. Lowercase letters, digits and dashes, 2 to 64 characters, starting with a letter. - **Port**: `8080`. - **Start Command**: leave empty, so `npm start` runs. - **Give this app a free subdomain**: leave on, and type `hello-acme` in **Subdomain**. Wait for *hello-acme.cloider.app is available*. - **Environment variables**, **Build variables** and **Build arguments**: leave empty for now. 5. Click **Create App**. **CLI** From inside the `hello` folder: ```bash cloider deploy --name hello --subdomain hello-acme ``` ```text title="Output" Packaged 2 files (0.6 KB) Detected JavaScript ✓ Created app hello on port 8080 Linked . to hello in .cloider/app.json, later deploys from here update it. ... ✓ Deployed version 1 of hello https://hello-acme.cloider.app ``` The CLI streams the build output while it waits. Pass `--detach` (`-d`) to return as soon as the build starts, then follow it with `cloider app build logs hello`. **Terraform** The Terraform provider doesn't manage apps. Use the **Dashboard** or **CLI** tab to deploy, and Terraform for the [resources it supports](/terraform/overview), such as a database the app connects to. **API** An API deploy is four calls: `POST /api/apps/source` (answers with a `token` and a `presigned_url`), a `PUT` of your zip to the `presigned_url`, `POST /api/apps/source//detect`, and `POST /api/apps` with `app_name`, `port`, `subdomain` and `source_token`. The request and response shapes are in [Apps endpoints](/api/endpoints/apps). The CLI's `cloider deploy` does exactly this for you. **MCP** ```text title="Prompt" Deploy /Users/jane/hello as an app called hello on the subdomain hello-acme. ``` ```json title="Tool call: deploy_app" {"dir": "/Users/jane/hello", "name": "hello", "subdomain": "hello-acme"} ``` The tool follows the build and answers with the app's URL. ### Step 3: Watch the build The first build turns your code into a container image, so it takes a little while. You can follow it live. **Dashboard** The app page opens after **Create App**. Its status moves through **Queued**, **Building**, **Deploying** and **Starting** to **Ready**. Open the **Builds** tab and click **Logs** on the build to see the build output as it happens. **CLI** ```bash cloider app builds hello cloider app build logs hello cloider app status hello ``` `cloider app build logs` follows a running build until it ends, and prints the whole log of a finished one. **Terraform** Not available in Terraform. Use the Dashboard or CLI tab. **API** ```bash curl -H "Authorization: Bearer $CLOIDER_TOKEN" https://cloider.com/api/apps//status curl -H "Authorization: Bearer $CLOIDER_TOKEN" https://cloider.com/api/apps//builds ``` See [App builds endpoints](/api/endpoints/app-builds) for build logs. **MCP** ```text title="Prompt" Show me the build log of the latest build of hello. ``` ```json title="Tool call: get_app_build_logs" {"app": "hello"} ``` ### Step 4: Open the app When the status is **Ready**, the app answers on its URL. The URL is on the app's **Overview** tab, printed by `cloider deploy`, and opened in your browser by `cloider app open hello`. ```bash curl https://hello-acme.cloider.app ``` ```text title="Output" Hello from Cloider! ``` HTTPS is set up for you. You can add your own domain later, see [App domains](/apps/domains). ### Step 5: Set an environment variable Environment variables configure the running app without changing the code. Changing them restarts the app with the new values; no new build is needed. Mark a value as **secret** to keep it out of every listing. **Dashboard** 1. Open the app and go to the **Environment** tab. 2. Under **Environment variables**, click **Add variable**. 3. Type `GREETING` as the name and `Hi` as the value. To store a value as a secret, click the lock icon next to it. 4. Click **Save changes**. **CLI** ```bash cloider app env set hello GREETING=Hi ``` ```text title="Output" ✓ Saved, hello restarts with 1 variable(s) ``` Add `--secret` to store values as secrets: `cloider app env set hello --secret API_KEY=...`. **Terraform** Not available in Terraform. Use the Dashboard or CLI tab. **API** `PUT /api/apps//env` replaces the whole environment with the list you send, so include every variable you want to keep: ```bash curl -X PUT https://cloider.com/api/apps//env \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"env_vars": [{"key": "GREETING", "value": "Hi", "secret": false}]}' ``` ```json title="Response" {"success": true} ``` **MCP** ```text title="Prompt" Set GREETING to Hi on the app hello. ``` ```json title="Tool call: set_app_variables" {"app": "hello", "variables": {"GREETING": "Hi"}} ``` After the restart, `curl https://hello-acme.cloider.app` answers `Hi from Cloider!`. Read [Environment variables](/apps/environment-variables) for build time variables and secrets. ### Step 6: Read the logs Everything your app prints to standard output and standard error is in its logs. The example server prints one line per request. **Dashboard** Open the app's **Logs** tab. New lines appear as they are printed. **CLI** ```bash cloider app logs hello -f ``` ```text title="Output" Listening on 8080 GET / GET /favicon.ico ``` `-n` sets how much history to print first: 100, 200 (the default), 500 or 1000 lines. **Terraform** Not available in Terraform. Use the Dashboard or CLI tab. **API** Logs are streamed from `GET /api/apps//logs/stream`. See [Apps endpoints](/api/endpoints/apps). **MCP** ```json title="Tool call: get_app_logs" {"app": "hello", "lines": 200} ``` More in [Logs](/apps/logs). ### Step 7: Deploy a new version Change the code, for example the text in `server.js`, and deploy again. **Dashboard** Open the app's **Builds** tab, click **Deploy new version** and upload the folder again. An app imported from Git shows **Deploy latest commit** instead, and also deploys on every push to its branch unless you turn that off. **CLI** Run the same command in the same folder. The folder is linked to the app in `.cloider/app.json`, so it builds version 2 of `hello`: ```bash cloider deploy ``` ```text title="Output" Packaged 2 files (0.6 KB) ✓ Uploaded version 2 of hello ... ✓ Deployed version 2 of hello https://hello-acme.cloider.app ``` **Terraform** Not available in Terraform. Use the Dashboard or CLI tab. **API** A new version is a `POST /api/apps//builds`, an upload to the presigned URL it returns, then `POST /api/apps//builds//uploaded`. See [App builds endpoints](/api/endpoints/app-builds). **MCP** ```text title="Prompt" Deploy my changes in /Users/jane/hello. ``` ```json title="Tool call: deploy_app" {"dir": "/Users/jane/hello"} ``` The current version keeps serving while the new one builds, and if the new build fails, the current version stays up. When a new version reaches **Ready**, the one before it is marked **Superseded** in the build history. Read [Builds and deployments](/apps/builds-and-deployments). ### Verify - The app's status is **Ready** in the dashboard, or `cloider app status hello` prints `Ready`. - `curl https://hello-acme.cloider.app` answers with your text. - The **Builds** tab, or `cloider app builds hello`, lists versions 1 and 2, with version 2 live. ### Troubleshooting | Symptom | Likely cause and fix | | --- | --- | | The build is **Failed** | Read the build log. Common causes are a missing start script, a dependency that fails to install, or a Dockerfile error. Fix it and deploy again, or retry the build. | | The app never gets past **Starting** | The app isn't listening on its port, or it crashes on start. Check the **Logs** tab and that the code listens on `PORT`. | | *Only up to 2000 files can be uploaded, zip your project first.* | The folder has too many files, usually `node_modules`. Leave it out (the CLI follows `.gitignore`) or upload a zip. | | `You have read-only access to this account, you can not create apps!` | Your role is Restricted, or you logged the CLI in with a read only token. Ask an owner or admin, or run `cloider login` again with **Read and write**. | | The subdomain is taken | Pick another name. Free subdomains are unique across all of Cloider. | More in [Apps troubleshooting](/apps/troubleshooting). ### Next steps - [Build configuration](/apps/build-configuration): Dockerfile or buildpacks, ports, build arguments. - [Deploy from Git](/apps/deploy-from-git): deploy on every push to GitHub, GitLab or Bitbucket. - [Full stack app with PostgreSQL](/guides/full-stack-app-with-postgresql): connect the app to a database. - [Create your first cluster](/get-started/create-your-first-cluster): when you need full Kubernetes. --- ## Get started with Cloider These pages take you from a new Cloider account to your first running app and Kubernetes cluster, one step at a time. Read them in order if you are new to Cloider, or jump to the page you need. If you only have five minutes, the [Quick start](/quick-start) does the whole path on one page. ### At a glance | Step | Page | What you get | | --- | --- | --- | | 1 | [Create an account](/get-started/create-an-account) | A Cloider account on the 14-day free trial, signed in with Google or GitHub | | 2 | [Dashboard tour](/get-started/dashboard-tour) | The sidebar, search, account menu and notifications explained | | 3 | [Choose a tool](/get-started/choose-a-tool) | When to use the dashboard, the CLI, Terraform, the REST API or an AI agent | | 4 | [Deploy your first app](/get-started/deploy-your-first-app) | A web app running on a free `cloider.app` subdomain with HTTPS | | 5 | [Create your first cluster](/get-started/create-your-first-cluster) | A serverless Kubernetes cluster, `kubectl` connected, a workload on the internet | | 6 | [Install the CLI](/get-started/install-the-cli) | The `cloider` command on your computer, logged in to your account | ### What you can build on Cloider Cloider is a pay-as-you-go cloud platform. Everything is managed from the same account: Deploy code from Git or your computer. No Dockerfile or servers to manage. Serverless clusters with a standard Kubernetes API, billed by usage. Managed PostgreSQL, MySQL, MongoDB and Redis with automatic backups. S3 compatible buckets for files, assets and backups. Private projects for your container images. Free subdomains, your own domains, DNS records and routing. ### Tools you can use Every product can be managed from more than one tool. Most people start with the dashboard and add the CLI later. The web interface at cloider.com/dashboard. The cloider command for terminals, scripts and CI. Infrastructure as code with the cloider/cloider provider. HTTP endpoints, authenticated with an API token. Let an AI agent such as Claude Code or Cursor manage Cloider. ### Where to go after getting started - [Concepts](/concepts) explains how Cloider works: accounts and roles, the billing model, resource lifecycle and security. - [Account and billing](/account) covers members, API tokens, usage, invoices and support. - [Guides](/guides) are end to end tutorials, such as a [full stack app with PostgreSQL](/guides/full-stack-app-with-postgresql). --- ## Install the Cloider CLI and log in The `cloider` CLI is a single binary that manages Cloider from a terminal: apps, clusters, databases, buckets, registry projects, domains, DNS, support, usage and billing. This page installs it and logs it in. The full reference is in the [CLI section](/cli/overview). ### At a glance | | | | --- | --- | | **Platforms** | macOS, Linux and Windows, each on amd64 and arm64 | | **Install** | `curl -fsSL https://cloider.com/install.sh \| sh`, on Windows `irm https://cloider.com/install.ps1 \| iex` in PowerShell | | **Log in** | `cloider login`, which creates an API token through the browser | | **Check** | `cloider whoami` | | **Updates** | Automatic, checked once a day. See [Updating](/cli/updating) | ### Install on macOS or Linux Run the install script: ```bash curl -fsSL https://cloider.com/install.sh | sh ``` ```text title="Output" Downloading cloider 1.4.0 for linux/amd64 Installed cloider 1.4.0 to /usr/local/bin/cloider Get started with: cloider login ``` The script downloads the latest release for your system, checks it against the release's SHA-256 checksums, and installs the `cloider` binary: - into `/usr/local/bin` if you can write there, - otherwise into `~/.local/bin`. If that directory isn't on your `PATH`, the script prints the `export PATH=...` line to add to your shell profile. You can change what it does with environment variables: | Variable | Effect | Example | | --- | --- | --- | | `CLOIDER_INSTALL_DIR` | Install into this directory | `curl -fsSL https://cloider.com/install.sh \| CLOIDER_INSTALL_DIR=$HOME/bin sh` | | `CLOIDER_VERSION` | Install this version instead of the latest | `curl -fsSL https://cloider.com/install.sh \| CLOIDER_VERSION=1.3.0 sh` | The script needs `curl` or `wget`, and `sha256sum` or `shasum`. ### Install on Windows Run the install script in PowerShell: ```powershell irm https://cloider.com/install.ps1 | iex ``` ```text title="Output" Downloading cloider 1.4.0 for windows/amd64 Installed cloider 1.4.0 to C:\Users\jane\AppData\Local\cloider\cloider.exe Added C:\Users\jane\AppData\Local\cloider to your Path Get started with: cloider login ``` The script downloads the latest release for your architecture, checks it against the release's SHA-256 checksums, installs `cloider.exe` into `%LOCALAPPDATA%\cloider` and adds that folder to your user `Path`. It takes the same `CLOIDER_INSTALL_DIR` and `CLOIDER_VERSION` variables, set with `$env:CLOIDER_VERSION = "1.3.0"` before you run it. More detail, including a manual install, is in [CLI install](/cli/install#install-on-windows). ### Check the installation ```bash cloider version ``` ```text title="Output" cloider 1.4.0 ``` ### Log in The CLI calls the Cloider API with an **API token**. `cloider login` creates one for you and saves it, so you don't have to copy anything by hand. ```bash cloider login ``` 1. The CLI opens your browser on the **Authorize Cloider CLI** page. If you aren't signed in to Cloider, sign in with Google or GitHub first. 2. Under **Account**, pick the account the CLI should work in. If you belong to several accounts, each shows your role in it. 3. Under **Access permissions**, choose **Read and write** to manage resources, or **Read only** to only look. **Read and write** is unavailable for an account where your role is Restricted. 4. Keep or change the **Description**, which is how the token is listed in **Access Control**. It defaults to `Cloider CLI on `. 5. Click **Authorize CLI**. The page says *You're all set*, and the terminal confirms: ```text title="Output" ✓ Logged in as jane@example.com in account jane@example.com (Read & Write) ``` The token is saved in plain text in `~/.cloider/token` (other profiles: `~/.cloider/tokens/`). Keep it private. #### Log in on a machine without a browser On a server or over SSH, use `--no-browser`. The CLI prints a link; open it on any computer, authorize, and paste the token the page shows (*Copy your token*) back into the terminal: ```bash cloider login --no-browser ``` #### Log in with an existing API token If you already have a token from **Access Control**, **API Tokens** (for example in CI), pass it on stdin: ```bash cloider login --with-token < token.txt ``` In CI you don't need to log in at all: set `CLOIDER_TOKEN` (or `CLOIDER_TOKEN_FILE`) and every command uses it. ```bash export CLOIDER_TOKEN=cldr_xxxxxxxxxxxxxxxxxxxxxxxx cloider cluster list ``` The CLI takes the token from the first of these that is set: the `--token-file` flag, `CLOIDER_TOKEN`, `CLOIDER_TOKEN_FILE`, then the saved login. See [CLI authentication](/cli/authentication). ### Check who you are logged in as ```bash cloider whoami ``` ```text title="Output" User: jane@example.com Account: jane@example.com Role: Owner Access: Read & Write Token: Cloider CLI on janes-macbook Profile: default API: https://cloider.com/api Token from: /Users/jane/.cloider/token ``` **Account** is the account the token works in, and **Access** is `Read` for a read only token or a Restricted member. ### Work with several accounts A token belongs to one account. To use the CLI with several accounts, log in once per account under a **profile** name and switch between them: ```bash cloider login --profile acme cloider profile use acme cloider --profile personal cluster list ``` See [Auth and profiles commands](/cli/commands/auth-and-profiles). ### Log out ```bash cloider logout ``` `cloider logout` revokes the token on Cloider and forgets it locally. Tokens can also be deleted in the dashboard under **Access Control**, **API Tokens**. ### Troubleshooting | Message | Fix | | --- | --- | | `cloider: command not found` | The install directory isn't on your `PATH`. Add the line the install script printed, for example `export PATH="$HOME/.local/bin:$PATH"`, to your shell profile. | | `Invalid or revoked API token!` (exit code `3`) | The token was deleted or its member was removed. Run `cloider login` again. | | `This API token is read only!` (exit code `5`) | You logged in with **Read only**. Run `cloider login` again and choose **Read and write**. | | `CLOIDER_TOKEN or CLOIDER_TOKEN_FILE is set, and takes precedence over this login.` | Unset the variable, or use it on purpose. | | *This link is incomplete* in the browser | Run `cloider login` again; the link must come from the CLI. | ### Next steps - [CLI overview](/cli/overview) and the [command reference](/cli/commands). - [Shell completion](/cli/shell-completion) for bash, zsh, fish and PowerShell. - [Deploy your first app](/get-started/deploy-your-first-app) with `cloider deploy`. - [Set up the MCP server](/mcp/setup) so an AI agent can use the CLI. --- ## Cloider accounts, members and roles explained On Cloider, **resources belong to accounts** and **people reach accounts through roles**. Every user has their own account, where they are the Owner, and can be invited into other accounts as an Admin or a Restricted member. API tokens act inside one account with the role of the member who created them, or less. ### At a glance | Term | What it is | | --- | --- | | **User** | A person, identified by the email address of their Google or GitHub sign in | | **Account** | The container for resources, usage and invoices. Every user has one, named after them | | **Owner** | The user whose account it is. Full access, including billing. Every account has exactly one | | **Member** | Another user invited into the account by email, with the role **Admin** or **Restricted** | | **Admin** | Full access to every resource and to members, but not to billing | | **Restricted** | Read-only access to resources, with a few exceptions listed below | | **API token** | A secret that acts in one account as the member who created it, either **Read** or **Read & Write** | ### Users and accounts When you sign in for the first time, Cloider creates your **user** and your **account** together. They share your email address and your name, which is why the account menu shows your own name while you work in your own account. - Your account is where your resources live and where their usage is billed. - You are its **Owner**. Ownership can't be given away or shared: an account has exactly one Owner. - You can be a **member** of any number of other accounts. In each one you have the role its Owner or an Admin gave you. The account you are working in is shown in the top right corner of the dashboard. Moving between accounts is covered in [Switch accounts](/account/switch-accounts). ```mermaid flowchart LR jane[Jane, user] -- Owner --> ja[Jane's account] jane -- Admin --> acme[Acme Inc account] sam[Sam, user] -- Owner --> sa[Sam's account] sam -- Restricted --> acme boss[Acme Inc owner, user] -- Owner --> acme ``` :::tip[Use a shared account for a team] For a company, sign up with the address that should own the infrastructure and receive the invoices (for example `ops@acme.com`), then invite each person as a member. Everyone keeps their own personal account next to it. ::: ### Members A **member** is a user invited into an account by email address. Owners and Admins invite members from **Access Control** in the dashboard (see [Access control](/account/access-control)). A membership has one of three statuses: | Status | Meaning | | --- | --- | | **Pending** | Invited, but the person hasn't opened the account yet. They receive an email titled *Invitation to Access a Cloider Account* | | **Active** | The person has signed in with the invited email address and opened the account at least once | | **Deactivated** | Access is switched off but the membership is kept, so it can be turned back on later | The invitation is tied to the **email address**, not to a provider. The invited person signs in with Google or GitHub using that address, and the account appears on their **Select an account** screen. Opening it the first time makes the membership **Active**. ### Roles #### Owner The Owner has full access to the account. They are the only one who can see **Billing** and its invoices, and the only one who can upgrade the account from the trial to the paid plan. The Owner is billed for every resource in the account, whoever created it. #### Admin Admins can do everything the Owner can do with resources and members: create, change and delete every kind of resource, invite and remove members, change roles, and manage every API token in the account. They can't see **Billing** or invoices. #### Restricted Restricted members have read-only access. They can see every resource, its status, settings, usage and logs, but they can't create, change or delete resources. The dashboard hides or disables the buttons that would. Restricted members can still: - open the browser **terminal** of clusters, apps and databases, and download a cluster's **kubeconfig**, - read connection details, such as a database's password and CA certificate, and the values of an app's secret variables, - open and reply to **support requests**, - generate **Read** API tokens for themselves, - leave the account. :::warning[Restricted is not "no secrets"] A Restricted member can read credentials: kubeconfigs, database passwords and secret variables. A kubeconfig is a cluster admin credential, and a terminal is a shell. Give the Restricted role only to people you trust with that data, and remove members who no longer need access. ::: When a request is refused because of the role, the error says so, for example: ```text You have read-only access to this account, you can not create clusters! ``` ### API tokens and roles An **API token** belongs to one account and acts as the member who created it. Cloider looks up that member's current role on every request, so a token never does more than its creator can do right now: - A **Read & Write** token has the creator's role. - A **Read** token is held to read requests (`GET`), whatever the creator's role. Any other request is refused with `This API token is read only!`. - Restricted members can only create **Read** tokens. If a member is changed to Restricted, their existing tokens are switched to **Read**. - If the creator is **removed** or **deactivated**, their tokens stop working at once. Removing a member also deletes their tokens. Some actions can't be done with any token, only from a signed in dashboard session: inviting and managing members, changing roles, generating, editing and deleting API tokens, connecting a Git provider, switching accounts and leaving an account. Those requests fail with `This endpoint can't be used with an API token!`. The one token action allowed is a token revoking itself (`cloider logout` does this). Read [API tokens](/account/api-tokens) for how to create and manage them. ### Permission matrix The matrix below shows who can do what. **Read token** and **Read & Write token** are API tokens; a Read & Write token has its creator's role, so the column assumes an Owner or Admin creator. | Action | Owner | Admin | Restricted | Read token | Read & Write token | | --- | --- | --- | --- | --- | --- | | See every resource, its status and settings | Yes | Yes | Yes | Yes | Yes | | See the **Usage** page and per resource usage | Yes | Yes | Yes | Yes | Yes | | See **Billing** and invoices | Yes | No | No | No | Owner's token only | | Upgrade from the trial to the paid plan | Yes | No | No | No | No | | Create, pause, resume and delete clusters | Yes | Yes | No | No | Yes | | Download a cluster's kubeconfig | Yes | Yes | Yes | Yes | Yes | | Create, resize, rename, pause, resume, restore and delete databases | Yes | Yes | No | No | Yes | | Read a database's password and CA certificate | Yes | Yes | Yes | Yes | Yes | | Create, deploy, configure, pause, resume and delete apps | Yes | Yes | No | No | Yes | | Change an app's variables, build arguments and build variables | Yes | Yes | No | No | Yes | | Read an app's secret variable values and logs | Yes | Yes | Yes | Yes | Yes | | Create and delete buckets, change visibility, versioning, policy and lifecycle | Yes | Yes | No | No | Yes | | Upload, delete and restore files in buckets | Yes | Yes | No | No | Yes | | List and download files in buckets | Yes | Yes | Yes | Yes | Yes | | Create, update and delete S3 access keys | Yes | Yes | No | No | Yes | | Create and delete registry projects, change settings and policies | Yes | Yes | No | No | Yes | | Delete images, tags and repositories, run retention | Yes | Yes | No | No | Yes | | Create, update and delete registry tokens | Yes | Yes | No | No | Yes | | Add and delete domains, DNS records and targets | Yes | Yes | No | No | Yes | | Open a browser terminal (cluster, app, database) | Yes | Yes | Yes | No | Yes | | Run a command with `exec` | Yes | Yes | No | No | Yes | | Connect or disconnect a Git provider | Yes (dashboard) | Yes (dashboard) | No | No | No | | Open and reply to support requests | Yes | Yes | Yes | No | Yes | | List members | Yes | Yes | Yes | Yes | Yes | | Invite members, change roles, activate, deactivate and remove members | Yes (dashboard) | Yes (dashboard) | No | No | No | | Leave the account | Not applicable | Yes (dashboard) | Yes (dashboard) | No | No | | Generate API tokens | Yes (dashboard) | Yes (dashboard) | Read only (dashboard) | No | No | | See API tokens | All in the account | All in the account | Their own | Per creator | Per creator | | Edit and delete other members' API tokens | Yes (dashboard) | Yes (dashboard) | No | No | No | "Per creator" means a token lists what its creator would see: every token for an Owner or Admin, their own for a Restricted member. ### Things that are not roles - **Cloider has no per resource permissions.** A role applies to the whole account. To separate environments, use separate accounts (for example one for production that few people can reach) or separate API tokens with the least access they need. - **Registry tokens and S3 access keys are not members.** They are credentials for one product (`docker` and S3 tools), scoped by what you choose when you create them. See [Registry tokens](/container-registry/tokens) and [Access keys](/object-storage/access-keys). - **Kubernetes RBAC inside a cluster is yours.** The kubeconfig Cloider gives you is a cluster admin credential. Create service accounts and RoleBindings inside the cluster for finer access. ### Next steps - [Access control](/account/access-control): invite members and change roles, step by step. - [API tokens](/account/api-tokens): create tokens for the CLI, Terraform, CI and the API. - [Switch accounts](/account/switch-accounts). - [Security](/concepts/security). --- ## Cloider billing model, pay as you go explained Cloider is **pay as you go**: there are no plans to commit to, no upfront fees and no charge for idle machines. Cloider measures what each resource uses, prices it at monthly rates prorated to the second, and sends one invoice per month for everything the account used. This page explains exactly what is measured and how. Current prices are on [cloider.com/#pricing](https://cloider.com/#pricing), from `cloider pricing`, and from the public `GET https://cloider.com/api/pricing` endpoint. The [Pricing reference](/reference/pricing) lists every price and unit. ### At a glance | | | | --- | --- | | **Model** | Pay as you go, metered continuously, invoiced monthly | | **Price unit** | Every price is per month. A billing month is 30 days (720 hours), and usage is prorated by the second | | **Compute** | Apps and clusters pay for **compute units**: CPU or memory, whichever is larger at the current ratio | | **Storage** | Cluster volumes, buckets and registry projects pay per GiB per month | | **Databases** | A flat monthly price per plan, prorated; only storage while paused | | **Free** | Domains, DNS, bucket requests and traffic, registry pushes and pulls | | **Trial** | 14 days with $25 of free credit, one cluster, database, bucket and registry project at a time | | **Invoices** | One per month for the month before, visible to the account Owner only | ### What is metered, product by product | Product | Metered | How it is measured | Price kind | | --- | --- | --- | --- | | **Kubernetes cluster**: compute | CPU and memory of the cluster's running containers, including its Kubernetes control plane | Sampled every few seconds. Each container counts the larger of what it **requests** and what it **uses** | Cluster compute unit per month | | **Kubernetes cluster**: volumes | Persistent volumes the cluster's PersistentVolumeClaims are bound to | Provisioned capacity in GiB, sampled every few seconds | Cluster volume, per GiB per month | | **Kubernetes cluster**: load balancers | Services of type `LoadBalancer` that have a public IP | Time each one is active | Cluster load balancer, per month each | | **App** | CPU and memory of the app's containers | Sampled every minute | App compute unit per month | | **Database** | The database's plan | Time the database is **Ready**, at the plan's monthly price | Database plan price per month, per engine | | **Database while paused** | The storage the plan provisioned | Time the database is **Paused** | Cluster volume rate, per GiB per month | | **Bucket** | Data stored | GiB stored, sampled every few minutes and averaged over time | Bucket storage, per GiB per month | | **Registry project** | Image data stored | GiB stored, sampled every few minutes and averaged over time | Registry storage, per GiB per month | | **Domains, DNS records, targets** | Nothing | | Free | #### Compute units Apps and clusters are billed in **compute units**. One compute unit is one vCPU, or the amount of memory the **CPU to memory ratio** pairs with one vCPU. A workload counts as whichever is larger: ```text compute units = max(vCPU, memory / memory per vCPU) ``` For example, if the ratio is 1 vCPU to 3 GiB: | Workload uses | vCPU | Memory / 3 GiB | Compute units billed | | --- | --- | --- | --- | | 0.5 vCPU, 1 GiB | 0.5 | 0.33 | 0.5 | | 0.25 vCPU, 3 GiB | 0.25 | 1.0 | 1.0 | | 2 vCPU, 6 GiB | 2 | 2.0 | 2.0 | So a CPU heavy workload pays for its CPU and a memory heavy workload pays for its memory, never both. The current ratio and compute unit prices for clusters and apps are listed by `cloider pricing` (shown as `cpu to memory ratio`) and in the [Pricing reference](/reference/pricing). Clusters and apps have separate prices that can differ. #### Requests count, not only usage For each running container, Cloider bills the **larger of its resource request and its actual use**, for CPU and for memory separately. A container that requests `500m` CPU and uses `100m` is billed for `500m`; one that requests `100m` and uses `300m` is billed for `300m`. This makes requests a budget lever: set small requests for workloads that idle, and your bill follows what they actually use. In Kubernetes, requests are set per container under `resources.requests`. #### How a month is prorated Every price is a monthly price, and a billing month counts as 30 days (720 hours). Usage is accumulated to the second, so: - a resource that exists for half a month costs half its monthly price, - an invoice shows hourly rates, which are the monthly price divided by 720, - a database on a plan priced at a monthly amount costs that amount for 720 hours of being Ready. #### When prices change Prices have effective dates. Each day's usage is priced at the rates in effect that day, so a price change applies from its date onward and never retroactively. If prices change during a month, the invoice splits that resource's lines into one line per price period. ### The free trial and credit A new account starts on the trial: 14 days, with $25 of free credit as advertised on [cloider.com](https://cloider.com). During the trial: - usage is metered exactly as it is on the paid plan, so the usage page shows real costs, - the account can have one Kubernetes cluster, one database, one bucket and one registry project at a time, - the Owner receives reminder emails before the end. If the Owner doesn't upgrade, the account is suspended the day after the trial ends and its resources are deleted. See [Create an account](/get-started/create-an-account#the-free-trial). ### What does a paused resource cost? Pausing stops compute but keeps data, so a paused resource costs only its storage, if it has any. | Resource | While paused you pay for | You stop paying for | | --- | --- | --- | | **Kubernetes cluster** | Its persistent volumes (provisioned GiB) | Compute: the control plane and workloads stop running | | **App** | Nothing | Compute: the app's containers stop | | **Database** | The storage of its plan, at the cluster volume rate (`paused_price` in the pricing endpoint) | The plan price | | **Bucket, registry project** | Can't be paused: data stored is billed until you delete it | | While a resource is moving between states (**Creating**, **Pausing**, **Starting**...), a database accrues nothing; clusters and apps pay for whatever compute is actually running. ### What does a deleted resource cost? Nothing from the moment it is deleted. Usage up to that moment stays on the month's invoice. Deleted resources stay visible on the **Usage** page, marked as terminated, for 60 days after their last usage, so you can see what they cost. ### Forecasts The **Usage** page and `cloider usage` show, for each resource and for the account: - **Cost to date (this month)**: what has accrued since the first of the month. - **Forecast this month**: cost to date plus the rest of the month at the current rate. - **Forecast next month**: a whole month at the current rate. Forecasts assume that current usage, statuses and prices stay as they are. A database that is starting is forecast at its plan price, and one that is pausing at its storage price. See [Usage](/account/usage). ### Invoices Cloider issues one invoice per account per month, for the month that just ended. The invoice is created early in the new month, once the last day's usage has been finalized. - An invoice is only created when the month's total is above zero. - It has one **item per resource** (for example `Cluster: "production"`, `Database: "orders-db"`, `App: "blog"`, `Bucket: "acme-assets"`, `Registry project: "acme"`), each with lines that show the service, usage, unit, rate and cost. - Units on the invoice: **Compute-Hour** for compute, **GB-Hour** for volumes and storage, **Hour** for load balancers and database plans. - A **✓ Paid** tag appears once the payment is recorded. Only the account **Owner** can see invoices, in **Billing** in the dashboard, with `cloider billing`, or through `GET /api/billing` with the Owner's Read & Write API token. See [Billing](/account/billing). ```json title="One invoice from GET /api/billing (trimmed)" { "month": "September 2026", "amount": 45.63, "paid": false, "created_at": "2026-10-01T02:10:00Z", "items": [ { "service_type": "Kubernetes", "description": "Cluster: \"production\"", "amount": 30.63, "subitems": [ { "service": "Compute", "description": "Avg. CPU usage: 0.84 cores\nAvg. memory usage: 1.9 GB\nUsage time: 491.07 Hours", "qty": 412.5, "unit": "Compute-Hour", "price": 0.069444, "amount": 28.63 }, { "service": "Persistent Volumes", "description": "Avg usage: 20 GB", "qty": 14400, "unit": "GB-Hour", "price": 0.000139, "amount": 2.00 } ] } ] } ``` The amounts in this example are illustrative, not current prices. ### Who pays The account **Owner** is billed for every resource in the account, whoever created it. Members (Admins and Restricted) are never billed for resources in accounts they don't own. See [Accounts and roles](/concepts/accounts-and-roles). ### Keep costs low - **Pause what you don't use.** A paused cluster, app or database stops its compute. See [Resource lifecycle](/concepts/resource-lifecycle). - **Right size requests.** Since requests count, small requests on idle workloads save money. - **Delete load balancers you don't need.** Each active `LoadBalancer` Service is billed for the time it runs. Route domains with [targets](/domains/targets) instead. - **Clean up storage.** Delete unused volumes, use [lifecycle rules](/object-storage/lifecycle-rules) on buckets and [retention policies](/container-registry/retention-policies) in the registry. - **Watch the forecast.** Check [Usage](/account/usage) regularly, or script `cloider usage -o json`. ### Next steps - [Pricing reference](/reference/pricing) - [Usage](/account/usage) and [Billing](/account/billing) - [Resource lifecycle](/concepts/resource-lifecycle) --- ## How Cloider works, the platform and its products Cloider is a pay-as-you-go cloud platform that runs your applications, Kubernetes workloads, databases, files and container images on infrastructure that Cloider operates for you. You never pick a server size or patch an operating system: you create a resource, use it, and pay for what it uses. This page explains the model behind the products, so you know what to expect from each one. ### At a glance | Product | What you get | How it is billed | Managed with | | --- | --- | --- | --- | | [Apps](/apps/overview) | Your code built and run as a container, on a free HTTPS subdomain | CPU and memory used | Dashboard, CLI, API, MCP | | [Kubernetes](/kubernetes/overview) | A cluster with a standard Kubernetes API, no nodes to manage | CPU and memory used, volume GiB, load balancer time | Dashboard, CLI, Terraform, API, MCP | | [Databases](/databases/overview) | Managed PostgreSQL 18, MySQL 9, MongoDB 8 or Redis 8 on a plan | Flat monthly plan price, prorated; storage only while paused | Dashboard, CLI, Terraform, API, MCP | | [Object Storage](/object-storage/overview) | S3 compatible buckets | GiB stored; requests and traffic are free | Dashboard, CLI, Terraform, API, MCP, any S3 tool | | [Container Registry](/container-registry/overview) | Private projects at `registry.cloider.com` | GiB stored; pushes and pulls are free | Dashboard, CLI, Terraform, API, MCP, Docker | | [Domains and DNS](/domains/overview) | Free `cloider.app` subdomains, your own domains, DNS records, routing | Not metered | Dashboard, CLI, Terraform, API, MCP | ### The serverless model On a traditional cloud you rent machines and fit your software onto them. On Cloider you describe what you want to run and Cloider places it on shared, managed infrastructure: - **No machines to size.** A Kubernetes cluster has no node pools. An app has no instance type. You set resource requests the Kubernetes way, or let Cloider run your app as built. - **No idle machines to pay for.** Compute is metered from what your workloads request and use, sampled continuously, so a quiet workload costs little. - **Standard interfaces.** Clusters speak the Kubernetes API, buckets speak S3, the registry speaks the Docker Registry HTTP API V2, databases speak their own wire protocols. Your existing tools work, and you can take your data and configuration elsewhere. - **Everything in one account.** One account holds every product, one usage page shows what each resource costs, and one monthly invoice covers it all. ```mermaid flowchart TB subgraph tools[Tools] D[Dashboard] C[cloider CLI] T[Terraform provider] M[MCP server] R[Your code] end API[Cloider REST API
cloider.com/api] subgraph products[Your account] A[Apps] K[Kubernetes clusters] DB[Databases] B[Buckets] REG[Registry projects] DNS[Domains and DNS] end D --> API C --> API T --> API M --> C R --> API API --> products ``` ### One API underneath every tool The dashboard, the CLI, the Terraform provider and the MCP server all use the same public REST API at `https://cloider.com/api`. Anything you can click, you can script, with the same validation and the same error messages. - The **dashboard** authenticates with a session from your Google or GitHub sign in. - Every other tool authenticates with an **API token** (`cldr_...`) that belongs to one account. - A few actions are dashboard only on purpose: inviting members, changing roles, and creating or deleting API tokens. See [Security](/concepts/security). Read [Choose a tool](/get-started/choose-a-tool) for when to use which. ### How each product runs #### Apps An app is source code that Cloider turns into a running service: 1. **Source.** You upload a folder or zip (dashboard or `cloider deploy`), or import a GitHub, GitLab or Bitbucket repository. Git apps can deploy on every push. 2. **Detection.** Cloider inspects the code to find the language, framework and port. 3. **Build.** If the project has a Dockerfile, Cloider builds it. Otherwise it builds the code with buildpacks. A folder of static HTML is served as a static site. 4. **Image.** The image is stored in your account's own container registry project for apps. 5. **Run.** Cloider runs the image as containers and routes your free subdomain (for example `https://blog.cloider.app`) to it with HTTPS. Each deploy is a numbered build (version 1, 2, 3...). You pay for the CPU and memory the app's containers use. A paused app runs no containers and accrues no compute. Read the [Apps overview](/apps/overview). #### Kubernetes clusters Each Cloider cluster is a dedicated Kubernetes control plane (its own API server and cluster state) that Cloider runs for you on shared infrastructure. Your Pods, Services and volumes are scheduled onto Cloider's managed worker capacity. From `kubectl`'s point of view it is a normal cluster: you get a kubeconfig with admin credentials and can install anything that runs on Kubernetes. - **Versions**: you choose a Kubernetes version when you create a cluster, from the versions currently offered (`GET /api/options` lists them). A cluster keeps its version; to move to a newer one, create a new cluster. - **Reaching your workloads**: route a domain to a Service with a [target](/domains/targets), or create a Service of type `LoadBalancer` to get a public IP. Ingress objects in the cluster are not served. - **Storage**: PersistentVolumeClaims get [persistent volumes](/kubernetes/persistent-volumes), billed per GiB of provisioned capacity. - **Billing**: compute is metered per container as the larger of what it requests and what it uses, plus volumes and load balancers. The cluster's own control plane components are part of its metered usage. Read the [Kubernetes overview](/kubernetes/overview). #### Databases A managed database is a PostgreSQL, MySQL, MongoDB or Redis database that Cloider provisions, configures, backs up and keeps running. - **Engines and versions**: PostgreSQL 18, MySQL 9, MongoDB 8 and Redis 8. - **Plans**: Hobby, Starter, Basic, Standard, Plus and Pro, each with a fixed CPU, RAM and storage size. You can [resize](/databases/resize) to a larger plan. - **Network access**: each database has a public endpoint under `db.cloider.com` and a list of [trusted sources](/databases/trusted-sources) that controls who can connect, plus optional [SSL/TLS](/databases/ssl-tls). - **Backups**: databases get a full backup every 14 days, and every change in between is recorded, so they can be restored to any second of the last 14 days from the dashboard, CLI, API or MCP. See [Backups and restore](/databases/backups-and-restore). - **Billing**: a flat monthly price per plan, prorated to the time the database is ready. While paused, only its storage is billed. Read the [Databases overview](/databases/overview). #### Object storage Buckets are S3 compatible object storage. Any S3 client, SDK or tool (AWS CLI, rclone, s3cmd, the AWS SDKs) works with them, using the endpoint of the bucket's region and an access key. - **Region and endpoint**: buckets live in the `eu-central-2` region (Europe), at `https://s3.eu-central-2.cloider.com`. Both path style and virtual hosted style addressing work. - **Access**: [access keys](/object-storage/access-keys) (S3 credentials) are managed per account in **Access Control**, and a bucket can be [public or private](/object-storage/public-and-private-buckets). - **Features**: [versioning](/object-storage/versioning), [lifecycle rules](/object-storage/lifecycle-rules), [bucket policies](/object-storage/bucket-policies), [static websites](/object-storage/static-websites) and a file browser in the dashboard. - **Billing**: GiB stored, averaged over time. Requests and data transfer are free. Read the [Object Storage overview](/object-storage/overview). #### Container registry The container registry stores Docker and OCI images in **projects** at `registry.cloider.com//:`. You log in with `cloider registry login` or a registry token, then use `docker push` and `docker pull` as usual. - **Projects** have a storage limit you set (5 GB by default, 20 GB at most) that stops pushes beyond it. - **Tokens** can be read only (pull) or read and write (push), for CI systems and clusters. - **Policies**: [retention rules](/container-registry/retention-policies) delete old images, and [immutable tags](/container-registry/immutable-tags) protect tags from being overwritten. - **Your apps' images** live in your account's apps project, which Cloider creates with your first app. - **Billing**: GiB stored, averaged over time. Pushes and pulls are free. Read the [Container Registry overview](/container-registry/overview). #### Domains and DNS Cloider runs authoritative DNS for your domains and routes HTTP traffic to your apps, cluster services and bucket websites. - **Free subdomains**: claim a name under `cloider.app`, such as `blog.cloider.app`, with HTTPS included. - **Custom domains**: add a domain you own and point it at Cloider's nameservers, `ns1.cloider.com` to `ns4.cloider.com`, then manage its [DNS records](/domains/dns-records). - **Targets**: a [target](/domains/targets) routes a path of a domain (for example `example.com/api`) to an app or to a Service in a cluster, or a whole domain to a bucket's static website. Read the [Domains overview](/domains/overview). ### Regions Object storage is the one product that names a region: buckets are created in `eu-central-2`, shown as **Europe** in the dashboard, and `GET /api/buckets/regions` lists the regions you can choose from with their endpoints. Apps, clusters, databases and the registry don't take a region parameter. ### Provisioning is asynchronous Creating a cluster, a database or an app starts work that takes from seconds to a few minutes. The API answers straight away with the new resource's id, and the resource's status then moves from **Creating** (or **Pending** for an app) to **Ready**. Every tool can wait for you: - the dashboard updates the status in place, - the CLI has `--wait`, - Terraform waits until the resource is ready before it continues, - the MCP tools take a `wait` argument. Read [Resource lifecycle](/concepts/resource-lifecycle) for every status and transition. ### Next steps - [Accounts and roles](/concepts/accounts-and-roles) - [Billing model](/concepts/billing-model) - [Security](/concepts/security) --- ## Cloider concepts The concept pages explain how Cloider works underneath the buttons and commands. Read them once and the rest of the documentation makes more sense: why a paused cluster still costs a little, why an API token can't invite members, or why a resource has both a name and an id. ### At a glance | Concept | In one sentence | Page | | --- | --- | --- | | The platform | Six products (apps, Kubernetes, databases, object storage, container registry, domains) on shared, managed infrastructure, used from five tools | [How Cloider works](/concepts/how-cloider-works) | | Accounts and roles | Resources belong to an account; people reach it as the Owner or as Admin or Restricted members | [Accounts and roles](/concepts/accounts-and-roles) | | Billing | Usage is metered continuously, priced per month, prorated to the second and invoiced monthly | [Billing model](/concepts/billing-model) | | Lifecycle | Every resource moves through statuses such as Creating, Ready, Paused and Terminating | [Resource lifecycle](/concepts/resource-lifecycle) | | Security | Isolation per account, HTTPS everywhere, hashed tokens, trusted sources and a shared responsibility model | [Security](/concepts/security) | The platform, the serverless model and how each product runs. Users, accounts, members, roles and the full permission matrix. What is metered, how it is priced, the trial and invoices. Statuses, pausing, deleting, names, labels and ids. Isolation, TLS, tokens, trusted sources and who is responsible for what. ### Related reference - [Pricing](/reference/pricing): every price kind and unit. - [Limits and quotas](/reference/limits-and-quotas): name lengths, per account maximums and sizes. - [Status values](/reference/status-values): every status, per product. - [Glossary](/reference/glossary): every term used in these docs. --- ## Resource lifecycle, statuses, pausing and deleting Every Cloider resource (a cluster, an app, a database, a bucket, a registry project, a domain) goes through the same broad life: it is **created**, **provisioned** in the background, becomes **Ready**, can be **paused** and **resumed** if it runs compute, and is finally **deleted**. Its **status** tells you where it is at any moment. ### At a glance | Stage | Clusters | Databases | Apps | Buckets, registry projects | | --- | --- | --- | --- | --- | | Being created | **Creating**, then **Starting** | **Creating**, then **Starting** | **Pending**, **Queued**, **Building**, **Deploying**, **Starting** | **Creating** | | Usable | **Ready** | **Ready** | **Ready** | **Ready** | | Pausing | **Pausing**, then **Paused** | **Pausing**, then **Paused** | **Pausing**, then **Paused** | Can't be paused | | Resuming | **Starting**, then **Ready** | **Starting**, then **Ready** | **Starting**, then **Ready** | | | Changing size | | **Upgrading** (resize) | A new build | | | Deleting | **Terminating**, then gone | **Terminating**, then gone | **Terminating**, then gone | **Terminating**, then gone | | Couldn't be provisioned | | | **Failed** | **Failed** | Every status value is listed with its meaning in [Status values](/reference/status-values). ### Creating and provisioning Creating a resource is **asynchronous**. The API (and so the dashboard, the CLI, Terraform and the MCP tools) accepts the request, stores the resource and answers at once with its id. Cloider then provisions it in the background, which takes from a few seconds (a bucket) to a few minutes (a cluster). ```mermaid stateDiagram-v2 [*] --> Creating Creating --> Starting Starting --> Ready Ready --> Pausing: pause Pausing --> Paused Paused --> Starting: resume Ready --> Terminating: delete Paused --> Terminating: delete Terminating --> [*] ``` While a resource is provisioning, some actions are disabled. For example, a cluster's **Kubeconfig**, **Terminal** and **Pause** menu items are greyed out until it is **Ready**. #### Waiting for a resource to be ready **Dashboard** List pages and detail pages poll the status while a resource is changing and update it in place. You don't need to reload. **CLI** `--wait` (`-w`) makes `create`, `pause`, `resume`, `resize` and `delete` wait until the change is done, for up to 15 minutes by default (`--timeout` changes it). The `status` subcommands print the current status: ```bash cloider cluster create staging --wait cloider db status orders-db ``` ```text title="Output" Ready ``` If a resource settles on **Failed**, `--wait` exits with an error. **Terraform** Resources wait until they are ready before `terraform apply` moves on. Clusters wait up to 20 minutes by default, which you can change with a `timeouts` block: ```hcl resource "cloider_cluster" "staging" { name = "staging" timeouts = { create = "30m" } } ``` **API** Poll the resource's status endpoint, which answers with the status as plain text: ```bash curl -H "Authorization: Bearer $CLOIDER_TOKEN" https://cloider.com/api/clusters/q7m2x/status ``` ```text title="Response" Starting ``` Status endpoints exist for clusters, databases, apps, buckets and registry projects: `GET /api///status`. Once deleted, a cluster, database or app answers `Terminated`, and a bucket or registry project answers `404`. **MCP** Tools that start a long change take a `wait` argument, which waits up to 15 minutes: ```json title="Tool call: create_cluster" {"name": "staging", "wait": true} ``` ### Pausing and resuming **Pausing** stops a resource's compute while keeping its data and configuration, so you stop paying for compute. Clusters, databases and apps can be paused. Buckets, registry projects and domains have no compute and can't be. | Resource | What a pause keeps | What it stops | Billing while paused | | --- | --- | --- | --- | | Kubernetes cluster | Everything in the cluster: objects, configuration, persistent volumes | The control plane and all workloads. `kubectl` can't reach it | Persistent volumes only | | Database | Its data, users and settings | The database server | The storage of its plan | | App | Its image, environment variables and settings | The app's containers | Nothing | Resuming starts everything again from where it was. Each resource goes through **Starting** before it is **Ready**. **Dashboard** In the product's list page, open the row menu (three dots) and click **Pause**, then confirm. **Resume** is in the same menu once the resource is paused. **CLI** ```bash cloider cluster pause staging --wait cloider cluster resume staging --wait cloider db pause orders-db cloider app pause blog ``` **Terraform** Clusters have a `paused` argument: ```hcl resource "cloider_cluster" "staging" { name = "staging" paused = true } ``` **API** ```bash curl -X POST -H "Authorization: Bearer $CLOIDER_TOKEN" https://cloider.com/api/clusters/q7m2x/pause curl -X POST -H "Authorization: Bearer $CLOIDER_TOKEN" https://cloider.com/api/clusters/q7m2x/resume ``` The same `pause` and `resume` endpoints exist under `/api/databases/` and `/api/apps/`. **MCP** ```json title="Tool call: pause_cluster" {"cluster": "staging", "wait": true} ``` `pause_database`, `resume_database`, `pause_app`, `resume_app` and `resume_cluster` work the same way. Product specific details are in [Pause and resume a cluster](/kubernetes/pause-and-resume), [Pause, resume and delete a database](/databases/pause-resume-and-delete) and [Pause, resume and delete an app](/apps/pause-resume-and-delete). ### Deleting Deleting removes a resource **and everything in it**, permanently. A deleted cluster takes its workloads and volumes with it; a deleted database takes its data; a deleted bucket its files. There is no undo and no recycle bin. - The dashboard asks you to confirm, and for clusters you type the name. - The CLI asks you to type the name. In scripts, `--yes` skips the question; without a terminal, a delete without `--yes` fails instead of waiting. - MCP delete tools are marked as destructive, so your agent's client can ask you first. The status becomes **Terminating** while Cloider removes the resource, and the resource then disappears from lists. Billing stops when it is deleted. Its past usage stays visible on the [Usage](/account/usage) page for 60 days. Deleting an app also releases its free subdomains unless you keep them (`cloider app delete blog --keep-domains`). Deleting a domain deletes its DNS records and targets. :::danger[Back up before you delete] Download what you need first: database backups or a dump, the files in a bucket, the images in a registry project. Once a delete has started it can't be stopped. ::: ### Names, labels and ids Every resource has an **id**, and most have a **name** and a **label**: | | What it is | Example | Unique? | | --- | --- | --- | --- | | **id** | Cloider's identifier, used in API paths | `q7m2x` | Yes | | **name** | What you typed when you created it | `staging` | Not for clusters, databases and apps | | **label** | The name shown in the dashboard and CLI. The name, with `-1`, `-2`... added when another resource of the same kind already uses it | `staging-1` | Yes, among the account's resources of that kind | - **Clusters, databases and apps** can share a name. The second `staging` cluster gets the label `staging-1`. A database's label can be changed later (`cloider db rename orders-db orders`), which doesn't change its host, user or database name. - **Buckets and registry projects** have names that are unique across all of Cloider, so they don't need labels: if `acme-assets` is taken by someone else, pick another name. - **Domains** are identified by the domain name itself, such as `example.com`. - **Ids** are short strings of lowercase letters and digits for resources (`q7m2x`). Support requests have uppercase ids. #### Referring to a resource in the CLI and MCP The CLI and MCP tools accept a resource's **id, name or label** wherever they need one: ```bash cloider cluster get staging # by label or name cloider cluster get q7m2x # by id ``` If a name matches more than one resource, the CLI lists them and asks for the id: ```text title="Output" Error: 2 clusters are named "staging", pick one by id: staging, staging (id q7m2x) staging-1, staging (id k3p9a) ``` Terraform and the API always use ids. ### Next steps - [Status values](/reference/status-values): every status, per product. - [Billing model](/concepts/billing-model): what each status costs. - [Limits and quotas](/reference/limits-and-quotas): name rules and per account limits. --- ## Cloider security model and shared responsibility Security on Cloider is shared: Cloider secures the platform, the network edges and the credentials it issues, and you secure what you run on it and who you give access to. This page explains both halves and the controls you have. ### At a glance | Area | What Cloider does | What you control | | --- | --- | --- | | **Sign in** | Google or GitHub sign in, 24 hour sessions in HTTP-only cookies | Your Google or GitHub account security, such as two-factor authentication | | **Access** | Checks the account and role on every request | Who you invite and with which role ([Accounts and roles](/concepts/accounts-and-roles)) | | **API tokens** | Stores only a hash, shows the token once, re-checks the creator's role on every request | Read vs Read & Write, where tokens are stored, revoking unused ones | | **Encryption in transit** | HTTPS for the dashboard, the API, apps, domains, buckets and the registry | Turning on SSL/TLS for databases and requiring it in clients | | **Network** | Isolates accounts and resources | Database [trusted sources](/databases/trusted-sources), public or private buckets and registry projects | | **Workloads** | Runs and patches the platform, control planes and database servers | Your code, images, dependencies, Kubernetes RBAC and secrets | ### Account isolation Every resource belongs to exactly one account, and every API request is checked against the account it is made in. A request for a resource of another account answers `404 Not Found`, so ids from other accounts are never confirmed to exist. - **Kubernetes clusters** each have their own control plane (API server and cluster state). The kubeconfig of one cluster gives no access to another. - **Databases** each have their own server, users and password, and their own list of trusted sources. - **Buckets** are reached with access keys that belong to your account. - **Registry projects** are private by default and reached with tokens that belong to your account. ### Signing in and sessions You sign in to the dashboard with **Google** or **GitHub**. Cloider never sees or stores a password. With GitHub, Cloider uses your primary, verified email address. - A dashboard session is kept in **HTTP-only cookies**, which page scripts can't read, and lasts **24 hours**. - **Logout** ends the session in that browser. - Your access is checked on every request: if an Owner or Admin removes or deactivates you, the account's API stops answering your requests at once. Protect the Google or GitHub account you sign in with, for example with two-factor authentication: it is the key to your Cloider account. ### API tokens API tokens let the CLI, Terraform, the MCP server and your own code call the API. Their design limits what a leaked token can do: - **Shown once.** A token is displayed only when it is generated. Cloider stores only a hash of it, so nobody, including Cloider, can show it again. - **Recognizable.** Every token starts with `cldr_` followed by 40 random characters, so secret scanners and code reviews can spot a leaked one. - **One account.** A token works in the account it was created in, and nowhere else. - **Never more than its creator.** A token acts with its creator's current role, looked up on every request. Removing, deactivating or demoting the creator takes effect on their tokens immediately. - **Read or Read & Write.** A **Read** token can only make read requests, whatever its creator's role. - **Can't escalate.** No token can invite members, change roles or create other tokens: those actions only work in a signed in dashboard session. The only change a token can make to tokens is to revoke itself. - **Traceable.** **Access Control**, **API Tokens** shows each token's description, creator, permission and when it was last used. Read [API tokens](/account/api-tokens) to manage them. ### Credentials for each product Besides API tokens, each product issues its own credentials. Treat all of them like passwords. | Credential | Grants | Shown | Where to manage it | | --- | --- | --- | --- | | **Kubeconfig** | Cluster admin access to one Kubernetes cluster | Any time, to every member | [Connect with kubectl](/kubernetes/connect-with-kubectl) | | **Database password** | The database user's access to one database | Any time, to every member | [Connect](/databases/connect) | | **S3 access key** | Access to buckets through any S3 tool | Secret shown once | [Access keys](/object-storage/access-keys) | | **Registry token** | Pull, or pull and push, to registry projects, optionally with an expiry | Secret shown once | [Registry tokens](/container-registry/tokens) | | **API token** | The Cloider API, as its creator | Shown once | [API tokens](/account/api-tokens) | Every member, including Restricted members, can read kubeconfigs and database passwords. Invite only people you trust with them. ### Encryption in transit - **Dashboard and API**: `https://cloider.com` and `https://cloider.com/api` are HTTPS only. - **Apps and domains**: free `cloider.app` subdomains and your own domains are served over HTTPS with certificates Cloider manages. See [HTTPS certificates](/domains/https-certificates). - **Object storage**: the S3 endpoint `https://s3.eu-central-2.cloider.com` uses HTTPS. - **Container registry**: `registry.cloider.com` uses HTTPS. - **Databases**: PostgreSQL, MySQL, MongoDB and Redis databases support SSL/TLS, which you turn on per database. You can download the database's CA certificate to verify the server. See [SSL/TLS](/databases/ssl-tls). ### Network access - **Databases** are reachable only from their **trusted sources**, the list of addresses and Cloider resources you allow. Keep the list as short as possible. See [Trusted sources](/databases/trusted-sources). - **Buckets** are private unless you make them public. A public bucket's files can be read by anyone with the URL. Use [bucket policies](/object-storage/bucket-policies) for finer rules. - **Registry projects** are private unless you make them public. Anyone can pull from a public project without a token; pushing always needs one. - **Cluster workloads** are only reachable from the internet when you expose them, with a [target](/domains/targets) or a `LoadBalancer` Service. ### Shared responsibility | Cloider is responsible for | You are responsible for | | --- | --- | | The physical and virtual infrastructure under every product | The code, containers and images you deploy, and their dependencies | | Running and monitoring Kubernetes control planes | What runs inside your clusters: RBAC, service accounts, network policies, secrets | | Running database servers and taking backups | Database users, data, queries, trusted sources and enabling SSL/TLS | | HTTPS certificates for apps and domains | DNS records you create for your own domains | | Hashing API tokens and enforcing roles | Who you invite, which role you give, and storing tokens and keys safely | | Isolating accounts from each other | Making buckets or registry projects public, and bucket policies | ### Best practices 1. **Use the least access that works.** Give people the Restricted role unless they need to change things. Use **Read** API tokens for monitoring and `terraform plan`. 2. **One token per use.** Create a separate token for each script, CI pipeline and computer, with a clear description, so you can revoke one without breaking the others. 3. **Keep tokens out of code.** Pass them in environment variables (`CLOIDER_TOKEN`) or secret files (`CLOIDER_TOKEN_FILE`), and in your CI system's secret store. Never commit them. 4. **Revoke what you don't use.** Delete tokens, access keys and registry tokens that haven't been used for a while; **Last used** tells you which. 5. **Remove people who leave.** Removing a member deletes their tokens too. 6. **Lock databases down.** Keep trusted sources short and turn on SSL/TLS. 7. **Keep buckets and registry projects private** unless their content is meant to be public. 8. **Use an MCP server in read-only mode** (`cloider mcp --read-only`) when an AI agent only needs to look. See [MCP safety and permissions](/mcp/safety-and-permissions). ### Report a security issue If you find a security problem in Cloider, contact support@cloider.com or open a [support request](/account/support) with the type **Product Related Question or Issue**. Don't include working credentials in the report. ### Next steps - [Accounts and roles](/concepts/accounts-and-roles) - [API tokens](/account/api-tokens) - [Trusted sources](/databases/trusted-sources) --- ## How Cloider builds an app (Dockerfile, buildpacks, static) Every time you deploy, Cloider turns your source into a container image with one of three **build strategies**: your own `Dockerfile`, **Cloud Native Buildpacks** for a project without one, or a **static site** server for plain HTML. This page explains how the strategy is chosen, what detection does, how the port and start command work, and how to pass values to a build with build arguments and build variables. ### At a glance Cloider checks the root of your source and uses the first strategy that applies: | Order | Strategy | Used when | The image starts with | | --- | --- | --- | --- | | 1 | **Dockerfile** | A file named exactly `Dockerfile` is at the root | The Dockerfile's `CMD` / `ENTRYPOINT` | | 2 | **Static site** | No `Dockerfile`, no start command, no project files at the root, and at least one `.html` or `.htm` file anywhere | A web server that serves your files on `$PORT` | | 3 | **Buildpacks** | Everything else | The start command, a `Procfile`, or what the buildpack detects | ```mermaid flowchart TD S[Source root] --> D{Dockerfile
at the root?} D -- yes --> DF[Build the Dockerfile] D -- no --> C{Start command set?} C -- yes --> BP[Buildpacks] C -- no --> H{HTML files and no
project files?} H -- yes --> ST[Static site] H -- no --> BP BP -- no buildpack matches --> F[Build fails:
nothing to build] ``` All three build for `linux/amd64`, push the image to your account's private apps registry project, and are limited to 30 minutes for the whole build. ### What does Cloider detect? When you create an app, Cloider reads the uploaded source once to fill in the **Configure your app** form for you. It reports: | Detected | Where it is used | | --- | --- | | **Language** and **framework** (for example JavaScript and Express, Python and Django, Go) | Shown on the form and as **Detected** on the app's **Overview** tab | | **Port** the project declares, such as in its framework configuration or a Dockerfile's `EXPOSE` | Prefilled as the app's **Port** | | **Name** from the project's own manifest, such as `package.json` or `pom.xml` | Prefilled as **App Name**, made valid (lowercase, `-` for anything else) | | **ARGs** your `Dockerfile` declares | Offered as **Declared in your Dockerfile** buttons under **Build arguments** | Detection is only a suggestion. The build never reads it: a `Dockerfile` or a buildpack decides how the app is built, and the port you confirm decides where traffic goes. When detection recognizes nothing, the form keeps its defaults (port `8080`) and the app is created normally. When a repository holds several projects, detection reports on the one closest to the root. ### Build with your own Dockerfile A `Dockerfile` at the root of the source always wins. Cloider builds it with BuildKit exactly as written, with the source root as the build context, and runs the image's own command. Use it when you need a specific base image, system packages, a compiled language buildpacks don't cover, or a multi-stage build. ```dockerfile title="Dockerfile" FROM node:22-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM node:22-alpine WORKDIR /app ENV NODE_ENV=production COPY --from=build /app/package*.json ./ RUN npm ci --omit=dev COPY --from=build /app/dist ./dist # Cloider sets PORT to the app's port CMD ["node", "dist/server.js"] ``` Things to know: - The file must be named `Dockerfile` (capital D) and sit at the root. `Dockerfile.prod`, `docker/Dockerfile` or `dockerfile` aren't used. If you upload a zip with a single top folder, its contents are the root. - The image's `ENTRYPOINT` and `CMD` start the app. A **Start command**, when you set one, runs with `sh -c` in their place, so the image needs a shell. Changing it redeploys the app without building it again. - `EXPOSE` doesn't route anything. Traffic goes to the app's **Port**, and the container gets that port as `PORT`. Either listen on `$PORT`, or set the app's port to the port your image listens on. - The image runs as the user the Dockerfile sets. Running as root works, and so does a non-root user on a port below 1024. - A `.dockerignore` at the root is honored as usual by BuildKit. - Only variables you set as **build arguments** reach the build. See [Build arguments](#build-arguments-for-a-dockerfile). ### Build with buildpacks A project without a `Dockerfile` is built with **Cloud Native Buildpacks**, using Heroku's open source builder (`heroku/builder:26`). A buildpack recognizes your language from the project files at the root (such as `package.json`, `requirements.txt`, `go.mod` or `pom.xml`), installs the runtime and your dependencies, runs your build step, and works out how to start the app. See [Supported languages](/apps/supported-languages) for each language's files and conventions. The finished image runs as a non-root user named `cloider`, gets the app's port as `PORT`, and must listen on it. #### How a buildpack app starts In order of precedence: 1. The app's **Start command** setting, if set. Cloider writes it into the source as a `Procfile` line, `web: `, replacing any `Procfile` you had. 2. A `Procfile` at the root of your project. Only the `web` process runs. 3. What the language's buildpack defines, for example the `start` script in a Node.js `package.json`. ```text title="Procfile" web: gunicorn app:app --bind 0.0.0.0:$PORT ``` #### Buildpack caching Cloider keeps a build cache for each app, and each build starts from the layers of the version deployed before it. The first build of an app downloads its runtime and dependencies; later builds usually reuse them and finish faster. ### Static sites A source with HTML and nothing else is served as a **static site**: no build step, no runtime. Cloider uses this strategy when all of these are true: - There is no `Dockerfile` at the root. - The app has no **Start command**. - None of these files is at the root: `package.json`, `requirements.txt`, `Pipfile`, `Pipfile.lock`, `pyproject.toml`, `poetry.lock`, `uv.lock`, `setup.py`, `setup.cfg`, `runtime.txt`, `.python-version`, `go.mod`, `pom.xml`, `mvnw`, `build.gradle`, `build.gradle.kts`, `settings.gradle`, `settings.gradle.kts`, `gradlew`, `system.properties`, `build.sbt`, `composer.json`, `composer.lock`, `Gemfile`, `Gemfile.lock`, `Procfile`, `project.toml`, or a `.csproj`, `.fsproj`, `.vbproj`, `.sln`, `.slnx` or `.cs` file. - There is no `.php` file anywhere. - There is at least one `.html` or `.htm` file somewhere. Every file of the source is served as is by an unprivileged nginx web server listening on `$PORT`. Put an `index.html` at the root: it is what `/` serves, and without it `/` answers `403 Forbidden`. Files are made readable for the server whatever permissions the zip carried. ```text my-site/ index.html about.html css/style.css img/logo.png ``` A site that needs a build step (React, Vue, Astro and other frameworks) has a `package.json`, so it is built by the Node.js buildpack instead. See [Supported languages](/apps/supported-languages#static-sites-and-single-page-apps). ### The app's port The **Port** is the one port Cloider sends traffic to. It is required, between 1 and 65535, and defaults to the detected port or `8080`. - Cloider sets the `PORT` environment variable in the container to this port. A variable named `PORT` that you set yourself is overridden. - A version only goes live once something accepts connections on this port, so an app listening elsewhere never becomes ready and its build fails with **The app did not start in time**. - Listen on all interfaces (`0.0.0.0`), not only on `localhost` / `127.0.0.1`, or the platform can't reach it. ```js title="server.js" const port = process.env.PORT || 3000; app.listen(port, "0.0.0.0", () => console.log(`Listening on ${port}`)); ``` Changing the port on the **Settings** tab redeploys the running version with the new port, without a rebuild. See [Settings](/apps/settings). ### The start command The **Start command** is optional and only used by buildpack builds. Leave it empty to let the project decide through its `Procfile` or its language's conventions. It must be a single line of at most 1000 characters. Setting it also stops a source from being treated as a static site, since a command means something should run. Changing the start command on the **Settings** tab rebuilds the source the app is serving, because the command is built into the image. ### Build arguments, build variables and environment variables An app has three lists of variables, and each reaches a different place: | List | Reaches | Strategy | Set with | | --- | --- | --- | --- | | **Environment variables** | The running app, when its container starts | All | **Environment** tab, `cloider app env`, `--env` / `--secret-env` | | **Build arguments** | The `docker build` of your `Dockerfile` | Dockerfile | **Environment** tab, `cloider app build-args`, `--build-arg` / `--secret-build-arg` | | **Build variables** | The buildpack build | Buildpacks | **Environment** tab, `cloider app build-env`, `--build-env` / `--secret-build-env` | Build arguments and build variables never reach the running app. Each variable in each list can be **secret** (locked): its value is never listed back, only revealed on request. Changing a build list rebuilds the version the app is serving. See [Environment variables](/apps/environment-variables) for limits, secrets and the full how-to. #### Build arguments for a Dockerfile A plain build argument is passed as `--build-arg`, which your Dockerfile reads with `ARG`: ```dockerfile title="Dockerfile" FROM node:22-alpine WORKDIR /app COPY . . # Set NEXT_PUBLIC_API_URL as a build argument of the app ARG NEXT_PUBLIC_API_URL ARG NODE_VERSION_LABEL=22 RUN npm ci && npm run build CMD ["npm", "start"] ``` An empty value isn't passed at all, so the `ARG`'s own default applies. That is how the **Declared in your Dockerfile** suggestions are added: with an empty value, keeping your defaults until you type one. A **secret build argument** is passed as a BuildKit build secret instead, never as a build arg, because build args are kept in the image's history. Read it with a secret mount, which exists only for that one `RUN` step: ```dockerfile title="Dockerfile" FROM node:22-alpine WORKDIR /app COPY package*.json .npmrc ./ # NPM_TOKEN is a secret build argument of the app; .npmrc reads ${NPM_TOKEN} RUN --mount=type=secret,id=NPM_TOKEN,env=NPM_TOKEN npm ci COPY . . CMD ["node", "server.js"] ``` The secret's id is the variable's name. Without `env=`, the value is a file at `/run/secrets/`: ```dockerfile RUN --mount=type=secret,id=PIP_INDEX_URL \ pip install --index-url "$(cat /run/secrets/PIP_INDEX_URL)" -r requirements.txt ``` **Dashboard** 1. Open the app and go to the **Environment** tab. 2. Under **Build arguments**, click a name under **Declared in your Dockerfile**, or **Add argument**. 3. Type the value. Click the lock icon to store it as a secret. 4. Click **Save and rebuild** (or **Save changes** if the app has never deployed). **CLI** ```bash cloider app build-args set web NEXT_PUBLIC_API_URL=https://api.example.com cloider app build-args set web --secret NPM_TOKEN=npm_xxxxxxxxxxxxxxxx ``` ```text title="Output" ✓ Saved 1 argument(s), building web again ✓ Saved 2 argument(s), building web again ``` On a first deploy, pass them as flags: ```bash cloider deploy --name web \ --build-arg NEXT_PUBLIC_API_URL=https://api.example.com \ --secret-build-arg NPM_TOKEN=npm_xxxxxxxxxxxxxxxx ``` **Terraform** The Terraform provider can't manage app variables. Use the dashboard, the CLI or the API. **API** `PUT` replaces the whole list. A secret sent back with `"value": null` keeps its stored value: ```bash curl -X PUT https://cloider.com/api/apps/w4d8hz/build-args \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"env_vars": [ {"key": "NEXT_PUBLIC_API_URL", "value": "https://api.example.com", "secret": false}, {"key": "NPM_TOKEN", "value": "npm_xxxxxxxxxxxxxxxx", "secret": true} ]}' ``` ```json title="Response" {"applied": "build"} ``` `applied` is `build` when a rebuild of the running version started, `none` when the app hasn't deployed yet. **MCP** Ask your agent: ```text title="Prompt" Set the build argument NEXT_PUBLIC_API_URL=https://api.example.com on the web app. ``` ```json title="Tool call: set_app_variables" {"app": "web", "kind": "build-args", "variables": {"NEXT_PUBLIC_API_URL": "https://api.example.com"}} ``` #### Build variables for buildpacks **Build variables** are the environment a buildpack build runs in. Use them to configure the buildpacks, or for anything your build step reads, such as a token for a private package registry: ```bash cloider app build-env set blog API_BASE_URL=https://api.example.com cloider app build-env set blog --secret NPM_TOKEN=npm_xxxxxxxxxxxxxxxx ``` The buildpack build also sees the app's **plain** (non-secret) environment variables. A build variable with the same name wins over an environment variable. Secret environment variables never reach a build; a secret build variable is available to the build step alone and isn't stored in the image. ### Build limits | Limit | Value | | --- | --- | | Whole build, from download to pushed image | 30 minutes | | New version answering on its port after deploy | 5 minutes | | Source | 100 MB compressed, 500 MB extracted | | Platform | `linux/amd64` | | Builds at once | One per app | | Build output kept | The last 64 KB | A build that is still running after the limit fails. See [Troubleshooting apps](/apps/troubleshooting) for the messages each failure shows. ### Next steps - [Supported languages](/apps/supported-languages): minimal projects per language. - [Environment variables](/apps/environment-variables): runtime variables and secrets. - [Builds and deployments](/apps/builds-and-deployments): watch a build, read its logs, retry it. - [Settings](/apps/settings): change the port and start command. --- ## App builds and deployments on Cloider Every deploy of a Cloider app creates a **build**: a numbered version that downloads your source, builds an image, deploys it and waits until it is running. The **Builds** tab is the app's deployment history, with each build's status, source, timing and full output. This page explains the build lifecycle, how a new version replaces the old one, how to follow and read build logs, and how to retry or redeploy. ### At a glance - Builds are numbered per app: `1`, `2`, `3` and so on. A retry keeps the number and counts as another attempt. - One build runs at a time per app. A deploy, retry or push that arrives during a build is refused or skipped. - A new version goes live only once it is running and has stayed up for 15 seconds. Until then the previous version keeps serving, so a broken build never takes a working app down. - The whole build is limited to 30 minutes, and the new version must answer on its port within 5 minutes of deploying. - Each build keeps the last 64 KB of its output, including the app's own output when it fails to start. - Cloider keeps the images of the two most recently deployed versions. There is no rollback to older images. ### What starts a build? | Action | Source of the build | | --- | --- | | Creating an app | The uploaded folder or the fetched repository | | `cloider deploy` in a linked folder, or **Deploy new version** | A new upload | | A push to the branch of a git app with auto deploy on, **Deploy latest commit**, or `cloider deploy --git` | The newest commit of the branch | | Changing the **Start command** on **Settings** | The source the app is serving, built again | | Saving **Build arguments** or **Build variables** | The source the app is serving, built again | | **Retry** on a failed build | That build's own source, built again as a new attempt | | **Deploy**, `cloider deploy --app`, or the deploy hook of an [image app](/apps/deploy-an-image) | The image the app's tag points at, nothing is built | Changing the **Port** doesn't build: it redeploys the image the app is already running with the new port. Saving **Environment variables** doesn't build either: it restarts the app. ### Build statuses A build moves through these statuses. The app's own status mirrors the build that is running, so the Apps list shows how far a deploy has got. ```mermaid flowchart LR P[Pending] --> Q[Queued] --> B[Building] --> D[Deploying] --> S[Starting] --> R[Ready] R -- next version goes live --> SU[Superseded] P & Q & B & D & S --> F[Failed] Q --> C[Canceled] F -- Retry --> Q ``` | Status | Meaning | | --- | --- | | `Pending` | Waiting for the source: the upload hasn't finished, or the commit is being fetched from git | | `Queued` | The source is in place and the build is waiting for a builder | | `Building` | Downloading the source and building the image | | `Deploying` | The image is pushed and being applied to the platform | | `Starting` | The new container is starting. Cloider waits for it to accept connections on the app's port and stay up for 15 seconds | | `Ready` | This version is running. It is the app's live version, marked **live** in the list | | `Superseded` | This version was live and a newer one replaced it | | `Failed` | The build, the deploy or the start failed. The message says which. A failed build can be retried | | `Canceled` | The app was deleted while the build waited in the queue | A build that sits in `Pending` for 15 minutes, or in one of the running statuses for 45 minutes without moving, is marked `Failed` so it can't block the app's next build. ### How does a new version replace the old one? Deploys are rolling and safe by default: 1. While the new version builds, the current one keeps serving. 2. When the image is ready, Cloider starts a container of the new version **next to** the old one. 3. The new container only receives traffic once it accepts connections on the app's port. 4. Cloider then watches it for 15 seconds. If it stays up, the build is `Ready`, the previous build becomes `Superseded`, and the old container is stopped. 5. If the new container crashes, never opens the port within 5 minutes, or its image can't be pulled, the build is `Failed` and the app stays `Ready` on its previous version. The first version of an app has nothing to fall back to, so when its build fails the app itself is `Failed` until a build succeeds. ### Follow a build and read its logs A build's output covers the whole deploy: downloading the source, the `docker build` or buildpack output, the push, the deploy and the wait for the app to start. It streams live while the build runs, colors included. **Dashboard** 1. Open the app and go to the **Builds** tab. 2. Click **Logs** on a build. The **Build output** dialog opens; a running build shows **Live** and follows new lines. 3. Click the maximize button for a full screen view, and **Close** when done. **CLI** List the builds: ```bash cloider app builds blog ``` ```text title="Output" VERSION STATUS SOURCE CREATED MESSAGE 3 Ready blog.zip 2026-10-20 10:12 - 2 Failed blog.zip 2026-10-20 09:58 The app did not start in time 1 Superseded blog.zip 2026-10-19 16:40 - ``` A git app's **SOURCE** shows the short commit SHA and message instead, for example `9f2c1e7 Add health endpoint`. Add `-o wide` for the build ids. Print a build's output. With no build it is the latest; a build still running is followed until it ends: ```bash cloider app build logs blog # the latest build cloider app build logs blog 2 # version 2 (or v2, or its id) ``` `cloider deploy` follows the build it starts by default; use `--detach` to return at once. **Terraform** The Terraform provider has no access to app builds. Use the dashboard, the CLI or the API. **API** List the builds, newest first: ```bash curl https://cloider.com/api/apps/a7c2mq/builds \ -H "Authorization: Bearer $CLOIDER_TOKEN" ``` ```json title="Response" [ { "hashed_id": "c9w3rt", "version": "3", "attempt": 1, "status": "Ready", "source_filename": "blog.zip", "source_size": 19352, "uploaded": true, "commit_sha": "", "commit_message": "", "image_ref": "registry.cloider.com/acme-apps/blog-a7c2mq:3", "message": "", "started_at": "2026-10-20T10:12:31Z", "finished_at": "2026-10-20T10:14:02Z", "created_at": "2026-10-20T10:12:20Z" } ] ``` Read a finished build's output as plain text: ```bash curl https://cloider.com/api/apps/a7c2mq/builds/c9w3rt/logs \ -H "Authorization: Bearer $CLOIDER_TOKEN" ``` Follow a running build as server-sent events. Each chunk of output arrives as `data:` lines, and the stream ends with an `end` event. Reconnect with the `Last-Event-ID` header to resume where you left off: ```bash curl -N https://cloider.com/api/apps/a7c2mq/builds/c9w3rt/logs/stream \ -H "Authorization: Bearer $CLOIDER_TOKEN" ``` ```text title="Response" : open id: 1729419151000-0 data: Downloading source for app a7c2mq... event: end data: 1 ``` **MCP** Ask your agent: ```text title="Prompt" Why did the last build of the blog app fail? ``` It calls `list_app_builds` and then `get_app_build_logs`: ```json title="Tool call: get_app_build_logs" {"app": "blog"} ``` #### What is in a failed build's output? When a version builds but fails to start, the output ends with a `--- app output ---` line followed by the last 100 lines your app printed before it stopped. That is usually where the real error is: a missing environment variable, a module that isn't installed, or an exception at startup. ```text title="Build output (end)" Deploying the image... Waiting for the app to start... The app started and then stopped. --- app output --- Error: Cannot find module 'express' Require stack: - /workspace/server.js ``` ### Retry a failed build **Retry** runs a failed build again on the same source, keeping its version number and adding an attempt (shown as **attempt 2**). It is the right move for a failure that wasn't caused by your code, such as a registry hiccup or a timeout. To fix your code, deploy a new version instead. **Dashboard** 1. Open the app's **Builds** tab. 2. Click **Retry** on the failed build. **Retry** is disabled while another build runs or the app is paused. **CLI** ```bash cloider app build retry blog 2 --follow ``` ```text title="Output" ✓ Retrying version 2 ... ✓ Deployed version 2 of blog https://blog-x7k2.cloider.app/ ``` **Terraform** The Terraform provider has no access to app builds. Use the dashboard, the CLI or the API. **API** ```bash curl -X POST https://cloider.com/api/apps/a7c2mq/builds/f2h6pk/retry \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` The response is the build, back in `Queued` with `attempt` increased. **MCP** ```text title="Prompt" Retry the failed build 2 of the blog app and wait until it is up. ``` ```json title="Tool call: retry_app_build" {"app": "blog", "build": "2", "follow": true} ``` Only a `Failed` build can be retried, and only if its source was uploaded. A build whose upload never finished says **This build never received its source, deploy a new version instead!** ### Redeploy without changing the code There is no separate redeploy button; each of these redeploys the app: - **Restart with new settings**: save the **Environment** tab. The app restarts with the same image. - **Same image, new port**: change the **Port** on **Settings**. - **Rebuild the same source**: change the **Start command** or a build list. The source the app serves is built again as a new version. - **Rebuild a git app**: click **Deploy latest commit**, even when the branch hasn't moved. - **Rebuild an upload**: run `cloider deploy` again in the linked folder. - **Pull an image app's tag again**: click **Deploy** on the **Builds** tab of an [image app](/apps/deploy-an-image). When the tag still points at the running image, nothing is deployed. ### Roll back to an earlier version Cloider doesn't roll back to an older image. To go back, deploy the old code again as a new version: check out the earlier commit and run `cloider deploy`, or revert the commit in git and push. Because a failed build never replaces the live version, you rarely need to roll back after a bad deploy: fix the problem and deploy again. An [image app](/apps/deploy-an-image) rolls back by deploying a tag that still points at the older image. ### Verify - The newest build is `Ready` and marked **live** on the **Builds** tab. - The app's **Overview** shows the same **Live version**. - `cloider app get blog` prints the version and `Status Ready`. ### Troubleshooting | Build message | What it means | | --- | --- | | `Image build failed` | The `docker build` or buildpack build failed. The output says why | | `Deploy failed` | The platform didn't accept the deploy. Retry the build | | `The app image could not be pulled` | The new image couldn't be pulled for 60 seconds. Retry the build | | `The app started and then stopped` | The container exited or crashed. See the app output at the end of the logs | | `The app did not start in time` | Nothing accepted connections on the app's port within 5 minutes. Usually a port mismatch | | `The app could not be started` | The container couldn't be created, for example an invalid command or entrypoint | | `The source upload did not finish` | The upload was interrupted or never confirmed. Deploy again | | `The build did not finish in time` | The build made no progress for 45 minutes. Retry it | | `The app's registry couldn't be prepared, retry the build in a moment.` | A temporary registry problem. Retry | | **This app is already building, wait for that build to finish before deploying a new version!** | One build at a time per app | [Troubleshooting apps](/apps/troubleshooting) goes through each failure and its fixes. ### Next steps - [Logs](/apps/logs): the running app's output after it deploys. - [Troubleshooting](/apps/troubleshooting): fix build and start failures. - [Deploy from git](/apps/deploy-from-git): builds on every push. - [App builds API](/api/endpoints/app-builds): every build endpoint. --- ## Run a cron job as an app A **cron job** is an app that runs on a schedule instead of serving on a port. You create it from a git repository, a folder on your computer or a Docker image, exactly like any other app, and pick a schedule such as "every day at 03:00". On every run Cloider starts your app's image, runs it until it exits, and keeps its status and output. Between runs nothing is running. A cron job keeps everything an app has: builds and deploys, environment variables, build variables and build arguments, the deploy hook of an image app, logs, the web terminal, `cloider app exec`, insights, pause, resume and delete. | | Cron job | Web service | | --- | --- | --- | | **Runs** | On its schedule, until the command exits | All the time | | **Port, subdomain, domains** | None | One port, served on your domains | | **What runs** | The start command, or the image's own `ENTRYPOINT` and `CMD` | The same | | **New version** | A build or deploy, picked up by the next run | A build or deploy, rolled out at once | | **Logs** | Per run, for the last 5 succeeded and 5 failed runs | The running app's | | **Terminal** | A shell in a running run, from the **Runs** tab | A shell in the running app | ### How runs work - The schedule is a standard [cron expression](#schedule-syntax) and is always in **UTC**. - Each run is a fresh container of the version the app has deployed, with the app's [environment variables](/apps/environment-variables). A run that exits with code `0` has **Succeeded**, any other exit code is **Failed**. A failed run is not retried. - **One run at a time.** When a run is still going at the next scheduled time, that scheduled run is skipped. - A run is stopped after **12 hours**. - A run that could not start on time, for example during maintenance, is dropped after 5 minutes instead of running late. - The last **5 succeeded and 5 failed** runs are kept, with their output, in the **Runs** tab. What a run executes is the app's **start command**, when it has one: - **Built from source with a buildpack:** the start command is built into the image. Left empty, your project's own `Procfile` `web` entry or start script runs. - **Built from your own `Dockerfile`, or a [Docker image](/apps/deploy-an-image):** the start command runs with `sh -c` in place of the image's `ENTRYPOINT` and `CMD`. Left empty, those run. This is how one image runs several jobs, for example `python manage.py clearsessions` in your web app's image, or `pg_dump ...` in `postgres`. ### Create a cron job **Dashboard** 1. Open **Apps** in the sidebar and click **Create App**. 2. Pick your code: a git repository, a Docker image, or a folder to upload. 3. In **Configure your app**, set **Type** to **Cron job**. 4. Pick a **Schedule** from the list, or type your own cron expression. The port and subdomain fields go away, a cron job has neither. 5. Set the **Start Command** to what each run executes, for example `python manage.py clearsessions`. Leave it empty to use your project's own `Procfile` or start script, or the image's own `CMD`. 6. Add environment variables if the job needs any, and click **Create App**. The first build deploys the job, and it runs on its schedule from then on. Click **Run now** on the **Runs** tab to try it right away. **CLI** `--schedule` creates a cron job, from a directory with [`cloider deploy`](/cli/commands/deploy), or from a repository or an image with [`cloider app create`](/cli/commands/app#cloider-app-create): ```bash cloider deploy --name cleanup --schedule '0 3 * * *' --start-command 'python cleanup.py' cloider app create --git github --repo acme/jobs --schedule '*/30 * * * *' cloider app create --image ghcr.io/acme/web --tag v2 --schedule '@daily' --start-command 'python manage.py report' ``` ```text title="Output" Detected Python ✓ Created cron job cleanup, run on 0 3 * * * (UTC) ...build output... ✓ Deployed version 1 of cleanup Next run 2026-10-29 03:00, or start one with `cloider app run cleanup`. ``` Quote the schedule, the shell would otherwise expand its `*`. A cron job takes no `--port` or `--subdomain`. **API** Pass `schedule` when you [create the app](/api/endpoints/apps#step-5-create-the-app). `port` and `subdomain` are not used: ```bash curl -X POST https://cloider.com/api/apps \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "app_name": "cleanup", "schedule": "0 3 * * *", "start_command": "python cleanup.py", "source_token": "9f2c4e7a1b3d5f60718293a4b5c6d7e8" }' ``` **MCP** `deploy_app`, `create_app_from_git` and `create_app_from_image` take a `schedule`: ```text Deploy ~/code/jobs as a cron job named cleanup that runs python cleanup.py every day at 03:00 UTC. ``` ### Schedule syntax A schedule has five fields, separated by spaces, in **UTC**: ```text ┌───────────── minute (0 to 59) │ ┌─────────── hour (0 to 23) │ │ ┌───────── day of the month (1 to 31) │ │ │ ┌─────── month (1 to 12, or JAN to DEC) │ │ │ │ ┌───── day of the week (0 to 6, Sunday is 0, or SUN to SAT) │ │ │ │ │ * * * * * ``` Each field takes `*` (every value), a number, a list (`1,15`), a range (`1-5`), a step (`*/15`, `0-30/10`, or `5/20`, which is `5-59/20`), or the three letter names of months and weekdays. When both the day of the month and the day of the week are set, a day matching either one runs the job. | Schedule | Runs | | --- | --- | | `*/5 * * * *` | Every 5 minutes | | `0 * * * *` | At the start of every hour | | `30 2 * * *` | Every day at 02:30 UTC | | `0 9 * * MON-FRI` | Every weekday at 09:00 UTC | | `0 0 * * 0` | Every Sunday at midnight UTC | | `0 0 1 * *` | On the 1st of every month at midnight UTC | | `0 */6 * * *` | Every 6 hours | The shorthands `@hourly`, `@daily` (or `@midnight`), `@weekly`, `@monthly` and `@yearly` (or `@annually`) work too. A schedule that can never run, like `0 0 30 2 *` (February 30th), is refused. UTC doesn't follow daylight saving time: `0 7 * * *` runs at 09:00 in Berlin in summer and at 08:00 in winter. ### See runs and run a job now The **Runs** tab lists the runs Cloider keeps, newest first: their status, whether the schedule or **Run now** started them, when they started and how long they took. A failed run says why, for example `Exited with code 1` or `Stopped after running for 12 hours`, and a run stuck before it starts says `The image could not be pulled`. Click **Logs** on a run to read its output. **Run now** starts a run immediately, on the version the app has deployed, without changing the schedule. It is refused while a run is still going. **Dashboard** Open the app and the **Runs** tab. The tab also shows the schedule and the next run in your local time. **CLI** ```bash cloider app runs cleanup ``` ```text title="Output" NAME STATUS TRIGGER STARTED DURATION MESSAGE app-manual-1793185200 Succeeded manual 2026-10-28 11:00 14s - app-29885940 Failed schedule 2026-10-28 03:00 3s Exited with code 1 app-29884500 Succeeded schedule 2026-10-27 03:00 12s - ``` ```bash cloider app run cleanup # start a run, print its name cloider app run cleanup -f # start a run, print its output, fail when the run fails ``` `cloider app run -f` exits with an error when the run fails, so a CI pipeline can run a job and check it worked. **API** ```bash curl https://cloider.com/api/apps/k3v9q/runs -H "Authorization: Bearer $CLOIDER_TOKEN" curl -X POST https://cloider.com/api/apps/k3v9q/runs -H "Authorization: Bearer $CLOIDER_TOKEN" ``` See [List a cron job's runs](/api/endpoints/apps#list-a-cron-jobs-runs) and [Run a cron job now](/api/endpoints/apps#run-a-cron-job-now). **MCP** `list_app_runs` lists the runs and `run_app` starts one, with `follow` to wait for its output. ### Logs A cron job has no **Logs** tab: click **Logs** on a run in the **Runs** tab to read its output, followed live while the run is going. ```bash cloider app logs cleanup # the latest run's output cloider app logs cleanup -f # follow every run as it starts cloider app logs cleanup --run app-29885940 # one run's output ``` Only the runs the **Runs** tab lists still have their output. See [Logs](/apps/logs). ### Debug with the terminal While a run is going, click **Terminal** on it in the **Runs** tab to open a shell inside that run's container, with its files and processes. The session ends when the run finishes. Between runs, `cloider app shell` and `cloider app exec` open a container with the cron job's image and environment variables, started for them and stopped again once it has been unused for 10 minutes. It is the quickest way to find out why a run fails: run the job's command by hand and watch what it does. ```bash cloider app shell cleanup cloider app exec cleanup -- python cleanup.py --dry-run ``` The terminal needs a shell in the image: an image without `sh`, like a distroless image, has nothing to open. See [Terminal and exec](/apps/terminal-and-exec). ### Change the schedule **Dashboard** Open the app's **Settings** tab, pick or type the new **Schedule**, and click **Save changes**. The job is redeployed on the new schedule in a few seconds, on the same version. **CLI** ```bash cloider app update cleanup --schedule '0 4 * * *' ``` **API** ```bash curl -X PUT https://cloider.com/api/apps/k3v9q/settings \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"schedule": "0 4 * * *", "start_command": "python cleanup.py"}' ``` **MCP** `update_app_settings` with `schedule`. A cron job stays a cron job: it can't be turned into a web service, or the other way around. Create a new app for that. ### Pause, resume and delete Pausing a cron job stops its schedule, and stops a run that is going at that moment. Its runs list, version and environment stay. Resuming it puts the schedule back, and the next run is the schedule's next time: runs missed while paused don't run. See [Pause, resume and delete](/apps/pause-resume-and-delete). ### Billing A cron job is billed at the same rates as any app, for the CPU and memory its runs use, and each run is measured on its own: - **For exactly as long as it ran**, from the second its container started to the second it finished. Time spent waiting to start, such as pulling the image, isn't billed, and neither is anything between runs. - **For the CPU it used**, as its container counted it, read every 5 seconds while it runs. - **For the memory it was using**, averaged over those same readings. A run that is over in a few seconds can end before its first reading. It is billed at what the job's last measured run used per second, and costs nothing while the job has no measured run yet. Only runs are billed. The container `cloider app shell` and `cloider app exec` open between runs is free. See [pricing](https://cloider.com/#pricing) and [Insights](/apps/insights). ### Troubleshooting **A run shows `The image could not be pulled`.** The run is stuck before it starts. For an image app, check the image still exists and the registry login on the **Settings** tab still works. **A scheduled run didn't happen.** The previous run was still going (one run at a time), the app was paused, or the schedule is in UTC and you read it in your local time. The **Runs** tab and the **Overview** show the next run in your local time. **The terminal says the app isn't running.** The image has no `sh`, or its shell container is still starting. Wait a few seconds and reconnect. **A run's terminal says the run isn't running any more.** The run finished. Read its output with **Logs** on the **Runs** tab. --- ## Deploy a Docker image as an app A Cloider app can run a Docker image you already built, instead of building one from your source. You give it an image and a tag, plus a login when the image is private, and Cloider runs it with a free subdomain, environment variables, logs and everything else an app has. A new version is a push to the tag followed by a deploy, from the dashboard, the CLI, the API, or a **deploy hook** URL that your CI calls. ### How image deploys work An image app never builds anything. Each deploy does the same three things: 1. Cloider asks the image's registry which image the tag points at right now, logged in when the app has a login. 2. It records that exact image, by its digest (`sha256:...`), as the new version in the **Builds** tab. 3. It runs that image, pulling it again on every start, and waits until it is listening on the app's port. Because every version is pinned to a digest, pushing a new image to the same tag (for example `latest`) and deploying is enough to ship it, and every row of the **Builds** tab says exactly which image it ran. The app's **Overview** tab shows the image and tag it follows, the login it is pulled with, and the exact image running. | | Image app | App built from source | | --- | --- | --- | | **What you give Cloider** | An image and a tag | A folder, a zip or a git repository | | **Who builds the image** | You, anywhere | Cloider, with your `Dockerfile` or a buildpack | | **A new version** | Push to the tag, then deploy | Upload again, or push to the branch | | **Start command** | Optional, runs with `sh -c` in place of the image's `ENTRYPOINT` and `CMD` | Available | | **Build variables, build arguments** | Not used, nothing is built | Available | | **Deploy hook URL** | Yes | No (git apps deploy on push) | ### Before you begin - A Cloider account. An app's compute is billed like any other app, see [pricing](https://cloider.com/#pricing). - An image on a registry Cloider can reach over the internet: Docker Hub, GitHub Container Registry (`ghcr.io`), GitLab (`registry.gitlab.com`), your [Cloider registry](/container-registry/overview) (`registry.cloider.com`) or any other public registry address. - The image has a `linux/amd64` build. Apps run on `linux/amd64`, so an image built only for Apple Silicon (`arm64`) is refused. Build for both with `docker buildx build --platform linux/amd64,linux/arm64`, or for amd64 alone with `--platform linux/amd64`. - The image listens on one port for HTTP. Cloider fills the port in from the image's `EXPOSE` when it exposes exactly one, and also passes it to the container as the `PORT` variable. ### Deploy a public image **Dashboard** 1. Open **Apps** in the sidebar and click **Create App**. 2. Under **Deploy a Docker image**, click **Docker image**. 3. Enter the **Image**, for example `nginx`, and the **Tag**, for example `1.27`. Pasting `nginx:1.27` into **Image** moves the tag into its own field. An empty tag means `latest`. 4. Click **Continue**. Cloider looks the image up and opens the next step with the app name and port filled in. 5. Check the **Port**, pick a subdomain, add environment variables if the image needs any, and click **Create App**. A **Start Command** is optional: it runs with `sh -c` in place of the image's `ENTRYPOINT` and `CMD`. There are no build variables or arguments: the image runs as it was built. **CLI** ```bash cloider app create --image nginx:1.27 --port 80 ``` The tag is the one written into `--image`. An image without one takes `--tag`, and `latest` when that isn't given either, so these two are the same, and the third deploys `nginx:latest`: ```bash cloider app create --image nginx:1.27 --port 80 cloider app create --image nginx --tag 1.27 --port 80 cloider app create --image nginx --port 80 ``` Giving a tag both ways (`--image nginx:1.27 --tag 1.28`) is an error. The CLI looks the image up, creates the app with a free subdomain, follows the first deploy and prints the app's URL. `--name`, `--subdomain`, `--no-subdomain`, `--env`, `--secret-env` and `--detach` work as they do for [`cloider deploy`](/apps/deploy-from-your-computer). **Terraform** The Terraform provider doesn't manage apps. Create the app with the dashboard, the CLI or the API. **API** Stage the image, then create the app on the returned token: ```bash curl -X POST https://cloider.com/api/apps/source/image \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"image": "nginx", "tag": "1.27"}' ``` ```json title="Response" { "token": "4b8e1d0c7a2f9e6b3d5c8a1f0e7d2b9c", "status": "Done", "app_name": "nginx", "port": 80, "image": {"image": "nginx", "tag": "1.27", "digest": "sha256:6784fb0834aa...", "username": ""} } ``` ```bash curl -X POST https://cloider.com/api/apps \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"app_name": "web", "port": 80, "subdomain": "web-7k2p.cloider.app", "source_token": "4b8e1d0c7a2f9e6b3d5c8a1f0e7d2b9c"}' ``` See [Create an app from an image](/api/endpoints/apps#create-an-app-from-an-image). **MCP** Ask your agent: ```text title="Prompt" Deploy the nginx:1.27 image as an app called web on port 80. ``` It calls the `create_app_from_image` tool: ```json title="Tool call: create_app_from_image" {"image": "nginx", "tag": "1.27", "name": "web", "port": 80} ``` ### Deploy a private image A private image needs a **username** and a **token** (or password) that can pull it. Use a token that can only pull, so the app's login can't push or delete anything: | Registry | Image looks like | Username | Token | | --- | --- | --- | --- | | Docker Hub | `acme/api` | Your Docker Hub username | A personal access token with **Read-only** access | | GitHub Container Registry | `ghcr.io/acme/api` | Your GitHub username | A personal access token (classic) with `read:packages` | | GitLab | `registry.gitlab.com/acme/api` | The deploy token's username | A deploy token with `read_registry` | | Cloider registry | `registry.cloider.com/acme/api` | Your registry username | A [registry token](/container-registry/tokens) with pull access | **Dashboard** In the **Deploy a Docker image** dialog, turn on **The image is private** and fill in **Username** and **Token or password** before clicking **Continue**. A login the registry refuses is reported right there. **CLI** The token is read from stdin with `--registry-password-stdin`, or asked for when you run the command in a terminal: ```bash echo "$GHCR_TOKEN" | cloider app create --image ghcr.io/acme/admin:v3 \ --registry-username acme-ci --registry-password-stdin ``` **Terraform** The Terraform provider doesn't manage apps. Create the app with the dashboard, the CLI or the API. **API** ```bash curl -X POST https://cloider.com/api/apps/source/image \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"image": "ghcr.io/acme/admin", "tag": "v3", "username": "acme-ci", "password": "'"$GHCR_TOKEN"'"}' ``` **MCP** ```json title="Tool call: create_app_from_image" {"image": "ghcr.io/acme/admin", "tag": "v3", "name": "admin", "registry_username": "acme-ci", "registry_password": "ghp_xxxxxxxxxxxxxxxx"} ``` The token is only used to pull this app's image. You can change or remove the login later in **Settings**. ### Deploy a new version Push the new image to the app's tag, then deploy. A deploy pulls the tag again and runs the image it points at now. To switch to another tag, give it when you deploy: the app follows that tag from then on. **Dashboard** Open the app, go to **Builds** and click **Deploy**. The dialog has the app's tag filled in: click **Deploy** to pull it again, or type another tag first. **CLI** ```bash cloider deploy --app admin # pull the app's tag again cloider deploy --app admin --tag v2 # deploy another tag, which the app then follows ``` **Terraform** The Terraform provider doesn't manage apps. Deploy with the dashboard, the CLI, the API or the deploy hook. **API** ```bash curl -X POST https://cloider.com/api/apps/d6h3t/builds/image \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"tag": "v2"}' ``` Leave `tag` empty to pull the app's own tag again. See [Deploy a tag of an image app](/api/endpoints/app-builds#deploy-a-tag-of-an-image-app). **MCP** ```json title="Tool call: deploy_app" {"app": "admin", "tag": "v2"} ``` When the tag still points at the image the app is running, nothing is deployed and you see `The app is already running the image ghcr.io/acme/admin:v2 points at. Push a new image to the tag, or deploy another tag.` ### Deploy from CI with a deploy hook A **deploy hook** is a URL that deploys the app when it receives a `POST`. It needs no API token, so it is the simplest way for a CI pipeline, or a registry's own push webhook, to ship a new version once the image is pushed. 1. Open the app, go to **Settings** and click **Show deploy hook** under **Deploy hook**. With the CLI, run `cloider app deploy-hook admin`. 2. Store the URL as a secret in your CI, for example `CLOIDER_DEPLOY_HOOK`. 3. Call it after the push: ```bash curl -X POST "$CLOIDER_DEPLOY_HOOK" # pull the app's tag again curl -X POST "$CLOIDER_DEPLOY_HOOK?tag=v2" # deploy another tag, which the app then follows ``` The tag can also be sent as a JSON body, `{"tag": "v2"}`. A body without a `tag`, such as a registry webhook's payload, deploys the app's own tag. ```yaml title=".github/workflows/deploy.yml" name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - uses: docker/build-push-action@v6 with: push: true platforms: linux/amd64 tags: ghcr.io/acme/admin:${{ github.sha }} - run: curl --fail -X POST "${{ secrets.CLOIDER_DEPLOY_HOOK }}?tag=${{ github.sha }}" ``` | Response | Meaning | | --- | --- | | `202` with `{"deployed": true, "build_hashed_id": ..., "version": ..., "image_ref": ...}` | A new version is deploying | | `200` with `{"deployed": false}` | The tag still points at the running image, nothing to deploy | | `404` | The URL is wrong, or it was regenerated | | `409` | The app is already deploying, is paused, or the account is suspended. Call again later | | `422` | The image or tag can't be read, for example `ghcr.io/acme/admin has no tag v4.` | Anyone with the URL can deploy the app, so keep it secret. Click **Regenerate** (or run `cloider app deploy-hook admin --rotate`) to replace it: the old URL stops working at once. ### Change the image or its login **Settings** of an image app has an **Image** section with the image, the tag and the login. Saving checks them with the registry first, then deploys when the tag now points at a different image. Leave **Token or password** empty to keep the saved one for the same username, and turn off **The image is private** to pull without a login. ```bash cloider app image admin --tag v4 cloider app image admin --image ghcr.io/acme/admin-next # no tag given: latest echo "$NEW_TOKEN" | cloider app image admin --registry-username acme-ci --registry-password-stdin cloider app image admin --no-login ``` A new `--image` follows the same rule as `cloider app create`: the tag written into it, else `--tag`, else `latest`. The **Port** is changed in the same tab, and saving it redeploys the running image on the new port. ### What an image app doesn't have - **Build variables** and **build arguments**: nothing is built. The API refuses them for an image app. A start command does apply, see [Change the start command](/apps/settings#change-the-start-command). - **Upload a new version**: an image app is deployed by tag. - **Terminal and exec** need a shell in the image (`/bin/sh`). A distroless or `scratch` image has none, so the [terminal](/apps/terminal-and-exec) can't open in it. ### Troubleshooting | Message | What to do | | --- | --- | | `nginx does not exist or is private. If it is private, add a login for its registry.` | Check the name, or turn on **The image is private** and add a login | | `The registry refused this login, check the username and the token, and that it can pull ghcr.io/acme/admin.` | The token is wrong, expired, or lacks pull access to that image | | `ghcr.io/acme/admin has no tag v4.` | Push the tag first, or pick one that exists | | `This image has no linux/amd64 build, which is what apps run on. Build it with --platform linux/amd64.` | Rebuild with `docker buildx build --platform linux/amd64` | | `This registry is not on a public address, apps can only pull from public registries.` | The registry's name resolves to a private address. Use a registry reachable over the internet | | `The registry is limiting how often its images can be read, try again in a few minutes.` | Docker Hub limits anonymous pulls. Add a Docker Hub login, even for a public image | | The build ends with `The app image could not be pulled` | The login stopped working after the deploy was queued, or the image was deleted. Fix the login in **Settings** and click **Retry** | | The build ends with `The app did not start in time` | Nothing listened on the app's port. Check the port against what the image listens on, and the app's **Logs** | ### Next steps - [Environment variables](/apps/environment-variables) for the image's configuration. - [Domains](/apps/domains) to serve the app on your own domain. - [Builds and deployments](/apps/builds-and-deployments) for versions, retries and rollbacks. - [Container registry](/container-registry/overview) to host your images on Cloider. --- ## Deploy an app from GitHub, GitLab or Bitbucket A Cloider app can deploy straight from a **GitHub**, **GitLab** or **Bitbucket** repository: you connect your git account once, pick a repository and a branch, and Cloider builds the latest commit. With **auto deploy** on, every push to that branch builds and deploys the next version, with no CI pipeline to write. This page covers connecting each provider, creating the app, auto deploy, manual deploys, changing the branch, and what to do when something goes wrong. ### How git deploys work A git app remembers its repository, its branch and the connection it reads them through. Each deploy, whether a push or a click, does the same thing: 1. Cloider asks the provider for the newest commit on the app's branch. 2. It downloads the repository as an archive at exactly that commit, so a push that lands in the meantime can't change what is built. 3. It builds and deploys that archive like any other upload. The build records the commit SHA and the first line of its message, so the **Builds** tab says which commit each version is. ```mermaid sequenceDiagram participant You participant Git as GitHub / GitLab / Bitbucket participant Cloider You->>Git: git push origin main Git->>Cloider: push webhook Cloider->>Git: newest commit on main? Cloider->>Git: download archive at that commit Cloider->>Cloider: build, deploy, wait until running ``` | | GitHub | GitLab | Bitbucket | | --- | --- | --- | --- | | **Where** | github.com | gitlab.com | bitbucket.org | | **How you connect** | Install the Cloider GitHub App on your user or organization and choose its repositories | Authorize the Cloider application on GitLab | Authorize the Cloider consumer on Bitbucket | | **What you can import** | The repositories the installation can see | Projects you are at least a **Reporter** on | Repositories you can read in your workspaces | | **How pushes arrive** | The GitHub App's webhook, nothing to set up | A project webhook Cloider adds when the app is created | A repository webhook Cloider adds when the app is created | | **Needed for auto deploy** | Nothing more | The **Maintainer** role on the project | Admin access to the repository | | **Repository is matched by** | Repository id | Project id | Repository uuid | Self-managed GitLab and Bitbucket Data Center aren't supported. ### Before you begin - A Cloider account with a role that can create resources. See [Accounts and roles](/concepts/accounts-and-roles). - Your code in a GitHub, GitLab or Bitbucket repository, with at least one commit on the branch you want to deploy. - A project Cloider can build: a `Dockerfile` at the repository root, a project file a buildpack recognizes, or a static site. See [Build configuration](/apps/build-configuration). - For the CLI, [install it and log in](/cli/install). ### Step 1: Connect your git provider Connecting happens in the browser, logged in to the dashboard, because the provider sends you back to the dashboard when you are done. You only do it once per git account. **Dashboard** 1. Open **Apps** in the sidebar and click **Create App**. 2. Under **Import a Git repository**, click **GitHub**, **GitLab** or **Bitbucket**. A provider you haven't connected yet sends you to its authorization page. 3. Approve the request: - **GitHub**: authorize the app. If none of your accounts has it installed yet, GitHub then asks where to install it and which repositories it may see. Pick **All repositories** or only the ones Cloider should deploy. - **GitLab** and **Bitbucket**: approve Cloider's access request. 4. You land back on **Create App** with the import dialog open. To add another GitHub organization or GitLab or Bitbucket user later, open the import dialog and click **Add account**. **CLI** ```bash cloider git connect github ``` ```text title="Output" Opening the browser to connect GitHub. If it doesn't open, visit: https://cloider.com/api/git/github/connect?return=%2Fdashboard%2Fapps%2Fnew-app Once connected, `cloider git repos github` lists what apps can deploy from. ``` Finish in the browser, then check the connection: ```bash cloider git installations github ``` ```text title="Output" ACCOUNT TYPE STATUS CONNECTED acme Organization Active 2026-10-20 09:02 ``` Use `gitlab` or `bitbucket` in place of `github` for the other providers. **Terraform** The Terraform provider can't connect git accounts or create apps. Connect the provider in the dashboard or with `cloider git connect`. **API** Connecting needs a browser session, so an API token can't start it: open `https://cloider.com/api/git/github/connect` (or `gitlab`, `bitbucket`) in a browser where you are logged in to the dashboard. Once connected, list the connections with a token: ```bash curl https://cloider.com/api/git/github/installations \ -H "Authorization: Bearer $CLOIDER_TOKEN" ``` ```json title="Response" { "configured": true, "installations": [ { "hashed_id": "g8x2kd", "provider": "GitHub", "account_login": "acme", "account_type": "Organization", "avatar_url": "https://avatars.githubusercontent.com/u/1234567", "suspended_at": null, "created_at": "2026-10-20T09:02:11Z" } ] } ``` **MCP** Ask your agent: ```text title="Prompt" Connect my GitHub account to Cloider. ``` It calls the `connect_git_provider` tool, which opens the browser on your machine for you to finish: ```json title="Tool call: connect_git_provider" {"provider": "github"} ``` :::info[Installing on a GitHub organization you don't own] When you install the GitHub App on an organization where you aren't an owner, GitHub sends an approval request to the owners instead. The dashboard shows **Waiting for approval**. Once an owner approves it, open **Create App** again and the organization is there. ::: ### Step 2: Create the app from a repository **Dashboard** 1. On **Create App**, click your provider under **Import a Git repository**. 2. In the dialog, pick the **Account** (the GitHub user or organization, or the GitLab or Bitbucket user), then the **Repository**. Type in **Search repositories** to filter a long list. 3. Pick the **Branch**. It defaults to the repository's default branch, and you can type a branch that isn't listed. 4. Click **Import**. Cloider fetches the repository (**Fetching your repository...**) and reads it (**Analyzing your code...**). 5. On **Configure your app**, check the **Deploying from** box (repository, branch and commit) and what was detected, then fill in **App Name**, **Port**, the optional **Start Command** and the **Subdomain**. Add environment variables, build variables or build arguments if the app needs them on its first build. 6. Click **Create App**. **CLI** ```bash cloider app create --git github --repo acme/api ``` ```text title="Output" Detected Go ✓ Created app api on port 8080 Downloading source for app k3m9tw... No Dockerfile in the source, looking for a buildpack... [buildpack output] App k3m9tw built and pushed as registry.cloider.com/acme-apps/api-k3m9tw:1 Deploying the image... Waiting for the app to start... The app is up, watching it for 15s... The app is running. ✓ Deployed version 1 of api https://api-q2v8.cloider.app/ ``` Useful flags: `--branch` (default: the repository's default branch), `--installation` when several accounts of the provider are connected, `--name`, `--port`, `--start-command`, `--subdomain` or `--no-subdomain`, `--env` and `--secret-env`, `--build-arg` and `--secret-build-arg`, `--build-env` and `--secret-build-env`, and `--detach` to return once the build starts. ```bash cloider app create --git gitlab --repo acme/web --branch main --name web --port 3000 ``` To see what you can import first: ```bash cloider git repos github cloider git branches github acme/api ``` **Terraform** The Terraform provider has no app resource. Create the app with the dashboard, the CLI or the API, then route a domain to it with [`cloider_target`](/terraform/resources/target) and its `app_id`. **API** Creating a git app is three calls. First, stage the repository. GitHub and Bitbucket take `repo_full_name`, GitLab takes the numeric `project_id` from the repository list: ```bash curl -X POST https://cloider.com/api/apps/source/github \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"installation_hashed_id": "g8x2kd", "repo_full_name": "acme/api", "branch": "main"}' ``` ```json title="Response" {"token": "Wq3...staging-token"} ``` Then poll the staged source until `status` is `Done`, `Failed` or `Unavailable`. It moves through `Fetching` and `Detecting` first: ```bash curl https://cloider.com/api/apps/source/$TOKEN \ -H "Authorization: Bearer $CLOIDER_TOKEN" ``` ```json title="Response" { "status": "Done", "app_name": "api", "language": "Go", "framework": "", "port": 8080, "message": "", "build_args": [], "git": { "provider": "GitHub", "repo_full_name": "acme/api", "repo_url": "https://github.com/acme/api", "branch": "main", "commit_sha": "9f2c1e7b4a0d8c6e5f3a2b1c0d9e8f7a6b5c4d3e", "commit_message": "Add health endpoint" } } ``` Finally, create the app on the staged source: ```bash curl -X POST https://cloider.com/api/apps \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "app_name": "api", "port": 8080, "source_token": "'"$TOKEN"'", "subdomain": "api-q2v8.cloider.app", "env_vars": [{"key": "LOG_LEVEL", "value": "info", "secret": false}] }' ``` ```json title="Response" {"hashed_id": "k3m9tw", "build_hashed_id": "b7n4xp", "version": "1"} ``` The [Git API reference](/api/endpoints/git) lists the repository and branch endpoints. **MCP** Ask your agent: ```text title="Prompt" Create a Cloider app from the GitHub repository acme/api on the main branch and tell me when it is live. ``` It calls the `create_app_from_git` tool, which follows the first build until the app is up: ```json title="Tool call: create_app_from_git" {"provider": "github", "repo": "acme/api", "branch": "main"} ``` A git app is created with **auto deploy on**. The first build starts immediately on the commit that was fetched. #### What is imported from the repository? Cloider downloads the whole repository at one commit, without its git history, and removes `.git`, `__MACOSX` and `.DS_Store` entries. The archive must be 100 MB or smaller, and 500 MB or smaller once extracted. The build runs at the repository root: a `Dockerfile` must be at the root to be used, and buildpacks look for project files such as `package.json` or `go.mod` at the root. See [Monorepos and subfolders](#can-i-deploy-a-subfolder-of-a-monorepo). ### Auto deploy on push With auto deploy on, a push to the app's branch starts a build of the newest commit on that branch. You see it appear on the **Builds** tab with the commit SHA and message as its source. Cloider ignores a push when: - It is to another branch, or it pushes a tag. - It deletes the branch. - The app is paused or being deleted. Resume the app and click **Deploy latest commit** to catch up. - A build of the app is already running. The push is skipped, not queued: push again, or click **Deploy latest commit** once the running build finishes. - The account is suspended. - The same delivery arrives twice. Providers retry deliveries, and each one builds at most once. If two of your apps deploy from the same repository and branch, a push builds both. #### Auto deploy on GitLab and Bitbucket GitHub delivers pushes through the Cloider GitHub App itself. GitLab and Bitbucket don't have that, so Cloider adds a **webhook** to the project or repository when the app is created, and removes it when the app is deleted or the connection is removed. Creating that webhook needs the **Maintainer** role on a GitLab project, or admin access to a Bitbucket repository. If the user who connected the provider doesn't have it, the webhook can't be created and Cloider turns auto deploy off for that app. The app still works: deploy with **Deploy latest commit** or `cloider deploy --git`, or ask someone with the right role to connect their account and turn auto deploy back on, which tries to create the webhook again. ### Deploy the latest commit manually You can build the newest commit of the branch at any time, for example after a skipped push or with auto deploy off. **Dashboard** 1. Open the app and go to the **Builds** tab. 2. Click **Deploy latest commit**. A git app has no **Deploy new version** upload button: an upload would be a version the repository knows nothing about, and the next push would replace it. **CLI** ```bash cloider deploy --git --app api ``` ```text title="Output" ✓ Building version 4 of api from acme/api@main ... ✓ Deployed version 4 of api https://api-q2v8.cloider.app/ ``` In a directory linked to the app (see [`.cloider/app.json`](/apps/deploy-from-your-computer#how-does-a-directory-stay-linked-to-an-app)), `cloider deploy --git` is enough. **Terraform** The Terraform provider can't deploy apps. Use the dashboard, the CLI or the API. **API** ```bash curl -X POST https://cloider.com/api/apps/k3m9tw/builds/git \ -H "Authorization: Bearer $CLOIDER_TOKEN" ``` ```json title="Response" { "hashed_id": "c5r8ws", "version": "4", "attempt": 1, "status": "Pending", "source_filename": "api.zip", "source_size": 0, "uploaded": false, "commit_sha": "", "commit_message": "", "image_ref": "", "message": "", "started_at": null, "finished_at": null, "created_at": "2026-10-20T10:31:02Z" } ``` The build answers at once and fills in the commit as it fetches it. Poll `GET /api/apps/k3m9tw/builds` to follow it. **MCP** Ask your agent: ```text title="Prompt" Deploy the latest commit of the api app. ``` ```json title="Tool call: deploy_app" {"app": "api", "git": true} ``` ### Change the branch or turn auto deploy off The branch and auto deploy are on the app's **Settings** tab. Changing them doesn't build anything by itself: the next push or the next **Deploy latest commit** uses them. A branch that doesn't exist isn't checked when you save, it shows up as a failed build on the next deploy. **Dashboard** 1. Open the app and go to the **Settings** tab. 2. Under **Repository**, change **Branch**, or switch **Auto deploy on push** on or off. 3. Click **Save changes**. **CLI** ```bash cloider app git api --branch release --auto-deploy=false ``` ```text title="Output" ✓ Saved, api deploys from release (auto deploy no) ``` **Terraform** The Terraform provider can't change apps. Use the dashboard, the CLI or the API. **API** Send both fields, the branch is required: ```bash curl -X PUT https://cloider.com/api/apps/k3m9tw/git \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"branch": "release", "auto_deploy": false}' ``` The response is an empty `200`. **MCP** Ask your agent: ```text title="Prompt" Make the api app deploy from the release branch and stop deploying on every push. ``` ```json title="Tool call: update_app_git" {"app": "api", "branch": "release", "auto_deploy": false} ``` ### Manage your git connections #### What happens when I disconnect a git account? Disconnecting removes the connection on Cloider's side and turns auto deploy off for every app that deploys through it. The apps keep running and remember their repository and branch; their page shows the repository as **disconnected** and they can't deploy from git until you connect the account again. For GitLab, Cloider also revokes its access token and removes the webhooks it created. For Bitbucket, it removes its webhooks; revoke the grant itself in your Bitbucket account settings. For GitHub, uninstall the Cloider app on GitHub to remove its access. ```bash cloider git disconnect github acme ``` Connecting the same git account again creates a new connection. Apps that deployed through the removed one keep showing it as **disconnected** and can't deploy from git any more, so create a new app from the repository, or deploy the old one from your computer with `cloider deploy --app`. To pause deploys without losing the connection, turn **Auto deploy on push** off instead. #### Changes you make on the provider | You do this on the provider | Cloider does this | | --- | --- | | Rename or transfer the repository | Keeps deploying it. Repositories are matched by their id, and the new name is picked up from the next push | | Remove the repository from the GitHub App's access | Turns auto deploy off for apps deploying from it | | Suspend the GitHub App installation | Turns auto deploy off for its apps and marks the connection suspended | | Unsuspend it | Clears the suspension. Auto deploy stays off until you turn it on | | Uninstall the GitHub App | Removes the connection, as if you disconnected it | | Revoke Cloider's access on GitLab or Bitbucket | Removes the connection the next time Cloider tries to use it | | Delete the branch | The next deploy fails with a message naming the branch | ### Can I deploy a subfolder of a monorepo? Not directly: Cloider always builds the repository root, and there is no setting for a subdirectory. Two ways around it: - Put a `Dockerfile` at the repository root that copies and builds the subfolder you want: ```dockerfile title="Dockerfile" FROM node:22-alpine WORKDIR /app COPY services/web/package*.json ./ RUN npm ci COPY services/web/ ./ RUN npm run build CMD ["npm", "start"] ``` - Or deploy the subfolder from your computer or CI with `cloider deploy services/web`. See [Deploy from your computer](/apps/deploy-from-your-computer). ### Verify - The **Builds** tab lists a build whose **Source** is the commit you pushed, and it reaches **Ready**. - The **Overview** tab shows the repository, branch and commit under **Deployed source**. - `cloider app builds api` lists the build with the short SHA and commit message. ### Troubleshooting | Message | Cause and fix | | --- | --- | | **No repositories to show.** | GitHub: the installation can only see the repositories you granted it; change them on GitHub. GitLab: you need at least the Reporter role on the project. Bitbucket: you need read access in the workspace | | **Only the first repositories are listed.** | The installation sees more than the list can load. Narrow the installation down on GitHub, or use `cloider app create --repo` with the exact name | | **This installation is suspended on GitHub. Re-enable it there before importing from it.** | Unsuspend the Cloider app in the GitHub account or organization settings | | `We could not read acme/api at main. The branch may be gone, or the repository may have no commits yet.` | Push a commit to the branch, or pick an existing branch | | `acme/api is over the 100MB limit.` | The repository archive is too big. Remove large files from the repository, or deploy a smaller folder with `cloider deploy` | | `GitHub no longer gives us acme/api at main` (on a build) | The branch was deleted or renamed, or the repository was removed from the installation. Change the branch on **Settings**, or grant access again | | `The GitLab connection was revoked, reconnect GitLab` | Cloider's access was revoked on GitLab, so the connection was removed. See [What happens when I disconnect a git account?](#what-happens-when-i-disconnect-a-git-account) | | **This app's GitHub connection is gone. Reconnect GitHub and try again.** | The connection is suspended or was removed. Unsuspend a suspended GitHub installation on GitHub. A removed connection can't be restored for an existing app, see [What happens when I disconnect a git account?](#what-happens-when-i-disconnect-a-git-account) | | **This app is already building, wait for that build to finish!** | Only one build runs per app. Wait, then deploy again | | **This app is paused. Resume it before deploying a new version!** | Resume the app first, see [Pause, resume and delete](/apps/pause-resume-and-delete) | | Pushes don't deploy a GitLab or Bitbucket app | Auto deploy switched itself off because the webhook couldn't be created. Check **Auto deploy on push** on **Settings**; see [Auto deploy on GitLab and Bitbucket](#auto-deploy-on-gitlab-and-bitbucket) | | **GitHub importing is not set up on this environment yet.** | The provider isn't available. Deploy from your computer instead | For build failures after the source is fetched, see [Troubleshooting apps](/apps/troubleshooting). ### Next steps - [Build configuration](/apps/build-configuration): how Cloider decides how to build your repository. - [Environment variables](/apps/environment-variables): configure the app and its build. - [Builds and deployments](/apps/builds-and-deployments): follow, read and retry builds. - [CI/CD with GitHub Actions](/guides/ci-cd-with-github-actions): deploy from your own pipeline instead. --- ## Deploy an app from your computer with cloider deploy You don't need a git provider to run an app on Cloider: `cloider deploy` packages the folder you are in, uploads it, and builds and deploys it, and the dashboard takes a folder, a `.zip` file or a single HTML file by drag and drop. This page covers the first deploy, later versions, what is uploaded and what is left out, how a folder stays linked to its app, deploying from CI, and the REST API calls underneath. ### Before you begin - A Cloider account with a role that can create resources. See [Accounts and roles](/concepts/accounts-and-roles). - For the CLI: [install `cloider` and log in](/cli/install). In CI, set `CLOIDER_TOKEN` to an [API token](/account/api-tokens) with read and write access. - A project Cloider can build: a `Dockerfile` at its root, project files a buildpack recognizes (such as `package.json`, `requirements.txt` or `go.mod`), or plain HTML. See [Supported languages](/apps/supported-languages). ### Deploy a folder with the CLI Run `cloider deploy` in the project's folder, or pass the folder as an argument. The first time, it creates the app: ```bash cd blog cloider deploy ``` ```text title="Output" Packaged 42 files (18.3 KB) Detected JavaScript Express ✓ Created app blog on port 3000 Linked . to blog in .cloider/app.json, later deploys from here update it. Downloading source for app a7c2mq... No Dockerfile in the source, looking for a buildpack... [buildpack output] App a7c2mq built and pushed as registry.cloider.com/acme-apps/blog-a7c2mq:1 Deploying the image... Waiting for the app to start... The app is up, watching it for 15s... The app is running. ✓ Deployed version 1 of blog https://blog-x7k2.cloider.app/ ``` What happened, step by step: 1. **Packaging.** The folder is zipped, leaving out what `.gitignore` and `.cloiderignore` list (see [What is uploaded?](#what-is-uploaded)). 2. **Upload and detection.** The archive is uploaded and Cloider reads it to detect the language, framework, port and a name. When detection recognizes nothing, the CLI says so and creates the app with its defaults. If detection hasn't answered after 3 minutes, the CLI stops with `detection took too long, try again`. 3. **Create.** The app is created with: - the name from `--name`, else the one detection found (for example the `name` in `package.json`), else the folder name made valid; - the port from `--port`, else the detected port, else `8080`; - a free subdomain `-<4 random characters>.cloider.app`, unless you pass `--subdomain` or `--no-subdomain`. 4. **Link.** The folder is linked to the app in `.cloider/app.json`. 5. **Build and follow.** The build log streams to your terminal until the new version is running, then the app's URL is printed. Build output goes to **stderr** and the URL to **stdout**, so a script can capture the address alone: ```bash URL=$(cloider deploy) curl -fsS "$URL" ``` #### Options for a new app These flags only apply when `cloider deploy` creates the app. On a folder that is already linked they are ignored. | Flag | What it sets | | --- | --- | | `--name blog` | The app name: 2 to 64 lowercase letters, digits and `-`, starting with a letter | | `--port 3000` | The port the app listens on | | `--start-command "node server.js"` | The command a buildpack build starts the app with | | `--subdomain blog-acme` | The free subdomain, a label (`blog-acme`) or a full name (`blog-acme.cloider.app`) | | `--no-subdomain` | Don't claim a free subdomain. Route a domain to the app later with a [target](/apps/domains) | | `-e`, `--env KEY=VALUE` | A runtime environment variable (repeatable) | | `--secret-env KEY=VALUE` | A secret runtime environment variable (repeatable) | | `--env-file .env.production` | Runtime variables from a `.env` file | | `--build-arg KEY=VALUE` | A Dockerfile build argument, for `ARG KEY` (repeatable) | | `--secret-build-arg KEY=VALUE` | A Dockerfile build secret, for `RUN --mount=type=secret,id=KEY` (repeatable) | | `--build-arg-file FILE` | Build arguments from a `.env` file | | `--build-env KEY=VALUE` | A buildpack build variable (repeatable) | | `--secret-build-env KEY=VALUE` | A secret buildpack build variable (repeatable) | | `--build-env-file FILE` | Build variables from a `.env` file | | `-d`, `--detach` | Return once the build starts, instead of following it | A complete first deploy of a production Node.js app: ```bash cloider deploy --name blog --port 3000 \ --env NODE_ENV=production \ --secret-env DATABASE_URL='postgres://blog:s3cret@orders-db.example:5432/blog' \ --subdomain blog-acme ``` See [Environment variables](/apps/environment-variables) for the difference between the variable kinds, and [`cloider deploy`](/cli/commands/deploy) for the command reference. ### Deploy a new version Run `cloider deploy` again in the same folder. Because the folder is linked, it uploads the current files as the next version of the same app: ```bash cloider deploy ``` ```text title="Output" Packaged 43 files (18.9 KB) ✓ Uploaded version 2 of blog Downloading source for app a7c2mq... ... The app is running. ✓ Deployed version 2 of blog https://blog-x7k2.cloider.app/ ``` The version that is live keeps serving until the new one is running. To deploy a folder to an app it isn't linked to, name the app: ```bash cloider deploy ./dist --app blog ``` A new deploy is refused while another build of the same app is running, or while the app is paused. ### How does a directory stay linked to an app? The first `cloider deploy` writes `.cloider/app.json` in the folder: ```json title=".cloider/app.json" { "app": "a7c2mq", "name": "blog", "account": "team@example.com", "api_url": "https://cloider.com/api" } ``` - `app` is the app's id, which is what later deploys use. `name` and `account` are there for you to read. - Commit the file to share the link with teammates and CI, or add `.cloider/` to `.gitignore` to keep it local. It is never uploaded with the source. - Link a folder to an existing app, or move the link to another app, with `cloider app link`: ```bash cloider app link blog cloider app link api ./services/api ``` ```text title="Output" ✓ Linked . to app blog ``` If the linked app is gone, or belongs to another account than the one you are logged in to, `cloider deploy` says so and asks you to deploy with `--app` or relink. ### What is uploaded? `cloider deploy` uploads every regular file in the folder, except: - `.git`, `.cloider` and `.DS_Store`, always. - Anything matched by a `.gitignore` or `.cloiderignore` file at the root of the folder, using `.gitignore` syntax. - Symbolic links and other special files. Use `.cloiderignore` for files that are in git but shouldn't be deployed, such as tests, docs or local data: ```text title=".cloiderignore" # Not needed to build or run the app tests/ docs/ *.md coverage/ .env.local ``` The archive can hold up to **2000 files** and **100 MB** compressed. Cloider also rejects a source that is over **500 MB** once extracted, that is empty, or that holds absolute paths or `..` entries. If you hit the file limit, the usual culprit is a dependency folder: make sure `node_modules/`, `vendor/`, `.venv/` or `target/` is in `.gitignore` or `.cloiderignore`. The build installs dependencies itself. :::tip[A zip with a single top folder] If the uploaded source contains a single folder at the top level, that folder is treated as the project root. A zip of `blog/` with everything inside it builds exactly like the contents of `blog/`. ::: ### Upload your source in the dashboard The dashboard takes a project folder, a `.zip` file, or a single HTML file. 1. Open **Apps** in the sidebar and click **Create App**. 2. Under **or upload your code**, drag and drop the folder, zip or HTML file onto **Application source**, or click **choose a folder** or **browse files**. The upload starts as soon as you pick it. 3. Wait while Cloider analyzes your code, then on **Configure your app** check the detected language, framework and port, and set **App Name**, **Port**, **Start Command** (optional) and **Subdomain**. 4. Optionally add environment variables, build variables and build arguments. 5. Click **Create App**. The dashboard uploader zips a folder in your browser. Unlike the CLI, it **doesn't** read `.gitignore` or `.cloiderignore`: it only leaves out `.git` and `.DS_Store`. Delete `node_modules/` and other dependency or build folders before you drop a folder, or zip the project yourself. A zip file you made is uploaded as it is. The same limits apply: 2000 files and 100 MB. Turning off **Give this app a free subdomain** opens a reminder that the app won't have an address. You can connect a domain later from **Domains**, see [Domains for apps](/apps/domains). #### Deploy a new version from the dashboard 1. Open the app and go to the **Builds** tab. 2. Click **Deploy new version**. 3. Drop the new folder, zip or HTML file and click **Deploy**. The new build appears at the top of the list. A git app shows **Deploy latest commit** instead, see [Deploy from git](/apps/deploy-from-git). ### Deploy from CI `cloider deploy` works the same in a pipeline: set `CLOIDER_TOKEN`, and either commit `.cloider/app.json` or pass `--app`. Nothing prompts without a terminal, and the command exits non-zero when the build or the start fails. ```yaml title=".github/workflows/deploy.yml" name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install the cloider CLI run: | curl -fsSL https://cloider.com/install.sh | CLOIDER_INSTALL_DIR="$HOME/bin" sh echo "$HOME/bin" >> "$GITHUB_PATH" - run: cloider deploy --app blog env: CLOIDER_TOKEN: ${{ secrets.CLOIDER_TOKEN }} ``` See [CI/CD with GitHub Actions](/guides/ci-cd-with-github-actions) and [CI/CD with GitLab](/guides/ci-cd-with-gitlab) for complete pipelines. ### Deploy with the REST API The CLI and the dashboard use the same public API. The archive itself never goes through the API: you get a presigned URL and `PUT` the zip to it. **Dashboard** Use **Create App** for a new app and **Deploy new version** on the **Builds** tab for a new version, as described above. **CLI** ```bash cloider deploy # a new app, or the next version of the linked one cloider deploy --app blog # the next version of a named app ``` **Terraform** The Terraform provider can't upload source or deploy apps. Run `cloider deploy` in your pipeline, then route a domain to the app with [`cloider_target`](/terraform/resources/target) if needed. **API** **A new app** takes five calls. Stage the upload with the archive's name, type and exact size in bytes: ```bash zip -r blog.zip . -x '.git/*' 'node_modules/*' SIZE=$(wc -c < blog.zip) curl -X POST https://cloider.com/api/apps/source \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"source\": {\"name\": \"blog.zip\", \"type\": \"application/zip\", \"size\": $SIZE}}" ``` ```json title="Response" {"token": "Wq3...staging-token", "presigned_url": "https://...signed-upload-url..."} ``` Upload the archive to the presigned URL with the same content type, then ask for detection: ```bash curl -X PUT "$PRESIGNED_URL" -H "Content-Type: application/zip" --data-binary @blog.zip curl -X POST https://cloider.com/api/apps/source/$TOKEN/detect \ -H "Authorization: Bearer $CLOIDER_TOKEN" ``` Poll the staged source until `status` is `Done` (or `Failed`, which still lets you create the app): ```bash curl https://cloider.com/api/apps/source/$TOKEN \ -H "Authorization: Bearer $CLOIDER_TOKEN" ``` ```json title="Response" { "status": "Done", "app_name": "blog", "language": "JavaScript", "framework": "Express", "port": 3000, "message": "", "build_args": [] } ``` Create the app on the staged source. Only `app_name`, `port` and `source_token` are required: ```bash curl -X POST https://cloider.com/api/apps \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "app_name": "blog", "port": 3000, "source_token": "'"$TOKEN"'", "start_command": "", "subdomain": "blog-x7k2.cloider.app", "env_vars": [ {"key": "NODE_ENV", "value": "production", "secret": false}, {"key": "DATABASE_URL", "value": "postgres://blog:s3cret@orders-db.example:5432/blog", "secret": true} ], "build_env_vars": [], "build_args": [] }' ``` ```json title="Response" {"hashed_id": "a7c2mq", "build_hashed_id": "b2k8vn", "version": "1"} ``` The first build is queued right away. If the subdomain or a variable list couldn't be saved, the response also carries `subdomain_error`, `env_error`, `build_env_error` or `build_args_error`; the app exists either way. A staged upload expires after 2 hours. **A new version** of an existing app takes three calls: ```bash curl -X POST https://cloider.com/api/apps/a7c2mq/builds \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"source\": {\"name\": \"blog.zip\", \"type\": \"application/zip\", \"size\": $SIZE}}" ``` ```json title="Response" {"hashed_id": "c9w3rt", "version": "2", "presigned_url": "https://...signed-upload-url..."} ``` ```bash curl -X PUT "$PRESIGNED_URL" -H "Content-Type: application/zip" --data-binary @blog.zip curl -X POST https://cloider.com/api/apps/a7c2mq/builds/c9w3rt/uploaded \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"uploaded": true}' ``` Send `{"uploaded": false}` if the upload failed, which fails the build at once instead of leaving it `Pending`. A build whose upload never finishes is failed after 15 minutes. See [App builds API](/api/endpoints/app-builds). **MCP** Ask your agent: ```text title="Prompt" Deploy /home/me/blog to Cloider as an app called blog on port 3000, with NODE_ENV=production. ``` It calls the `deploy_app` tool, which follows the build and answers with the URL: ```json title="Tool call: deploy_app" {"dir": "/home/me/blog", "name": "blog", "port": 3000, "env": {"NODE_ENV": "production"}} ``` For the next version, the same tool with only the directory, or with `app` to pick the app: ```json title="Tool call: deploy_app" {"dir": "/home/me/blog"} ``` ### Verify - `cloider deploy` ends with `✓ Deployed version N of ` and prints the URL. - `cloider app get blog` shows `Status Ready`, the version and the URL. - `cloider app open blog` opens the app in your browser. ### Troubleshooting | Message | Cause and fix | | --- | --- | | `more than 2000 files to deploy, list what isn't needed in .cloiderignore` | Add dependency, build and test folders to `.cloiderignore` | | `the archive is 140 MB, over the 100 MB limit; list what isn't needed in .cloiderignore` | Same, or remove large assets and serve them from [object storage](/object-storage/overview) | | `nothing to deploy, every file is ignored` | Your ignore files match everything. Check `.gitignore` and `.cloiderignore` | | **Only up to 2000 files can be uploaded, zip your project first.** (dashboard) | Remove `node_modules/` and similar folders, or zip the project without them | | **The selection is 152 MB, the limit is 100MB.** (dashboard) | Same as above | | **This upload has expired, upload your app again!** | More than 2 hours passed between the upload and creating the app. Start again | | `the extracted source is 612MB, over the 500MB limit` (in the build log) | The archive is small but expands beyond 500 MB. Remove generated or binary files | | `the source archive is empty` (in the build log) | The zip has no files. Check what you zipped | | `. is linked to app a7c2mq, which this account doesn't have; deploy with --app, or relink with cloider app link` | The linked app was deleted or belongs to another account. Use `--app`, or `cloider app link` | | `couldn't find a free subdomain, pick one with --subdomain` | Pass `--subdomain` with a name of your own | | **This app is already building, wait for that build to finish before deploying a new version!** | One build at a time per app. Wait, then deploy again | | **This app is paused. Resume it before deploying a new version!** | `cloider app resume blog`, then deploy | | `detection took too long, try again` | Detection is queued with other work. Run `cloider deploy` again, or pass `--name` and `--port` | If the upload works but the build or the start fails, see [Troubleshooting apps](/apps/troubleshooting). ### Next steps - [Build configuration](/apps/build-configuration): detection, Dockerfile or buildpacks, and the port. - [Environment variables](/apps/environment-variables): runtime variables, secrets and build-time variables. - [Builds and deployments](/apps/builds-and-deployments): build history, logs and retries. - [`cloider deploy` reference](/cli/commands/deploy): every flag. --- ## Free subdomains and custom domains for Cloider apps Every Cloider app can have a free address such as `https://blog-x7k2.cloider.app`, claimed when you create the app, and any number of your own domains such as `https://example.com` or `https://example.com/blog`. An app is put on a domain with a **target**: a route from a path of the domain to the app. Cloider serves every address over HTTPS with a certificate it manages. This page covers the free subdomain, custom domains, path routing, and what happens to domains when an app is paused or deleted. ### At a glance | | Free subdomain | Custom domain | | --- | --- | --- | | **Example** | `blog-x7k2.cloider.app` | `example.com`, `www.example.com` | | **How you get it** | Claimed when you create the app, or later from **Domains** | Added in **Domains**, then delegated to Cloider's nameservers at your registrar | | **HTTPS** | Automatic | Automatic, once the domain points at Cloider | | **Routed to the app with** | A target on `/`, created for you | A target you add | | **When the app is deleted** | Deleted with it by default | Kept; only its route to the app is removed | - The app's **Address** on the **Overview** tab is its first domain: the free subdomain claimed when it was created, if it has one. - The address answers once the first version is running. Before that the Overview says **live once the first build deploys**. - Requests reach the app as plain HTTP on its port. Cloider terminates TLS in front of it. ### The free cloider.app subdomain When you create an app, Cloider suggests `-<4 random characters>.cloider.app` and checks that it is free as you type. You can type your own label instead, with these rules: - 2 to 63 characters: lowercase letters, digits and `-`. - Starts with a letter, doesn't start or end with `-`, and has no dots. - Not already taken by anyone. **Dashboard** On **Create App**, step **Configure your app**: 1. Type a label in **Subdomain**, for example `blog-acme`. The field shows **blog-acme.cloider.app is available** or why it can't be used. 2. Leave **Give this app a free subdomain** on. 3. Click **Create App**. Turning **Give this app a free subdomain** off creates the app with no address; a dialog, **This app won't have an address**, reminds you to connect a domain later. **CLI** `cloider deploy` and `cloider app create` claim `-<4 random characters>.cloider.app` unless told otherwise: ```bash cloider deploy --name blog --subdomain blog-acme # blog-acme.cloider.app cloider deploy --name blog --no-subdomain # no address yet ``` Check a name first: ```bash cloider domain check blog-acme.cloider.app ``` **Terraform** Claim a subdomain and route it to an existing app with `cloider_domain` and `cloider_target`: ```hcl title="main.tf" resource "cloider_domain" "blog" { name = "blog-acme.cloider.app" } # The app's id comes from `cloider app list -o wide` resource "cloider_target" "blog" { domain = cloider_domain.blog.name app_id = "a7c2mq" } ``` **API** Pass the full subdomain when creating the app (see [Deploy from your computer](/apps/deploy-from-your-computer#deploy-with-the-rest-api)): ```json {"app_name": "blog", "port": 3000, "source_token": "Wq3...staging-token", "subdomain": "blog-acme.cloider.app"} ``` Check availability first: ```bash curl "https://cloider.com/api/domains/availability?domain_name=blog-acme.cloider.app" \ -H "Authorization: Bearer $CLOIDER_TOKEN" ``` If the subdomain can't be claimed, the app is still created and the response carries a `subdomain_error`. **MCP** ```text title="Prompt" Deploy /home/me/blog as an app called blog on the subdomain blog-acme. ``` ```json title="Tool call: deploy_app" {"dir": "/home/me/blog", "name": "blog", "subdomain": "blog-acme"} ``` See [Free subdomains](/domains/free-subdomains) for claiming more subdomains. ### Put an app on your own domain To serve an app on a domain you own, add the domain to Cloider, point it at Cloider's nameservers, add DNS records for the host names you serve, and add a target to the app. 1. **Add the domain.** In **Domains**, add `example.com` (or run `cloider domain create example.com`). Cloider shows the nameservers to use. See [Add a custom domain](/domains/add-a-custom-domain). 2. **Change the nameservers** at your registrar to the ones Cloider shows, and wait for the change to propagate. See [Nameservers](/domains/nameservers). 3. **Add DNS records** for the subdomains you serve. The domain itself comes with an `ALIAS` record that points at Cloider, and the target in step 4 points it at the app's free subdomain, but a target doesn't add records. Point `www` (or any subdomain) at your app's free subdomain with a `CNAME`, as [Add a custom domain](/domains/add-a-custom-domain) shows step by step. 4. **Route it to the app** with a target, as shown below. The app must have deployed: its status must be `Ready` (or `Paused`). **Dashboard** 1. Open **Domains** in the sidebar and click your domain. 2. Click **Add target**. In **Add a new target**, set **Target type** to **App** and pick the app under **App**. 3. Set **Path** (`/` for the whole domain) and, optionally, **Target path**. 4. Click **Save**. The domain serves the app over HTTPS once its certificate is issued. **CLI** ```bash cloider target create example.com --app web cloider target create example.com --app blog --path /blog ``` ```text title="Output" ✓ Routed example.com/ to the app ✓ Routed example.com/blog to the app ``` **Terraform** ```hcl title="main.tf" resource "cloider_domain" "example" { name = "example.com" } resource "cloider_target" "web" { domain = cloider_domain.example.name app_id = "w4d8hz" } resource "cloider_target" "blog" { domain = cloider_domain.example.name path = "/blog" app_id = "a7c2mq" } ``` See [`cloider_target`](/terraform/resources/target). **API** ```bash curl -X POST https://cloider.com/api/targets \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"domain_name": "example.com", "target_type": "App", "app_id": "w4d8hz", "path": "/", "target_path": "/"}' ``` ```json title="Response" { "hashed_id": "t4k9vq", "domain_name": "example.com", "message": "Target created successfully" } ``` **MCP** ```text title="Prompt" Route example.com to the web app. ``` ```json title="Tool call: create_domain_target" {"domain": "example.com", "app": "web"} ``` #### Route paths to different apps A domain routes each path to one target, and a path covers everything under it, so `/` catches whatever no longer path does. For example, `example.com` can serve the `web` app on `/`, the `api` app on `/api` and the `blog` app on `/blog`: ```bash cloider target create example.com --app web cloider target create example.com --app api --path /api cloider target create example.com --app blog --path /blog ``` By default a request reaches the app with its full path (`/api/users` arrives as `/api/users`). Set a **target path** to rewrite the prefix, for example to serve the `api` app at `/api` while it handles requests at `/`: ```bash cloider target create example.com --app api --path /api --target-path / ``` Now `https://example.com/api/users` reaches the app as `/users`. An app served under a path must generate links and redirects relative to that path, or use the target path to strip it. ### See an app's domains Every domain routed to the app is listed with it: - **Dashboard**: the **Address** on the **Overview** tab, and each domain's page under **Domains**. - **CLI**: `cloider app get blog` prints `URL` and `Domains`; `cloider app open blog` opens the URL in your browser. - **API**: `url` and `domains` on `GET /api/apps/`: ```json { "url": "https://blog-x7k2.cloider.app/", "domains": [ {"domain_name": "blog-x7k2.cloider.app", "domain_type": "Subdomain"}, {"domain_name": "example.com", "domain_type": "CustomDomain"} ] } ``` ### Remove a domain from an app Delete the target that routes the domain to the app, from the domain's page in **Domains**, with `cloider target delete `, or by deleting the `cloider_target` in Terraform. The domain itself stays yours. To give up a free subdomain entirely, delete the domain. See [Targets](/domains/targets) and [Delete a domain](/domains/delete-a-domain). ### Domains when an app is paused or deleted - **Paused**: the domains and targets stay. The address keeps resolving but has no app behind it until you resume. - **Deleted**: the app's free subdomains are deleted with it by default (the dashboard pre-selects them, and `cloider app delete --keep-domains` keeps them). A custom domain is never deleted with an app; it only loses its route to the app, and its other records and targets stay. See [Pause, resume and delete](/apps/pause-resume-and-delete). ### Troubleshooting | Message | Cause and fix | | --- | --- | | **This subdomain is already taken. Please try another one.** | Pick another label | | **You already own this subdomain. Connect it to the app from Domains instead.** | The subdomain is already in your account. Add a target to it instead of claiming it again | | **This app is not ready yet. Wait until it finishes deploying, then add the target.** | Targets need a deployed app. Wait for the first build to be `Ready` | | **This path is already in use on this domain. Please choose a different path.** | Another target uses that path. Delete or update it, or pick another path | | **This app's name is too long to be exposed on a domain.** | Apps with very long names can't be routed from a domain. Create the app with a shorter name, for example under 50 characters | | The custom domain doesn't load | The nameservers aren't pointing at Cloider yet, the host name has no `A` or `CNAME` record on the domain, or the certificate isn't issued yet. See [Domains troubleshooting](/domains/troubleshooting) | | `502` or an error page on the address | The app isn't answering on its port. See [Troubleshooting apps](/apps/troubleshooting) | ### Next steps - [Add a custom domain](/domains/add-a-custom-domain): delegate your domain to Cloider. - [Targets](/domains/targets): every routing option. - [HTTPS certificates](/domains/https-certificates): how certificates are issued. - [DNS records](/domains/dns-records): email and other records on the same domain. --- ## Environment variables and secrets for Cloider apps Environment variables are how you configure a Cloider app without changing its code: database URLs, API keys, feature flags, `NODE_ENV`. An app has one list of **environment variables** for the running app, and two lists that only its builds read, **build arguments** and **build variables**. Any variable can be a **secret**, whose value is never shown again unless you ask for it. This page covers all three lists, secrets, limits, and every way to set them. ### At a glance | List | Who reads it | Applied by | Dashboard section | CLI | | --- | --- | --- | --- | --- | | **Environment variables** | The running app, when its container starts | A restart of the app | **Environment variables** | `cloider app env` | | **Build arguments** | Your `Dockerfile` build (`ARG`, `RUN --mount=type=secret`) | A rebuild | **Build arguments** | `cloider app build-args` | | **Build variables** | A buildpack build (no `Dockerfile`) | A rebuild | **Build variables** | `cloider app build-env` | - **Secret** values are never listed, in the dashboard, the CLI or the API. You read one by name, one at a time. - `PORT` is always set by Cloider to the app's port and overrides a variable of the same name. - Plain environment variables are also visible to a **buildpack** build. Secret environment variables never reach any build. - Values are stored with the app on the platform, not in the source. ### Set environment variables Saving the environment **restarts the app** so its container picks up the new values. Nothing is rebuilt. **Dashboard** 1. Open the app and go to the **Environment** tab. 2. Under **Environment variables**, click **Add variable** and fill in the **Name** and **Value**. To add several at once, click **Paste .env**, paste `KEY=VALUE` lines and click **Add these**. 3. Click the lock icon on a row to store it as a secret (**Click to store as a secret**). 4. Click **Save changes**. To remove a variable, click its trash icon and save. **Discard** throws away unsaved edits. **CLI** `set` adds or replaces the variables you name and keeps the rest: ```bash cloider app env set blog NODE_ENV=production LOG_LEVEL=info cloider app env set blog --secret DATABASE_URL='postgres://blog:s3cret@orders-db.example:5432/blog' ``` ```text title="Output" ✓ Saved, blog restarts with 2 variable(s) ✓ Saved, blog restarts with 3 variable(s) ``` List them. Secret values are never shown: ```bash cloider app env list blog ``` ```text title="Output" KEY VALUE NODE_ENV production LOG_LEVEL info DATABASE_URL (secret) ``` Read one value, secrets included, or remove variables: ```bash cloider app env get blog DATABASE_URL cloider app env unset blog LOG_LEVEL ``` Import a `.env` file. `--secret` stores every value in it as a secret, and `--replace` removes the variables the file doesn't have: ```bash cloider app env import blog .env.production cloider app env import blog secrets.env --secret cat .env.production | cloider app env import blog - ``` The file format is `KEY=VALUE` per line, with optional `export `, `#` comments, and single or double quotes. **Terraform** The Terraform provider can't manage app variables. Use the dashboard, the CLI or the API. **API** `GET` lists the variables, with `null` as the value of every secret: ```bash curl https://cloider.com/api/apps/a7c2mq/env \ -H "Authorization: Bearer $CLOIDER_TOKEN" ``` ```json title="Response" [ {"key": "NODE_ENV", "secret": false, "value": "production"}, {"key": "LOG_LEVEL", "secret": false, "value": "info"}, {"key": "DATABASE_URL", "secret": true, "value": null} ] ``` `PUT` replaces the whole list: a variable missing from it is removed, and a secret sent with `"value": null` keeps its stored value. So you can send back what `GET` returned, with your changes: ```bash curl -X PUT https://cloider.com/api/apps/a7c2mq/env \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"env_vars": [ {"key": "NODE_ENV", "secret": false, "value": "production"}, {"key": "DATABASE_URL", "secret": true, "value": null}, {"key": "SENTRY_DSN", "secret": true, "value": "https://abc123@o1.ingest.example/42"} ]}' ``` ```json title="Response" {"success": true} ``` Read one value, secrets included, as plain text: ```bash curl "https://cloider.com/api/apps/a7c2mq/env/value?key=DATABASE_URL" \ -H "Authorization: Bearer $CLOIDER_TOKEN" ``` **MCP** Ask your agent: ```text title="Prompt" Set NODE_ENV=production on the blog app, and store DATABASE_URL as a secret with the connection string of orders-db. ``` It calls `set_app_variables`, once per kind of value: ```json title="Tool call: set_app_variables" {"app": "blog", "variables": {"NODE_ENV": "production"}} ``` ```json title="Tool call: set_app_variables" {"app": "blog", "variables": {"DATABASE_URL": "postgres://blog:s3cret@orders-db.example:5432/blog"}, "secret": true} ``` `list_app_variables`, `get_app_variable` and `unset_app_variables` read and remove them. You can also set environment variables when you create the app: on **Configure your app** in the dashboard, or with `--env`, `--secret-env` and `--env-file` on `cloider deploy` and `cloider app create`. ### Secret variables A secret (locked) variable works exactly like a plain one inside the app. The difference is who can see it afterwards: - Its value is never included when variables are listed: the dashboard shows a password field, the CLI `(secret)`, and the API `null`. - You reveal one value at a time by name: the eye icon (**Show**) in the dashboard, `cloider app env get`, or `GET /api/apps//env/value`. - Saving the list without touching a secret keeps its value. Use secrets for passwords, tokens, private keys and connection strings with credentials. Members with read-only access can see which variables exist. ### Build arguments and build variables The two build lists work like the environment, with the same secrets and limits, but only a build reads them and the running app never sees them. Saving one of them **rebuilds** the version the app is serving (the button says **Save and rebuild**), or applies from the first build if the app hasn't deployed yet. - **Build arguments** are for apps with a `Dockerfile`. A plain one is passed as `--build-arg NAME=value` for an `ARG NAME`; a secret one is passed as a build secret for `RUN --mount=type=secret,id=NAME,env=NAME`. An empty value isn't passed, so the `ARG`'s default applies. Names your `Dockerfile` declares are offered under **Declared in your Dockerfile**. - **Build variables** are for apps built with buildpacks. They are the build's environment, and win over a plain environment variable of the same name. ```bash cloider app build-args set web NEXT_PUBLIC_API_URL=https://api.example.com cloider app build-args set web --secret NPM_TOKEN=npm_xxxxxxxxxxxxxxxx cloider app build-env set blog --secret NPM_TOKEN=npm_xxxxxxxxxxxxxxxx ``` ```text title="Output" ✓ Saved 1 argument(s), building web again ✓ Saved 2 argument(s), building web again ✓ Saved 1 variable(s), building blog again ``` The API paths are `/api/apps//build-args` and `/api/apps//build-env`, with the same `GET`, `PUT` and `/value` as `/env`; their `PUT` answers `{"applied": "build"}` or `{"applied": "none"}`. With MCP, pass `"kind": "build-args"` or `"kind": "build-env"` to the variable tools. See [Build configuration](/apps/build-configuration#build-arguments-for-a-dockerfile) for Dockerfile examples. A build list can't be changed while a build is running or while the app is paused. ### Which list should I use? | You want to... | Use | | --- | --- | | Give the running app a database URL, API key or setting | Environment variable (secret for credentials) | | Bake a public URL into a frontend bundle built by your `Dockerfile` | Build argument | | Install private npm or pip packages in a `Dockerfile` build | Secret build argument, read with `RUN --mount=type=secret` | | Install private packages in a buildpack build | Secret build variable | | Configure how a buildpack builds your app | Build variable | | Change the port | Not a variable: the app's **Port** setting, see [Settings](/apps/settings) | ### Variable names and limits | Rule | Limit | | --- | --- | | Name | Starts with a letter or `_`, then letters, digits and `_`; up to 128 characters. Names are case sensitive | | Variables per list | 100 | | Size of one value | 16 KB | | Size of a whole list | 64 KB (names plus values) | | Duplicates | A name can appear once per list | Values can be any text. A multi-line value such as a PEM key fits in one variable when you set it with the CLI or the API, for example `cloider app env set blog --secret TLS_KEY="$(cat key.pem)"`. For larger configuration files, store them in a [bucket](/object-storage/overview) or in your source. ### Connect an app to a Cloider database A typical setup puts the connection string of a [managed database](/databases/overview) in a secret variable: 1. Create the database, for example `orders-db`, and copy its connection string from [its connection details](/databases/connect). 2. Add the app as a [trusted source](/databases/trusted-sources) of the database, so the database accepts its connections. 3. Set the connection string on the app as a secret: ```bash cloider app env set blog --secret DATABASE_URL='postgres://orders:s3cret@orders-db.example:5432/orders?sslmode=require' ``` ### Verify Check what the running app actually sees from inside its container: ```bash cloider app exec blog printenv NODE_ENV ``` ```text title="Output" production ``` See [Terminal and exec](/apps/terminal-and-exec). ### Troubleshooting | Message | Cause and fix | | --- | --- | | `Variable name must start with a letter or underscore and contain only letters, numbers and underscores` | Rename the variable, for example `API-KEY` to `API_KEY` | | `This variable is set more than once` | The list has two rows with the same name. Remove one | | `This value exceeds the allowed size of 16KB` | Store large values in a bucket or in your source | | `The environment variables exceed the allowed size of 64KB` | The whole list is too big. Move large values elsewhere | | `An app can have at most 100 environment variables` | Combine related values, for example into one JSON variable | | **This app is building, wait for that build to finish before changing its build variables!** | Build lists can't change during a build. Wait and save again | | **This app is paused. Resume it before deploying a new version!** | Build lists can't change while the app is paused. Resume it first | | The app still sees an old value | Saving the environment restarts the app; check the new container is running on the **Logs** tab. A build argument or build variable needs a rebuild, which saving starts | | A variable is empty during a `Dockerfile` build | Set it as a build argument and declare `ARG NAME` in the stage that uses it. Environment variables don't reach a `Dockerfile` build | ### Next steps - [Build configuration](/apps/build-configuration): Dockerfile `ARG` and build secrets in depth. - [Terminal and exec](/apps/terminal-and-exec): inspect the running environment. - [Settings](/apps/settings): the port and start command. - [Security](/concepts/security): how Cloider protects tokens and secrets. --- ## App insights, requests, CPU, memory and cost per day The **Insights** tab shows how much traffic an app serves and what it uses and costs: requests per day split by status class, the data sent back, average CPU and memory for this month and today, the cost so far this month, a forecast for this month and next, and daily charts for the last 31 days. The same numbers come from `cloider app usage` and the REST API. CPU and memory are the figures an app is billed on, so this page also explains how they are measured. Requests and data out are shown only and never billed. ### At a glance | Card or chart | Shows | | --- | --- | | **Requests (Today)** | Requests to the app's domains so far today | | **Requests (This Month)** | Requests since the first of the month | | **Server Errors (This Month)** | The share of this month's requests answered with a 5xx status, and how many | | **Data Out (This Month)** | Bytes sent back to clients since the first of the month | | **Requests** | Requests per day, stacked by 2xx, 3xx, 4xx and 5xx, last 31 days | | **Data Out** | Bytes sent back per day, last 31 days | | **Avg. Usage (This Month)** | Average CPU in cores and memory in GB over the days of this month | | **Avg. Usage (Today)** | Average CPU and memory so far today | | **Cost to Date (This Month)** | What the app has cost since the first of the month | | **Cost Forecast** | The expected cost for **This Month** and **Next Month** at the current usage | | **CPU Usage** | Average cores per day, last 31 days | | **Memory Usage** | Average GB per day, last 31 days | | **Cost** | Cost per day, last 31 days | Usage is sampled every minute while the app runs. A new app shows **Usage is sampled every minute while the app runs, and shows here once the first samples are in.** until its first samples arrive. ### How are requests counted? Requests are counted where they enter Cloider, at the ingress in front of every app domain: the subdomain the app was created with and any [custom domain](/apps/domains) or target pointing at it. Every request is counted, including the ones the app never sees, such as the `503` a client gets while the app is down or restarting. - **Requests** is the total. The chart splits it by status class: **2xx** success, **3xx** redirect, **4xx** client error and **5xx** server error. WebSocket upgrades (`101`) are in the total only. - **Data Out** is the size of the responses sent back, headers included, in binary units (1 KB = 1024 bytes). - Counts are added every minute to the current day, in UTC. - Days from before requests were counted show no bar. ### How is usage measured? Each sample reads the CPU and memory of the app's container. The terminal that the dashboard's **Terminal** tab opens isn't counted, only your app. - **CPU** is in cores: `0.25 cores` is a quarter of one CPU core on average. - **Memory** is in GB. - Samples are averaged over time, so a short spike barely moves a daily average. - A **paused** app has no container, so its samples are zero and it costs nothing for compute. Cost is calculated from the same averages: the larger of the CPU cores used and the memory used converted to cores at a fixed ratio, times the compute rate, times the time the app ran. The rate is on the [pricing page](/reference/pricing), and [the billing model](/concepts/billing-model) explains how daily costs become an invoice. ### Read an app's usage **Dashboard** 1. Open the app. 2. Go to the **Insights** tab. For every resource of the account at once, open [**Usage**](/account/usage) in the sidebar. **CLI** ```bash cloider app usage blog ``` ```text title="Output" Month: 2026-10 So far: $1.84 Forecast this month: $2.91 Forecast next month: $2.88 Daily usage is in -o json. ``` The daily figures, CPU in millicores and memory in MiB, are in the JSON: ```bash cloider app usage blog -o json ``` **Terraform** The Terraform provider doesn't read usage. Use the dashboard, the CLI or the API. **API** ```bash curl https://cloider.com/api/apps/a7c2mq/usage \ -H "Authorization: Bearer $CLOIDER_TOKEN" ``` The response is the app with its usage. A trimmed example: ```json title="Response" { "hashed_id": "a7c2mq", "app_label": "blog", "status": "Ready", "daily_usage": [ { "resource_type": "app", "date": "2026-10-19T00:00:00Z", "total_cost": 0.0947, "total_time_seconds": 86400, "metrics": { "cpu": 38.5, "memory": 212.4, "compute_cost": 0.0947, "requests": 18342, "requests_by": {"2xx": 17610, "3xx": 402, "4xx": 318, "5xx": 12}, "bytes_out": 96468992 } } ], "monthly_usage": { "avg_cpu_usage": 41, "avg_memory_usage": 208, "compute_total_cost": "1.84", "total_cost": 1.84, "days": 20, "forecast_this_month": 2.91, "forecast_next_month": 2.88 }, "forecast_rest_of_day": 0.0312, "date": "2026-10-20", "month": "2026-10" } ``` `metrics.cpu` and `monthly_usage.avg_cpu_usage` are in millicores (1000 = one core), `metrics.memory` and `avg_memory_usage` in MiB. `metrics.requests` is the day's request count and `metrics.requests_by` its split by status class, `metrics.bytes_out` the bytes sent back. Days from before requests were counted have none of the three. `daily_usage` holds up to the last 31 days. **MCP** Ask your agent: ```text title="Prompt" How much has the blog app cost this month, and what is the forecast? ``` It calls the `get_usage` tool for one app: ```json title="Tool call: get_usage" {"kind": "app", "name": "blog"} ``` ### Use insights to size and save - **5xx share goes up**: the app is failing requests. Check the [logs](/apps/logs) around the days it rose. - **Memory climbs day after day**: the app probably leaks memory. Check the [logs](/apps/logs) and restart it by saving the **Environment** tab, then fix the leak. - **High CPU at idle**: look for busy loops, aggressive polling or a development server running in production (for example `npm run dev` instead of `npm start`). - **An app nobody uses**: [pause it](/apps/pause-resume-and-delete). A paused app costs nothing for compute and keeps its configuration. For live numbers rather than daily averages, look inside the container, if its image has `ps`: ```bash cloider app exec blog -- ps aux ``` ### Next steps - [Logs](/apps/logs): what the app is doing right now. - [Pause, resume and delete](/apps/pause-resume-and-delete): stop paying for idle apps. - [Usage](/account/usage): every resource's usage in one place. - [Pricing](/reference/pricing): current rates. --- ## View and stream Cloider app logs An app's **logs** are everything its running container prints to standard output and standard error: requests your framework logs, `console.log` and `print` calls, warnings and stack traces. Cloider streams them live from the app's container to the dashboard's **Logs** tab, to `cloider app logs`, to the REST API and to AI agents. This page shows each way to read them and explains what the stream keeps and what it doesn't. A [cron job](/apps/cron-jobs)'s logs are its runs' instead, on the **Runs** tab. ### At a glance | | | | --- | --- | | **What** | The stdout and stderr of the app's running container | | **History** | The last 100, 200 (default), 500 or 1000 lines the current container printed | | **Live** | New lines arrive as the app prints them, for up to 60 minutes per stream | | **Kept** | Not stored by Cloider: logs live in the container, so a restart or a new deploy starts a fresh log | | **Build output** | Separate, on the **Builds** tab. See [Builds and deployments](/apps/builds-and-deployments) | | **Not available** | While the app is paused, or before its first version deploys | ### Read the logs **Dashboard** 1. Open the app and go to the **Logs** tab. 2. The tab shows **Connecting to the app...**, then the latest lines, and keeps following with a **Live** badge. 3. Pick how much history to load with **Lines** (100, 200, 500 or 1000). Changing it opens a new stream. 4. Scroll up to read without being pulled back down; scroll to the bottom to follow again. When the stream ends (after 60 minutes, or if the connection can't be restored), the tab says **This stream has ended. Reconnect to keep following the app.** Click **Reconnect**. The stream is open only while the **Logs** tab is on screen. Colored output (ANSI escape codes) is shown in color. **CLI** Print the last 200 lines and exit: ```bash cloider app logs blog ``` ```text title="Output" > blog@1.0.0 start > node server.js Listening on 3000 GET / 200 4.112 ms - 18 GET /health 200 0.731 ms - 2 ``` Follow new lines as they come, like `tail -f`, with more history: ```bash cloider app logs blog -f -n 1000 ``` `-n`, `--lines` takes 100, 200, 500 or 1000. Logs go to stdout, so you can pipe them: ```bash cloider app logs blog -n 1000 | grep -i error cloider app logs blog -f | tee blog.log ``` If a followed stream reconnects, the CLI prints `--- reconnected, the last lines may repeat ---` on stderr, since a reconnect starts from a fresh tail. **Terraform** The Terraform provider can't read app logs. Use the dashboard, the CLI or the API. **API** The logs are a server-sent events stream. `lines` is `100`, `200`, `500` or `1000` (anything else means `200`), and `follow=false` sends the tail and ends instead of following: ```bash curl -N "https://cloider.com/api/apps/a7c2mq/logs/stream?lines=200&follow=false" \ -H "Authorization: Bearer $CLOIDER_TOKEN" ``` ```text title="Response" : open event: reset data: 1 data: Listening on 3000 data: data: GET / 200 4.112 ms - 18 data: GET /health 200 0.731 ms - 2 data: event: end data: 1 ``` - Each chunk of output arrives as one event whose lines are `data:` fields; join them with newlines to get the output exactly as printed. - `reset` opens every stream. When you reconnect, discard what you had: the new stream starts from a fresh tail. - `end` means nothing more is coming on this stream. - Comment lines (`: ping`) are keep-alives sent while the app is quiet. **MCP** Ask your agent: ```text title="Prompt" Show me the last 500 lines of the blog app's logs and tell me if there are errors. ``` It calls the `get_app_logs` tool, which returns the latest lines and doesn't follow: ```json title="Tool call: get_app_logs" {"app": "blog", "lines": 500} ``` ### What do the logs contain? - Everything the app's process writes to **stdout** and **stderr**, in order. Write your logs there rather than to files inside the container: files aren't streamed and are lost with the container. - Only the app's **current** container. During a rollout the stream moves to the new version's container once it is running. - Lines your app prints while starting, including crash output. If the container restarts, the stream picks up the new one, and prints `--- waiting for a pod ---` while there is no container to read. They don't contain the build output (see the **Builds** tab), nor requests that never reached your app. ### How long are app logs kept? Cloider doesn't store app logs. The stream reads them from the running container, so: - A restart (saving environment variables, a crash) or a new deploy starts with an empty log. - History is limited to what the container still holds, up to the 1000 lines you can ask for. To keep logs, send them from your app to a logging service of your choice, or write them to an [object storage bucket](/object-storage/overview). ### Why does the Logs tab say nothing was printed? | What you see | Meaning | | --- | --- | | **Nothing printed yet.** | The stream is open and the app hasn't printed anything since its container started | | **Nothing was printed.** | The stream ended without output | | `This app has not deployed a version yet.` | No version is running yet. Check the **Builds** tab | | `The logs could not be read, try again!` | The stream couldn't be opened. Reconnect in a moment | | No **Logs** tab | The app is paused. [Resume it](/apps/pause-resume-and-delete) to see logs | Some runtimes buffer output when it isn't a terminal. If lines arrive late or in bursts, turn buffering off: for Python set the environment variable `PYTHONUNBUFFERED=1`; in other languages flush after writing, or log to stderr. ### Next steps - [Insights](/apps/insights): CPU and memory over time. - [Terminal and exec](/apps/terminal-and-exec): run commands inside the container. - [Troubleshooting](/apps/troubleshooting): crash loops and start failures. --- ## Cloider App Engine overview An **app** on Cloider is your source code running as a web service: you hand Cloider a folder, a zip file or a GitHub, GitLab or Bitbucket repository, and Cloider builds it into a container image, runs it, and serves it over HTTPS on a free `*.cloider.app` address or on your own domain. There are no servers, clusters or Kubernetes manifests to manage. This page explains how App Engine works, what it costs, its limits, and where to go next. ### At a glance | | | | --- | --- | | **What it is** | A managed container built from your source code and served over HTTPS | | **Sources** | A folder, a `.zip` file or a single HTML file uploaded from the dashboard; a local directory with `cloider deploy`; a GitHub, GitLab or Bitbucket repository | | **How it is built** | Your own `Dockerfile` if the project has one; otherwise Cloud Native Buildpacks; a plain folder of HTML is served as a static site | | **Languages** | Node.js, Python, Go, Java, Scala, PHP, Ruby, .NET and static sites without a Dockerfile, anything with one. See [Supported languages](/apps/supported-languages) | | **Address** | A free subdomain such as `https://blog-x7k2.cloider.app`, plus any [custom domain](/apps/domains) | | **Instances** | One container per app | | **Source size** | Up to 100 MB compressed and 500 MB extracted per deploy | | **Billing** | Pay as you go for the CPU and memory the running container uses. A paused app uses none. See [Usage and billing](#how-is-an-app-billed) | | **Managed with** | Dashboard, [`cloider` CLI](/cli/commands/app), [REST API](/api/endpoints/apps), [MCP server](/mcp/tools). Terraform can route a domain to an app with [`cloider_target`](/terraform/resources/target) | ### How does an app work? An app goes from source code to a running service in four steps, and Cloider does all of them for you every time you deploy: 1. **Upload or fetch the source.** The dashboard and the CLI zip your folder and upload it. A git app has its branch fetched as an archive at the latest commit. 2. **Build an image.** Cloider looks at the root of the source and picks a [build strategy](/apps/build-configuration): your `Dockerfile`, a buildpack for your language, or a static web server for plain HTML. 3. **Deploy it.** The image is pushed to a private registry project of your account and started as a container. The container is told which port to listen on through the `PORT` environment variable. 4. **Wait until it is really running.** A version only counts as deployed once something answers on the app's port and keeps running for 15 seconds. Until then the previous version keeps serving traffic. ```mermaid flowchart LR A[Source code
folder, zip or git] --> B[Build
Dockerfile, buildpack
or static site] B --> C[Image in your
apps registry project] C --> D[Container listening
on PORT] D --> E[HTTPS
blog-x7k2.cloider.app] ``` Each deploy creates a new **build** with the next version number (1, 2, 3 and so on). The builds list on the app's **Builds** tab is the history of every deploy, with its status, source, duration and logs. See [Builds and deployments](/apps/builds-and-deployments). #### What runs inside an app? Each app runs as one container from the image of its live build. The container: - Gets the app's [environment variables](/apps/environment-variables), plain and secret, when it starts. - Gets a `PORT` variable set to the app's port, which overrides any variable of the same name you set. - Runs as a non-root user when it was built by a buildpack or as a static site. An image built from your own `Dockerfile` runs as the user the `Dockerfile` sets. Binding a port below 1024 (for example 80) works either way. - Is reached from the internet through its domains, over HTTPS. Cloider terminates TLS and forwards plain HTTP to the app's port. - Runs on `linux/amd64`. #### Where are an app's images stored? Cloider pushes every image it builds to a private [container registry](/container-registry/overview) project that it creates for your account with your first app. The project carries an **Apps** badge in the registry list. Cloider manages it for you: you can browse its images, but you can't delete them or change its policies. Cloider keeps the image the app is running and the one deployed before it, deletes older and failed images after each build, and deletes an app's images when you delete the app. An app [created from a Docker image](/apps/deploy-an-image) is the exception: Cloider doesn't build or copy its image, it pulls the image from your registry on every start, pinned to the digest its tag pointed at when it was deployed. ### Ways to deploy an app | Method | Best for | Later versions | | --- | --- | --- | | [Import a git repository](/apps/deploy-from-git) (GitHub, GitLab, Bitbucket) | Teams with code in git | Every push to the branch deploys automatically, or click **Deploy latest commit** | | [`cloider deploy`](/apps/deploy-from-your-computer) from a directory | Developers, scripts, CI, AI agents | Run `cloider deploy` again in the same directory | | [Upload in the dashboard](/apps/deploy-from-your-computer#upload-your-source-in-the-dashboard) | Quick tests, static sites, a zip from somewhere else | Click **Deploy new version** and upload again | | [Deploy a Docker image](/apps/deploy-an-image) from any registry | Teams that build their own images in CI | Push to the tag, then click **Deploy**, run `cloider deploy --app`, or call the app's deploy hook | You can create an app from a git repository with the CLI too (`cloider app create --git github --repo acme/api`), and an AI agent can do all of the above through the [MCP server](/mcp/overview). Any of these can also be a [cron job](/apps/cron-jobs): an app with no port or address that runs on a schedule, such as every night at 03:00 UTC, and exits. ### Create your first app The fastest path is a folder on your computer and the CLI. In the folder of a Node.js, Python, Go or any other supported project, run: **Dashboard** 1. Open **Apps** in the sidebar and click **Create App**. 2. Choose where the code comes from: connect GitHub, GitLab or Bitbucket and pick a repository and branch, or drop a folder, a `.zip` file or an HTML file. 3. Check what Cloider detected (language, framework and port), the app name and the free subdomain. 4. Click **Create App**. The app page opens and shows the first build as it runs. **CLI** ```bash cd blog cloider deploy ``` ```text title="Output" Packaged 42 files (18.3 KB) Detected JavaScript Express ✓ Created app blog on port 3000 Linked . to blog in .cloider/app.json, later deploys from here update it. Downloading source for app a7c2mq... No Dockerfile in the source, looking for a buildpack... [buildpack output] App a7c2mq built and pushed as registry.cloider.com/acme-apps/blog-a7c2mq:1 Deploying the image... Waiting for the app to start... The app is up, watching it for 15s... The app is running. ✓ Deployed version 1 of blog https://blog-x7k2.cloider.app/ ``` Build output goes to stderr and the URL to stdout, so `URL=$(cloider deploy)` captures just the address. **Terraform** The Terraform provider doesn't create or deploy apps. Deploy the app with the dashboard or `cloider deploy`, then Terraform can route one of your domains to it with a `cloider_target`: ```hcl title="main.tf" resource "cloider_domain" "blog" { name = "acme-blog.cloider.app" } # The app's id comes from `cloider app list -o wide` resource "cloider_target" "blog" { domain = cloider_domain.blog.name app_id = "a7c2mq" } ``` **API** Creating an app through the API is a few calls: stage the source, upload it, run detection, then create the app. See [Deploy from your computer](/apps/deploy-from-your-computer#deploy-with-the-rest-api) for the full sequence. To list your apps: ```bash curl https://cloider.com/api/apps \ -H "Authorization: Bearer $CLOIDER_TOKEN" ``` ```json title="Response" [ { "hashed_id": "a7c2mq", "app_name": "blog", "app_label": "blog", "port": 3000, "start_command": "", "status": "Ready", "source_filename": "blog.zip", "source_size": 18734, "uploaded": true, "language": "JavaScript", "framework": "Express", "git_provider": "GitHub", "repo_full_name": "", "repo_url": "", "repo_branch": "", "auto_deploy": false, "git_connected": false, "image_ref": "registry.cloider.com/acme-apps/blog-a7c2mq:1", "dockerfile_args": [], "version": "1", "url": "https://blog-x7k2.cloider.app/", "domains": [{"domain_name": "blog-x7k2.cloider.app", "domain_type": "Subdomain"}], "created_at": "2026-10-20T09:12:44Z" } ] ``` **MCP** Ask your agent: ```text title="Prompt" Deploy the project in /home/me/blog to Cloider and tell me its URL. ``` It calls the `deploy_app` tool: ```json title="Tool call: deploy_app" {"dir": "/home/me/blog"} ``` For a guided walkthrough, see [Deploy your first app](/get-started/deploy-your-first-app). ### What does the app page show? Every app has its own page in the dashboard with these tabs: | Tab | What it does | Page | | --- | --- | --- | | **Overview** | Status, live version, address, port, start command, the deployed source or repository, what was detected, and the image | This page | | **Insights** | CPU and memory charts | [Insights](/apps/insights) | | **Builds** | Every build with its status, source, duration, logs and **Retry** for failed ones; **Deploy new version** or **Deploy latest commit** | [Builds and deployments](/apps/builds-and-deployments) | | **Environment** | Environment variables, build variables and build arguments | [Environment variables](/apps/environment-variables) | | **Settings** | Port, start command, and for a git app the branch and **Auto deploy on push** | [Settings](/apps/settings) | | **Logs** | The running app's output, live | [Logs](/apps/logs) | | **Terminal** | A shell inside the app's container | [Terminal and exec](/apps/terminal-and-exec) | The **Logs** and **Terminal** tabs are hidden while the app is paused, and **Terminal** only appears once a version has deployed. **Pause**, **Resume** and **Delete** are in the actions menu of each app on the Apps list. ### App statuses An app's status follows the build that is running on it, so the Apps list shows how far a deploy has got. | Status | Meaning | | --- | --- | | `Pending` | The source is still uploading | | `Queued` | The build is waiting its turn | | `Building` | The image is being built | | `Deploying` | The image is being applied to the platform | | `Starting` | The new container is starting; Cloider waits until it answers on the app's port and stays up | | `Ready` | The app is running and serving its live version | | `Failed` | The first build failed, so there is nothing to serve. Once an app has deployed a version, a failed build leaves it `Ready` on the previous version | | `Pausing` / `Paused` | The app is being stopped, or is stopped | | `Terminating` | The app is being deleted | See [Status values](/reference/status-values) for every product's statuses. ### How is an app billed? An app is billed for **compute only**: the CPU and memory its container uses, measured continuously and charged by the time it runs. There is no charge for builds, requests or traffic. - Usage is measured in **compute units**: for each moment, the larger of the CPU cores in use and the memory in use converted to cores at a fixed CPU to memory ratio. - Usage is metered continuously, charged for the time the app runs, and summed per day. The current rate is on the [pricing page](/reference/pricing). - A **paused** app has no running container, so it uses no compute and costs nothing. - Your app's images live in the account's apps registry project, which is billed like any [registry project](/container-registry/usage-and-billing), by the image data it stores. Cloider keeps only the two most recently deployed images per app. See an app's month to date cost and forecast with `cloider app usage blog`, on the [Usage](/account/usage) page, or in the [billing model](/concepts/billing-model). ### Limits | Limit | Value | | --- | --- | | App name | 2 to 64 characters: lowercase letters, digits and `-`, starting with a letter, not ending with `-` | | Source archive | 100 MB compressed, 500 MB extracted | | Files per deploy from the CLI | 2000 | | Build time | 30 minutes for the whole build | | Start time | 5 minutes for a new version to answer on its port | | Builds at once | One per app. A second deploy is refused until the first finishes | | Port | 1 to 65535 | | Start command | One line, up to 1000 characters | | Variables | 100 per list (environment, build variables, build arguments), 16 KB per value, 64 KB per list | | Instances | One container per app | | Build log kept | The last 64 KB of each build's output | See [Limits and quotas](/reference/limits-and-quotas) for every product. ### Who can manage apps? Every member of an account sees its apps. Members with read-only access can open the app's tabs and read its logs, builds and variables, but they can't create, deploy, change, pause or delete apps, open a terminal or run commands. An API token with read access has the same limits. See [Accounts and roles](/concepts/accounts-and-roles). ### Apps and other Cloider products - **Databases.** An app can connect to a [managed database](/databases/overview). Add the app as a [trusted source](/databases/trusted-sources) of the database and put the connection string in a secret environment variable such as `DATABASE_URL`. - **Object storage.** Store uploads and assets in an [S3 compatible bucket](/object-storage/overview) and give the app a [bucket access key](/object-storage/access-keys) through secret variables. - **Domains.** Route any path of a [custom domain](/domains/add-a-custom-domain) to an app with a [target](/domains/targets). - **Kubernetes.** When you outgrow one container, a [serverless Kubernetes cluster](/kubernetes/overview) runs any number of workloads with the standard Kubernetes API. ### Next steps Connect GitHub, GitLab or Bitbucket and deploy on every push. `cloider deploy` a folder, or upload it in the dashboard. Detection, Dockerfile or buildpacks, port and build arguments. Minimal projects that deploy as they are. --- ## Pause, resume and delete a Cloider app **Pausing** an app stops its container but keeps everything it needs to come back: its live version, its image, its environment variables, its domains and its build history. A paused app costs nothing for compute. **Resuming** starts it again on the same version. **Deleting** an app removes it for good, together with its images and, by default, its free subdomains. This page covers all three and what each one costs. Pausing a [cron job](/apps/cron-jobs) stops its schedule and a run going at that moment, and resuming it puts the schedule back. ### At a glance | | Pause | Resume | Delete | | --- | --- | --- | --- | | **Container** | Stopped | Started on the version it was serving | Removed | | **Image, variables, builds** | Kept | Kept | Removed | | **Free subdomain** | Kept, answers with an error | Serves the app again | Deleted by default | | **Custom domain** | Kept, answers with an error | Serves the app again | Kept; only its route to the app is removed | | **Compute cost** | None | Billed again | None | | **Allowed when** | The app is `Ready` and no build is running | The app is `Paused` | Any time, unless it is already being deleted | | **Statuses** | `Pausing` then `Paused` | `Starting` then `Ready` | `Terminating`, then gone from the list | Pause, resume and delete are in the actions menu (the three dots) of each app on the **Apps** list. ### Pause an app Pause an app you don't need right now: a staging app overnight, a demo between presentations, a side project. Its address keeps resolving but answers with an error until you resume it. **Dashboard** 1. Open **Apps** in the sidebar. 2. Click the three dots of the app and choose **Pause**. 3. In **Pause App**, click **Pause app**. The status goes `Pausing`, then `Paused`. The **Logs** and **Terminal** tabs are hidden while it is paused. **CLI** ```bash cloider app pause blog --wait ``` ```text title="Output" ✓ App blog is paused ``` Without `--wait` the command returns at once with `✓ App blog is pausing`. **Terraform** The Terraform provider can't pause apps. Use the dashboard, the CLI or the API. **API** ```bash curl -X POST https://cloider.com/api/apps/a7c2mq/pause \ -H "Authorization: Bearer $CLOIDER_TOKEN" ``` The response is the account's app list, with this app in `Pausing`. Poll `GET /api/apps/a7c2mq/status` until it answers `Paused`. **MCP** ```text title="Prompt" Pause the blog app. ``` ```json title="Tool call: pause_app" {"app": "blog", "wait": true} ``` #### What can I do with a paused app? While an app is paused: - **Can't**: deploy a new version, retry a build, change the port, start command, build arguments or build variables, open a terminal, run commands or read logs. Pushes to a git app's branch are skipped. - **Can**: read the **Overview**, **Insights** and **Builds** tabs, change environment variables (they apply when it resumes), manage its domains, and delete it. #### How is a paused app billed? A paused app has no running container, so it records no CPU or memory and costs nothing for compute. Its images stay in your account's apps registry project, which is billed by storage like any [registry project](/container-registry/usage-and-billing). See [Insights](/apps/insights) for the app's cost. ### Resume an app Resuming starts the app again on the version it was serving, with its current environment variables. Nothing is built. **Dashboard** 1. Open **Apps** in the sidebar. 2. Click the three dots of the paused app and choose **Resume**. 3. In **Resume App**, which says the version it comes back on, click **Resume app**. **CLI** ```bash cloider app resume blog --wait ``` ```text title="Output" ✓ App blog is ready ``` **Terraform** The Terraform provider can't resume apps. Use the dashboard, the CLI or the API. **API** ```bash curl -X POST https://cloider.com/api/apps/a7c2mq/resume \ -H "Authorization: Bearer $CLOIDER_TOKEN" ``` The app goes `Starting`, then `Ready`. **MCP** ```json title="Tool call: resume_app" {"app": "blog", "wait": true} ``` A git app with commits pushed while it was paused isn't updated by resuming. Click **Deploy latest commit** afterwards to catch up. ### Delete an app Deleting an app removes its container, its versions and builds, its environment variables and build variables, and its images. A GitLab or Bitbucket webhook Cloider created for it is removed too. **This can't be undone.** What happens to its domains: - **Free subdomains** claimed for the app are deleted with it by default, since they only exist for it. Keep them by unticking them in the dashboard, or with `--keep-domains`. - **Custom domains** are never deleted with an app. They only lose their route to the app; their DNS records and other targets stay. **Dashboard** 1. Open **Apps** in the sidebar. 2. Click the three dots of the app and choose **Delete**. 3. In **Delete App**, review what goes. Under **Delete this subdomain too**, untick a subdomain to keep it. Custom domains are listed as **kept, it just stops pointing at this app**. 4. Click **I want to delete the app**. 5. Type the app's name to confirm and click **Delete app**. **CLI** ```bash cloider app delete blog ``` The CLI asks you to confirm. In scripts, pass `--yes`. Keep the app's free subdomains with `--keep-domains`, and wait until the deletion is finished with `--wait`: ```bash cloider app delete blog --yes --keep-domains --wait ``` ```text title="Output" ✓ Deleted app blog ``` **Terraform** The Terraform provider can't delete apps. If Terraform manages a `cloider_target` that routes to the app, remove it from your configuration first, then delete the app with the dashboard, the CLI or the API. **API** `delete_domains` lists the subdomains to delete with the app. Only free subdomains are deleted; a custom domain named here only loses its route: ```bash curl -X DELETE https://cloider.com/api/apps/a7c2mq \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"delete_domains": ["blog-x7k2.cloider.app"]}' ``` The response is the app list, with this app in `Terminating`. `GET /api/apps/a7c2mq/status` answers `Terminated` once it is gone. **MCP** ```text title="Prompt" Delete the blog app but keep its cloider.app subdomain. ``` ```json title="Tool call: delete_app" {"app": "blog", "keep_domains": true, "wait": true} ``` `delete_app` is marked as a tool that deletes, so MCP clients that honor that ask you before running it. A build waiting in the queue when the app is deleted is `Canceled`. Billing for the app stops once it is deleted. ### Troubleshooting | Message | Cause and fix | | --- | --- | | **Only a running app can be paused!** | The app isn't `Ready`, for example it is `Failed` or still deploying | | **This app is building, wait for that build to finish before pausing it!** | Wait for the build, then pause | | **Only a paused app can be resumed!** | The app isn't `Paused`. If it is `Pausing`, wait a moment | | **This app is already being deleted!** | The app is `Terminating` | | **You have read-only access to this account, you can not pause apps!** | Ask an account admin for write access | | The address shows an error after resuming | The app is still starting. Wait for `Ready`, then check its [logs](/apps/logs) | ### Next steps - [Insights](/apps/insights): what an app costs per day. - [Billing model](/concepts/billing-model): how paused and deleted resources are billed. - [Delete a domain](/domains/delete-a-domain): remove a custom domain you no longer need. --- ## Change a Cloider app's port, start command and branch The **Settings** tab holds what an app is run with: the **Port** it listens on and its **Start command**, plus, for an app created from a repository, the **Branch** it deploys from and **Auto deploy on push**. Saving applies the change to the running app in the cheapest way that can carry it. This page explains each setting, what saving does, and how to change them from every tool. ### At a glance | Setting | Applies to | Saving does | | --- | --- | --- | | **Port** | Every app | Redeploys the running version on the new port, no rebuild | | **Start command** | Apps built by buildpacks | Rebuilds the source the app is serving, as a new version | | **Branch** | Git apps | Nothing until the next push or **Deploy latest commit** | | **Auto deploy on push** | Git apps | Turns deploys on push on or off | | **Image**, **Tag**, registry login | Image apps | Checks them with the registry, then deploys when the tag points at a different image | | **Deploy hook** | Image apps | Shows the URL that deploys the app, or replaces it with **Regenerate** | - Port and start command changes are refused while a build is running or the app is paused (the **Save changes** button says why: **A build is already running** or **Resume the app to change its settings**). - On an app that has never deployed, the new values are saved and used by its next build. - The app's name, environment variables and domains are elsewhere: see [Environment variables](/apps/environment-variables) and [Domains](/apps/domains). An app can't be renamed. ### Change the port The port is where Cloider sends the app's traffic, and the container receives it as the `PORT` environment variable. Change it when your app listens on a different port than the one detected, for example a Dockerfile image that always listens on `80`. Saving a new port redeploys the image the app is already running: the container restarts with the new `PORT`, and traffic moves to the new port. The app goes `Deploying`, then `Starting`, then `Ready` once something answers on the new port. No new build or version is created. :::warning[A wrong port takes the app down] If nothing listens on the new port within 5 minutes, the app goes `Failed` and stops answering, since traffic now goes to a port nobody serves. Put the right port back on **Settings** to recover. An app that reads `$PORT` (every buildpack app, and Dockerfile apps written to) follows the change on its own. ::: **Dashboard** 1. Open the app and go to the **Settings** tab. 2. Change **Port** (1 to 65535). 3. Click **Save changes**. The toast says **Your app is redeploying on its new port.** **CLI** ```bash cloider app update web --port 80 --wait ``` ```text title="Output" ✓ Saved, redeploying web ``` **Terraform** The Terraform provider can't change app settings. Use the dashboard, the CLI or the API. **API** Send both settings; `start_command` is optional and empty means the project decides: ```bash curl -X PUT https://cloider.com/api/apps/w4d8hz/settings \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"port": 80, "start_command": ""}' ``` ```json title="Response" {"applied": "redeploy"} ``` `applied` says what happened: `redeploy` (new port), `build` (new start command, which also carries a new port), or `none` (nothing changed, or the app hasn't deployed yet). **MCP** ```text title="Prompt" The web app listens on port 80. Update its port and wait until it is back up. ``` ```json title="Tool call: update_app_settings" {"app": "web", "port": 80, "wait": true} ``` ### Change the start command The start command is what your app runs when it starts. Where it goes depends on how the app's image is made: - **Buildpack build:** Cloider writes it into the source as a `Procfile` line, `web: `, so it becomes part of the image. Saving a new one **rebuilds** the source the app is serving: a new version is built and deployed, and the **Builds** tab opens on it. - **Your own `Dockerfile`, or an [image you built](/apps/deploy-an-image):** the container runs it with `sh -c` in place of the image's `ENTRYPOINT` and `CMD`, so the image needs a shell. Saving a new one only **redeploys** the image the app is running, in a few seconds. - Leave it empty to let the project decide, through its own `Procfile`, its language's convention (such as the `start` script of `package.json`), or the image's own `ENTRYPOINT` and `CMD`. - One line, at most 1000 characters. Use `$PORT` for the port: `gunicorn app:app --bind 0.0.0.0:$PORT`. - A static site has no command; setting one makes Cloider build the source with buildpacks instead. **Dashboard** 1. Open the app and go to the **Settings** tab. 2. Type the command in **Start command**, for example `node dist/server.js`, or clear it. 3. Click **Save changes**. A buildpack app's toast says **Your app is rebuilding to start on the new command.**, an image or `Dockerfile` app's **Your app is redeploying with its new settings.** **CLI** ```bash cloider app update api --start-command "gunicorn app:app --bind 0.0.0.0:\$PORT" --wait ``` ```text title="Output" ✓ Saved, building api again ``` An image or `Dockerfile` app answers `✓ Saved, redeploying api` instead. Clear it with `--start-command ""`. **Terraform** The Terraform provider can't change app settings. Use the dashboard, the CLI or the API. **API** ```bash curl -X PUT https://cloider.com/api/apps/k3m9tw/settings \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"port": 8080, "start_command": "gunicorn app:app --bind 0.0.0.0:$PORT"}' ``` ```json title="Response" {"applied": "build"} ``` `applied` is `redeploy` for an image or `Dockerfile` app. **MCP** ```json title="Tool call: update_app_settings" {"app": "api", "start_command": "gunicorn app:app --bind 0.0.0.0:$PORT", "wait": true} ``` ### Change the branch or auto deploy A git app has a **Repository** section with the **Branch** every deploy is taken from and the **Auto deploy on push** switch. Neither deploys anything by itself: the next push, or **Deploy latest commit** on the **Builds** tab, uses them. A branch that doesn't exist isn't checked when you save; the next deploy fails with a message naming it. ```bash cloider app git api --branch release cloider app git api --auto-deploy=false ``` On GitLab and Bitbucket, turning auto deploy on also adds the webhook that delivers pushes, which needs the Maintainer role (GitLab) or admin access (Bitbucket); without it, auto deploy switches itself back off. If the connection behind the repository is gone or suspended, the section says so and the app can't deploy from git. See [Deploy from git](/apps/deploy-from-git#change-the-branch-or-turn-auto-deploy-off) for every tool. ### Change an image app's image, tag or login An app [created from a Docker image](/apps/deploy-an-image) has an **Image** section below the start command, with the **Image**, the **Tag** and **The image is private** switch for its login. Saving checks them with the registry first and refuses what it can't pull, then deploys when the tag now points at a different image. An empty **Token or password** keeps the saved one for the same username. ```bash cloider app image admin --tag v4 cloider app image admin --no-login ``` The **Deploy hook** section below it shows the URL that deploys the app when your CI calls it. See [Deploy from CI with a deploy hook](/apps/deploy-an-image#deploy-from-ci-with-a-deploy-hook). ### Change a cron job's schedule A [cron job](/apps/cron-jobs) has a **Schedule** instead of a port. Saving a new one redeploys the job on it in a few seconds, on the same version. See [Change the schedule](/apps/cron-jobs#change-the-schedule). ### Where are pause and delete? **Pause**, **Resume** and **Delete** are in the actions menu (the three dots) of each app on the **Apps** list, and in the CLI as `cloider app pause`, `resume` and `delete`. See [Pause, resume and delete](/apps/pause-resume-and-delete). ### Troubleshooting | Message | Cause and fix | | --- | --- | | `App port must not be empty` / `App port must be between 1 and 65535` | Enter a valid port | | `Start command must be a single line` | Remove line breaks; chain commands with `&&` or move them into a script | | `Start command must be at most 1000 characters` | Move the command into a script in your source and run that | | **This app is building, wait for that build to finish before changing its settings!** | Wait for the running build | | **This app is paused. Resume it before deploying a new version!** | Resume the app, then save | | **The settings were saved but the app could not be redeployed!** | The values are saved; save again or deploy a new version to apply them | | The app went `Failed` after a port change | Nothing answers on the new port. Set the port your app listens on | | A new start command fails with `sh: not found` | The image has no shell to run it with, like a distroless image. Clear the start command and change the image's `CMD` instead | | `nothing to change, give --port or --start-command` (CLI) | Pass at least one of the two flags | ### Next steps - [Build configuration](/apps/build-configuration): how the port and start command are used during builds. - [Builds and deployments](/apps/builds-and-deployments): follow the rebuild a start command change starts. - [Pause, resume and delete](/apps/pause-resume-and-delete). --- ## Supported languages and frameworks for Cloider apps Cloider runs any language that fits in a container: with a `Dockerfile` at the root of your project, Cloider builds it as written. Without one, Cloider recognizes **Node.js, Python, Go, Java, Scala, PHP, Ruby and .NET** projects and builds them with Cloud Native Buildpacks, and serves plain HTML as a **static site**. This page lists the files that select each language and gives a minimal project for each that deploys as it is. ### At a glance | Language | Recognized by a file at the project root | Built by | | --- | --- | --- | | Node.js (JavaScript, TypeScript) | `package.json` | Buildpacks | | Python | `requirements.txt`, `pyproject.toml`, `Pipfile`, `Pipfile.lock`, `poetry.lock`, `uv.lock`, `setup.py`, `setup.cfg`, `runtime.txt`, `.python-version` | Buildpacks | | Go | `go.mod` | Buildpacks | | Java, Kotlin (Maven or Gradle) | `pom.xml`, `mvnw`, `build.gradle`, `build.gradle.kts`, `settings.gradle`, `settings.gradle.kts`, `gradlew`, `system.properties` | Buildpacks | | Scala | `build.sbt` | Buildpacks | | PHP | `composer.json`, `composer.lock` | Buildpacks | | Ruby | `Gemfile`, `Gemfile.lock` | Buildpacks | | .NET (C#, F#, Visual Basic) | A `.csproj`, `.fsproj`, `.vbproj`, `.sln` or `.slnx` file, or a single `.cs` file | Buildpacks | | Static site | One or more `.html` / `.htm` files and none of the files above | Static web server | | Anything else (Rust, Elixir, Deno, Bun, C++ and more) | `Dockerfile` | Your Dockerfile | The buildpacks are Heroku's open source Cloud Native Buildpacks (`heroku/builder:26`). They pick the runtime version, install your dependencies and run your build step following each language's usual conventions, described below. For everything a buildpack doesn't cover, add a `Dockerfile`: it always takes priority. See [Build configuration](/apps/build-configuration) for how the strategy is chosen. ### Rules every app follows Whatever the language, a project deploys cleanly when it: 1. **Listens on `$PORT`**, on all interfaces (`0.0.0.0`). Cloider sets `PORT` to the app's port and only sends traffic there. 2. **Says how it starts**: a `Procfile` with a `web:` line, the app's **Start command**, a `start` script (Node.js), or a Dockerfile `CMD`. 3. **Commits its lockfile** (`package-lock.json`, `poetry.lock`, `Gemfile.lock`, `composer.lock`, `go.sum`), so the build installs exactly the dependencies you tested. 4. **Leaves dependencies out of the upload** (`node_modules/`, `.venv/`, `vendor/`): the build installs them. See [What is uploaded?](/apps/deploy-from-your-computer#what-is-uploaded). 5. **Reads configuration from environment variables**, set on the app's **Environment** tab. See [Environment variables](/apps/environment-variables). ### Node.js Cloider builds a project with a `package.json` at its root with the Node.js buildpack. It installs dependencies with the package manager your lockfile belongs to, runs your `build` script if there is one, and starts the app with the `start` script unless a `Procfile` or a start command says otherwise. Pick the Node.js version with `engines.node`. ```text blog/ package.json package-lock.json server.js ``` ```json title="package.json" { "name": "blog", "version": "1.0.0", "private": true, "scripts": { "start": "node server.js" }, "engines": { "node": "22.x" }, "dependencies": { "express": "^4.21.2" } } ``` ```js title="server.js" const express = require("express"); const app = express(); const port = process.env.PORT || 3000; app.get("/", (req, res) => res.send("Hello from Cloider")); app.listen(port, "0.0.0.0", () => console.log(`Listening on ${port}`)); ``` Run `npm install` once to create `package-lock.json`, then `cloider deploy`. Framework notes: - **Next.js**: `"build": "next build"` and `"start": "next start -p $PORT"`. Variables named `NEXT_PUBLIC_*` are inlined at build time, so set them before the build: a plain environment variable or a build variable both reach a buildpack build. - **Nuxt**: `"build": "nuxt build"` and `"start": "node .output/server/index.mjs"`. Nuxt's server reads `PORT`. - **NestJS**, **Fastify**, **Koa**, **Hono on Node**: start the compiled entry file and listen on `process.env.PORT`. - **TypeScript**: compile in the `build` script (`tsc`) and start the compiled JavaScript. ### Python A project with `requirements.txt`, `pyproject.toml` (with Poetry or uv), a `Pipfile` or another Python file from the table is built with the Python buildpack. Pin the Python version in `.python-version`. Python web apps need to say how to start, so add a `Procfile` (or set the app's start command) that runs a production server on `$PORT`. #### Flask ```text api/ app.py requirements.txt .python-version Procfile ``` ```python title="app.py" from flask import Flask app = Flask(__name__) @app.get("/") def index(): return {"message": "Hello from Cloider"} ``` ```text title="requirements.txt" flask==3.1.0 gunicorn==23.0.0 ``` ```text title=".python-version" 3.13 ``` ```text title="Procfile" web: gunicorn app:app --bind 0.0.0.0:$PORT ``` #### FastAPI ```text title="requirements.txt" fastapi==0.115.6 uvicorn==0.34.0 ``` ```text title="Procfile" web: uvicorn main:app --host 0.0.0.0 --port $PORT ``` #### Django ```text title="Procfile" web: gunicorn mysite.wsgi --bind 0.0.0.0:$PORT ``` For Django, read `SECRET_KEY`, `DEBUG` and `ALLOWED_HOSTS` from environment variables, and include the app's domain in `ALLOWED_HOSTS` (for example `blog-x7k2.cloider.app`). Run migrations from the [terminal](/apps/terminal-and-exec): ```bash cloider app exec api python manage.py migrate ``` ### Go A project with `go.mod` at its root is compiled by the Go buildpack, using the Go version from the `go` line of `go.mod`. Keep the server's `main` package at the module root. ```text api/ go.mod main.go ``` ```text title="go.mod" module example.com/api go 1.23 ``` ```go title="main.go" package main import ( "fmt" "log" "net/http" "os" ) func main() { port := os.Getenv("PORT") if port == "" { port = "8080" } http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello from Cloider") }) log.Printf("Listening on %s", port) log.Fatal(http.ListenAndServe(":"+port, nil)) } ``` If the module has several `main` packages, set the app's start command to the one to run. ### Java A Maven (`pom.xml`) or Gradle (`build.gradle`, `build.gradle.kts`) project is built with the JVM buildpacks. Pick the Java version in `system.properties` (for example `java.runtime.version=21`). Give the jar a fixed name and start it from a `Procfile`, which makes the start command independent of your version number. #### Spring Boot with Maven ```xml title="pom.xml (excerpt)" app org.springframework.boot spring-boot-maven-plugin ``` ```text title="system.properties" java.runtime.version=21 ``` ```properties title="src/main/resources/application.properties" server.port=${PORT:8080} ``` ```text title="Procfile" web: java -jar target/app.jar ``` Kotlin projects built with Gradle work the same way; point the `Procfile` at the jar Gradle builds. ### Scala An sbt project (`build.sbt`) is built with the Scala buildpack. Add a `Procfile` with the command that starts your server on `$PORT`, for example the script `sbt-native-packager` stages. ### PHP A project with `composer.json` is built with the PHP buildpack, which installs your Composer dependencies. Commit `composer.lock`. The smallest app that deploys as is uses PHP's built-in server from a `Procfile`; for production traffic, use the buildpack's web server or a Dockerfile with a PHP web server. ```text site/ composer.json composer.lock public/index.php Procfile ``` ```json title="composer.json" { "require": { "php": "^8.3" } } ``` ```php title="public/index.php" "Hello from Cloider"); var port = Environment.GetEnvironmentVariable("PORT") ?? "8080"; app.Run($"http://0.0.0.0:{port}"); ``` If you want full control over the SDK and runtime images, use a Dockerfile: ```dockerfile title="Dockerfile" FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY . . RUN dotnet publish -c Release -o /out FROM mcr.microsoft.com/dotnet/aspnet:8.0 WORKDIR /app COPY --from=build /out . ENTRYPOINT ["dotnet", "Api.dll"] ``` ### Static sites and single page apps A folder of HTML, CSS, JavaScript and images with no project files is served as a static site, with no build step. Put `index.html` at the root: ```html title="index.html" Acme

Hello from Cloider

``` You can even drop a single HTML file on **Create App** in the dashboard. A site built with a framework (React, Vue, Svelte, Astro, Vite) has a `package.json`, so it is built by the Node.js buildpack, which then needs a `start` script that serves the build output: ```json title="package.json (excerpt)" { "scripts": { "build": "vite build", "start": "serve -s dist -l tcp://0.0.0.0:$PORT" }, "dependencies": { "serve": "^14.2.4" } } ``` Or build it in a Dockerfile and serve the files with an unprivileged nginx on port 8080 (set the app's port to `8080`): ```dockerfile title="Dockerfile" FROM node:22-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM nginxinc/nginx-unprivileged:alpine COPY --from=build /app/dist /usr/share/nginx/html ``` ### Any other language with a Dockerfile A `Dockerfile` runs anything that builds for `linux/amd64`. For example a Rust web server: ```dockerfile title="Dockerfile" FROM rust:1.83 AS build WORKDIR /src COPY . . RUN cargo build --release FROM debian:bookworm-slim # the crate (and its binary) is named api COPY --from=build /src/target/release/api /usr/local/bin/api CMD ["api"] ``` Read `PORT` in your program, or set the app's port to the one it listens on. ### What if my language isn't detected? If the build log ends with `nothing to build for : no Dockerfile, no buildpack matched, and not a static site (an .html page with no project files)`, Cloider found nothing it could build at the root of your source. Check that: - The project files are at the root of what you uploaded, not one folder down next to other folders. (A zip with one single top folder is fine.) - The file names match the table above exactly. - Or add a `Dockerfile`, which works for any language. ### Next steps - [Build configuration](/apps/build-configuration): the build strategies, port and start command in detail. - [Deploy from your computer](/apps/deploy-from-your-computer): deploy one of these examples with `cloider deploy`. - [Full stack app with PostgreSQL](/guides/full-stack-app-with-postgresql): an app with a managed database. --- ## Open a shell in a Cloider app and run commands You can work inside a running app's container in two ways: the **Terminal** tab and `cloider app shell` open an interactive shell, like SSH, and `cloider app exec` runs one command and returns its output and exit code, which is what scripts, CI jobs and AI agents need. Typical uses are database migrations, one-off scripts, checking environment variables and debugging. This page covers both, with their limits. A [cron job](/apps/cron-jobs) has nothing running between runs, so it has no **Terminal** tab: a running run has a **Terminal** button on the **Runs** tab, and `cloider app shell` and `cloider app exec` open a container with its image and environment that is started for them. ### At a glance | | Terminal tab / `cloider app shell` | `cloider app exec` / API / MCP | | --- | --- | --- | | **For** | People, interactively | Scripts, CI, AI agents | | **Runs** | `bash -l`, or `sh -l` when the image has no bash | One command, directly or with `sh -c` | | **Output** | A terminal | Separate stdout and stderr, and the exit code | | **Input** | Your keyboard | Optional stdin, up to 2 MiB | | **Time limit** | Until you exit; idle for 10 minutes ends the terminal | 10 minutes by default, up to 1 hour | | **Needs** | A deployed, running app; read and write access | Same | Both run inside the app's current container, with its files and environment variables, as the container's user. Anything you change there (files, installed packages) is lost when the container restarts or a new version deploys, so make lasting changes in your source and deploy them. ### Open a terminal **Dashboard** 1. Open the app and go to the **Terminal** tab. It appears once a version has deployed, and is hidden while the app is paused. 2. Wait for **Connecting to the app...**. The terminal starts on demand, which can take a few seconds the first time. 3. You get a shell in the app's working directory. Type `exit` to end the session. **CLI** ```bash cloider app shell blog ``` ```text title="Output" Connected, exit the shell to end the session. cloider@blog:/workspace$ node --version v22.11.0 cloider@blog:/workspace$ exit ``` `cloider app shell` needs a real terminal on both ends. In scripts, use `cloider app exec`. **Terraform** The Terraform provider can't open terminals. Use the dashboard or the CLI. **API** The interactive terminal is a browser session the dashboard opens through `GET /api/apps//terminal`. For automation, run single commands with `POST /api/apps//exec` instead, see [Run a command with exec](#run-a-command-with-exec). **MCP** An MCP client has no terminal. Agents run commands one at a time with the `app_exec` tool, see [Run a command with exec](#run-a-command-with-exec). The prompt shows the app's name as the host name: Cloider sets the container's hostname to the app name. #### When does the terminal stop? The terminal runs next to your app only while someone uses it. The dashboard's tab keeps it awake while it is open, and after 10 minutes with nobody using it, it goes to sleep until it is next opened. Closing the tab or leaving it ends your session. The terminal isn't part of your app's usage and isn't billed. ### Run a command with exec `cloider app exec` runs one command in the app's container and exits with the command's exit code. Stdout and stderr stay separate and nothing is a terminal, so the output is exactly what the command printed. **Dashboard** The dashboard runs commands through the **Terminal** tab. For one-off commands from a script, use the CLI, the API or MCP. **CLI** ```bash cloider app exec blog uptime cloider app exec blog ls -la cloider app exec blog printenv NODE_ENV ``` How arguments are run: - **Several arguments** run as they are, without a shell. This also works in images that have no shell. - **One quoted argument with spaces** is a command line, run with `sh -c`, so pipes, `&&` and `$VARS` work. ```bash cloider app exec blog "ps aux | grep node" cloider app exec blog 'echo $DATABASE_URL | cut -d@ -f2' ``` Flags for `exec` go **before** the app. Everything after the app belongs to the command, and `--` may separate the two: ```bash cloider app exec --timeout 30m blog -- npm run migrate ``` Send a local file or script in with `-i` (stdin, up to 2 MiB): ```bash cloider app exec -i blog -- sh -c 'cat > /tmp/seed.json' < seed.json cloider app exec -i blog sh < setup.sh ``` Get one JSON object, for scripts and agents. The exit code is still set: ```bash cloider app exec -o json blog -- node -e 'console.log(process.version)' ``` ```json title="Output" {"exit_code": 0, "stdout": "v22.11.0\n", "stderr": "", "timed_out": false} ``` **Terraform** The Terraform provider can't run commands in apps. Use the CLI, the API or MCP. **API** Send the command as an argument list. `stdin` is base64 and optional, `timeout` is in seconds (default 600, at most 3600): ```bash curl -N -X POST https://cloider.com/api/apps/a7c2mq/exec \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"command": ["sh", "-c", "node --version && echo done >&2"], "timeout": 60}' ``` The answer is a server-sent events stream. `stdout` and `stderr` events carry base64 encoded output, then one of `exit` (the exit code), `timeout` (the timeout in seconds) or `error` (a message) ends it, followed by `end`: ```text title="Response" : open event: stdout data: djIyLjExLjAK event: stderr data: ZG9uZQo= event: exit data: 0 event: end data: 1 ``` Closing the connection stops the command. The command is limited to 64 KiB of arguments and 2 MiB of stdin. **MCP** Ask your agent: ```text title="Prompt" Run the database migrations of the api app with "python manage.py migrate" and tell me if they worked. ``` It calls the `app_exec` tool, which answers with the exit code and the output: ```json title="Tool call: app_exec" {"app": "api", "command": "python manage.py migrate", "timeout_seconds": 600} ``` ```json title="Result" {"exit_code": 0, "stdout": "Operations to perform:\n Apply all migrations: ...\n", "stderr": "", "timed_out": false} ``` A `command` with spaces runs with `sh -c`. `stdin` passes text to the command. #### Exit codes | Code | Meaning | | --- | --- | | `0` | The command succeeded | | Any other | The command's own exit code | | `124` | `--timeout` stopped the command | | `126` | The command exists but can't run (permissions) | | `127` | The command doesn't exist in the image | The CLI's own error codes (`1` to `5`) can also be a command's. With `-o json`, the JSON is only printed when the command ran, which tells the two apart. Like `kubectl exec`, a command that prints nothing can keep running in the container after a timeout or Ctrl-C. ### Common tasks ```bash # Run database migrations cloider app exec api python manage.py migrate cloider app exec --timeout 30m web -- npx prisma migrate deploy cloider app exec shop -- php artisan migrate --force cloider app exec store -- bundle exec rails db:migrate # Check what the app sees cloider app exec blog printenv cloider app exec blog -- cat /etc/os-release # Test a dependency from inside the app cloider app exec blog "wget -qO- http://localhost:\$PORT/health" ``` ### Troubleshooting | Message | Cause and fix | | --- | --- | | **This app has not been deployed yet!** | There is no container until the first build succeeds. Check the **Builds** tab | | **This app is paused. Resume it to open a terminal!** / **...to run a command!** | Resume the app, see [Pause, resume and delete](/apps/pause-resume-and-delete) | | `Your app isn't running right now, so there is nothing to open a terminal in. Check the Logs tab, then reconnect.` | The container is restarting or crashing. Check the [logs](/apps/logs) | | `This app's image has no shell (e.g. a distroless image), so a terminal can't be opened in it. Use the Logs tab instead.` | Distroless and scratch images have no `sh`. Use `cloider app exec` with a full argument list, or add a shell to the image | | **Terminals need a Read & Write API token!** | The CLI's token is read-only. Log in again with Read & Write access | | **You have read-only access to this account, you can not run commands!** | Your role in the account is read-only | | **The terminal could not be started, try again in a moment!** | The terminal didn't wake in time. Try again | | **This app has no running container right now!** | The app is between containers (a restart or a rollout). Try again in a few seconds | | `127` exit code | The command isn't installed in the image. Buildpack images don't include every tool, so check with `cloider app exec "command -v "` | ### Next steps - [Logs](/apps/logs): what the app prints. - [Environment variables](/apps/environment-variables): configure the app instead of editing files in the container. - [MCP tools](/mcp/tools): every tool an AI agent can call. --- ## Troubleshoot Cloider app builds and deploys Most app problems fall into one of four phases: getting the **source** to Cloider, **building** it, **starting** it, or **running** it. Each phase has its own messages, and the build output on the **Builds** tab almost always says what went wrong. This page starts with a quick checklist, then goes through every message by phase with its cause and fix. ### Quick checklist Run through these first; they fix most failed deploys: 1. **Read the build output.** Open the **Builds** tab and click **Logs** on the failed build, or run `cloider app build logs `. The last lines say what failed. For a start failure, read what follows `--- app output ---`. 2. **Check the port.** The app must listen on `$PORT` (or the app's **Port** must match what it listens on), on `0.0.0.0`. 3. **Check how it starts.** A buildpack app needs a `start` script, a `Procfile` or a **Start command**; a `Dockerfile` needs a `CMD` or `ENTRYPOINT`, or a **Start command** to run in their place. 4. **Check its variables.** A crash right after start is often a missing environment variable such as `DATABASE_URL`. 5. **Run it locally the same way**: `PORT=8080 npm start`, `PORT=8080 gunicorn app:app --bind 0.0.0.0:$PORT`, or `docker build . && docker run -e PORT=8080 -p 8080:8080 `. ```mermaid flowchart TD A[Deploy failed] --> B{Build message} B -- Image build failed --> C[Read the build output:
Dockerfile or buildpack error] B -- The app did not start in time --> D[Port mismatch or
listening on localhost] B -- The app started and then stopped --> E[Read the app output:
crash, missing variable,
no start command] B -- The app could not be started --> F[Bad CMD or entrypoint] B -- The source upload did not finish --> G[Deploy again] ``` ### Source and upload problems | Message | Cause | Fix | | --- | --- | --- | | `more than 2000 files to deploy, list what isn't needed in .cloiderignore` | Too many files, usually dependencies | Add `node_modules/`, `.venv/`, `vendor/`, build output and tests to `.gitignore` or `.cloiderignore` | | `the archive is 140 MB, over the 100 MB limit; ...` | The zip is over 100 MB | Same, and move large media to [object storage](/object-storage/overview) | | **Only up to 2000 files can be uploaded, zip your project first.** | The dashboard upload doesn't read `.gitignore` | Delete dependency folders before dropping the folder, or use `cloider deploy` | | **The app source exceeded the allowed size of 100MB!** | The archive is over 100 MB | Make it smaller | | **This upload has expired, upload your app again!** | Over 2 hours between upload and **Create App** | Start the upload again | | **Your app source is no longer available, upload it again!** | The staged upload expired or was already used | Upload again | | `We could not read your app's source` | Detection couldn't open the archive | Create the app anyway with your own name and port, or check the zip opens locally | | `detection took too long, try again` | Detection is queued behind other work | Run the command again, or pass `--name` and `--port` | | `the source archive contains absolute or parent paths` (build output) | The zip has `/...` or `../` entries | Re-create the zip from inside the project folder | | `the extracted source is 612MB, over the 500MB limit` (build output) | The zip expands past 500 MB | Remove generated or binary files | | `the source archive is empty` (build output) | Nothing was zipped | Check what you zipped | | `The source upload did not finish` | The upload was interrupted or never confirmed | Deploy again | See [Deploy from your computer](/apps/deploy-from-your-computer). ### Git problems | Message | Cause | Fix | | --- | --- | --- | | **No repositories to show.** | The connection can't see the repository | GitHub: grant the installation access to it. GitLab: you need Reporter or higher. Bitbucket: you need read access | | `We could not read acme/api at main. The branch may be gone, or the repository may have no commits yet.` | Empty repository or wrong branch | Push a commit, or pick an existing branch | | `acme/api is over the 100MB limit.` | The repository archive is over 100 MB | Remove large files, or deploy a subfolder with `cloider deploy` | | `GitHub no longer gives us acme/api at main` (build) | Branch deleted or renamed, or access removed | Fix the branch on **Settings**, or grant access again | | **This app's GitHub connection is gone. Reconnect GitHub and try again.** | The connection was suspended or removed | See [What happens when I disconnect a git account?](/apps/deploy-from-git#what-happens-when-i-disconnect-a-git-account) | | Pushes don't deploy | Auto deploy is off, the push was to another branch, a build was running, or the app is paused | Check **Settings**, then click **Deploy latest commit**. On GitLab and Bitbucket, see [auto deploy needs a webhook](/apps/deploy-from-git#auto-deploy-on-gitlab-and-bitbucket) | ### Build failures The build message is **Image build failed** and the build output ends with the reason. #### Nothing to build ```text title="Build output" nothing to build for a7c2mq: no Dockerfile, no buildpack matched, and not a static site (an .html page with no project files) ``` Cloider found no `Dockerfile`, no project file a buildpack recognizes, and no HTML. Usually the project is one folder down, next to other folders, so its files aren't at the root. Fix it by deploying the project folder itself (`cloider deploy ./backend`), moving the files to the root, or adding a `Dockerfile` at the root. See [Supported languages](/apps/supported-languages). #### Dockerfile build failed ```text title="Build output" build failed for registry.cloider.com/acme-apps/web-w4d8hz:5 ``` The lines above it are the `docker build` output. Common causes: - **A file isn't there.** `COPY` failed because the file is excluded by `.dockerignore`, `.gitignore` or `.cloiderignore`, or isn't in the repository. Remember that the build context is the source root. - **A build argument is missing.** An `ARG` with no default that your build needs. Set it as a [build argument](/apps/environment-variables#build-arguments-and-build-variables); the dashboard lists the `ARG`s your `Dockerfile` declares under **Declared in your Dockerfile**. - **A private package can't be installed.** Pass the token as a secret build argument and mount it with `RUN --mount=type=secret,id=NPM_TOKEN,env=NPM_TOKEN npm ci`. Environment variables don't reach a Dockerfile build. - **Wrong platform.** Images build for `linux/amd64`. A base image or binary that only exists for `arm64` fails. - **It works on my machine.** Run `docker build --platform linux/amd64 .` locally from the same folder to reproduce. #### Buildpack build failed ```text title="Build output" buildpack build failed for registry.cloider.com/acme-apps/blog-a7c2mq:3 ``` The buildpack's own output above says why. Common causes: - **No lockfile, or a lockfile out of date.** Commit `package-lock.json`, `poetry.lock`, `Gemfile.lock` or `composer.lock`, generated from the current dependency list. - **A runtime version that doesn't exist.** Check `engines.node`, `.python-version` or `system.properties`. - **The build script failed.** For Node.js, the `build` script runs during the build: run `npm run build` locally. - **Private packages.** Set the token as a secret [build variable](/apps/environment-variables#build-arguments-and-build-variables). - **Dependencies uploaded from your machine.** A `node_modules/` or `.venv/` built on macOS can break a Linux build. Leave them out of the upload. #### The build did not finish in time The whole build is limited to 30 minutes. Speed it up: commit lockfiles so the dependency cache is reused, avoid building assets you don't serve, and use a multi-stage `Dockerfile` with small final images. Use **Retry** for a one-off slowness. ### Start failures The image built, was deployed, and then didn't come up. The previous version, if there is one, keeps serving. #### The app did not start in time ```text title="Build output" Waiting for the app to start... The app did not start within 300s. --- app output --- Server listening on http://127.0.0.1:3000 ``` Nothing accepted connections on the app's port within 5 minutes. Causes, most common first: 1. **Port mismatch.** The app listens on another port than the app's **Port**. Listen on `$PORT`, or change the port on [Settings](/apps/settings). A Dockerfile image with a fixed port (nginx on 80, a Java server on 8080) needs the app's port set to that port. 2. **Listening on localhost.** `127.0.0.1` or `localhost` isn't reachable from outside the container. Listen on `0.0.0.0`: `app.listen(port, "0.0.0.0")`, `--bind 0.0.0.0:$PORT`, `--host 0.0.0.0`. 3. **Slow start.** Migrations or cache warm up before listening take more than 5 minutes. Start the server first, or run the slow work separately with [`cloider app exec`](/apps/terminal-and-exec). 4. **Development server.** A dev server (`npm run dev`, `flask run`) may bind localhost only. Use the production command. #### The app started and then stopped ```text title="Build output" The app started and then stopped. --- app output --- Error: Missing required environment variable DATABASE_URL ``` The container exited or crashed within its first seconds. The app output under the marker is the last 100 lines it printed. Usual causes: - **A missing environment variable** or wrong credentials. Set it on the **Environment** tab. - **A dependency that isn't installed.** `Cannot find module`, `ModuleNotFoundError`: add it to your dependency file, not only to your local environment. - **No start command.** A buildpack image with nothing to run: add a `Procfile` with a `web:` line, a `start` script, or set the **Start command** on [Settings](/apps/settings). - **The process exits on its own.** A script that finishes isn't a server. The app must keep running and serve HTTP. - **A crash at startup** such as a failed database connection. Make sure the app is a [trusted source](/databases/trusted-sources) of the database it connects to. #### The app could not be started The container couldn't be created at all, usually because its command doesn't exist in the image: a `CMD` naming a binary that isn't there, a script without execute permission, or a start command with a typo. Check the `CMD`, `ENTRYPOINT` or start command. #### The app image could not be pulled The new image couldn't be pulled for 60 seconds. This is a platform side problem: **Retry** the build. ### Runtime problems | Symptom | Cause and fix | | --- | --- | | The address shows an error page or `502` | The app isn't answering on its port: it crashed, or it is restarting. Check the [Logs](/apps/logs) tab. A crashed container is restarted automatically | | The app works, then errors after a while | Look for crashes or memory growth on [Logs](/apps/logs) and [Insights](/apps/insights) | | The address doesn't load at all | The app has no domain (**No domain connected** on Overview), the app is paused, or a custom domain's nameservers or its `A` or `CNAME` record aren't set. See [Domains](/apps/domains) | | New environment variables aren't picked up | Saving the **Environment** tab restarts the app; build arguments and build variables need the rebuild that saving them starts | | Redirect loops or `http://` links | Requests reach the app as plain HTTP behind HTTPS. Configure your framework to trust the proxy and generate `https://` URLs | | App went `Failed` after a port change | Nothing answers on the new port. Put the right port back on **Settings** | | A file the app wrote is gone | The container's filesystem is reset on every restart and deploy. Store files in a [bucket](/object-storage/overview) or a [database](/databases/overview) | ### Refused actions | Message | Meaning | | --- | --- | | **This app is already building, wait for that build to finish before deploying a new version!** | One build per app at a time | | **This app is paused. Resume it before deploying a new version!** | Builds and setting changes need a running app | | **Only a failed build can be retried!** | Retry is only for `Failed` builds | | **This build never received its source, deploy a new version instead!** | The build's upload never finished | | **You have read-only access to this account, you can not deploy apps!** | Your role or token is read-only | | **Your account is suspended. Upgrade to a paid plan to continue using our services.** | See [Billing](/account/billing) | | **Couldn't prepare your app's registry, try again in a moment!** | A temporary registry problem when creating the app. Try again | ### Still stuck? Collect the app name, the failed build's version and its output (`cloider app build logs `), and [contact support](/account/support). For problems across products, see the general [Troubleshooting](/reference/troubleshooting) page. ### Next steps - [Builds and deployments](/apps/builds-and-deployments): build statuses and logs. - [Build configuration](/apps/build-configuration): strategies, port and start command. - [Supported languages](/apps/supported-languages): known-good minimal projects. --- ## Connect to a Kubernetes cluster with kubectl You connect to a Cloider cluster the same way you connect to any Kubernetes cluster: with a kubeconfig file. The kubeconfig holds the cluster's API endpoint, the certificate authority that signs it, and a client certificate that authenticates you as the cluster's admin. This page shows how to get it, how to merge several clusters into one kubeconfig, and how to use it with `kubectl`, Lens, k9s and other tools. :::tip[No local setup needed] If you only need to run a few commands, the [browser terminal and `cloider cluster exec`](/kubernetes/web-terminal-and-exec) give you `kubectl` and `helm` already connected to the cluster, with nothing to install. ::: ### Before you begin - A cluster in the `Ready` status. See [Create a cluster](/kubernetes/create-a-cluster). The kubeconfig only exists once the cluster is `Ready`. - `kubectl` installed on your computer (see below). - For the CLI, Terraform, API and MCP: an API token. See [API tokens](/account/api-tokens). ### What is a kubeconfig? A kubeconfig is a YAML file that tells `kubectl` and other Kubernetes clients where a cluster's API is and how to authenticate to it. It has three lists, `clusters`, `users` and `contexts`, and a `current-context`. A **context** joins one cluster with one user, and switching context switches which cluster your commands go to. By default `kubectl` reads `~/.kube/config`, or the files listed in the `KUBECONFIG` environment variable. A Cloider cluster's kubeconfig looks like this (the certificates are shortened): ```yaml title="config" apiVersion: v1 kind: Config clusters: - cluster: certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t... server: https://staging-x8f2kq.cloider.com name: staging contexts: - context: cluster: staging user: staging name: staging current-context: staging users: - name: staging user: client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t... client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQ... ``` - `server` is the cluster's public API endpoint: the cluster name, a dash, the cluster id, on Cloider's domain, port 443. - The context in the downloaded file is named after the cluster. The CLI's `--merge` renames it to `cloider-` so it doesn't clash with your other contexts. - The client certificate and key authenticate you as cluster admin. **Treat the file like a password**: anyone who has it has full control of the cluster. :::warning[Keep the kubeconfig private] The kubeconfig grants full admin access to the cluster. Don't commit it to git, paste it in tickets or share it in chat. Store it with `0600` permissions (the CLI does this for you) and in CI keep it in a secret variable. ::: ### Install kubectl `kubectl` is the official Kubernetes command line client. Install a version within one minor version of your cluster's Kubernetes version. **macOS** ```bash brew install kubectl ``` **Linux** ```bash curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl ``` On arm64 machines replace `amd64` with `arm64`. **Windows** ```powershell winget install -e --id Kubernetes.kubectl ``` Check it: ```bash kubectl version --client ``` ### Get the kubeconfig **Dashboard** 1. Open **Kubernetes** in the sidebar. 2. Click the actions button (the three dots) on the cluster's row. 3. Click **Kubeconfig**. Your browser downloads a file named `config`. 4. Move it somewhere permanent and protect it: ```bash mkdir -p ~/.kube mv ~/Downloads/config ~/.kube/staging.yaml chmod 600 ~/.kube/staging.yaml export KUBECONFIG=~/.kube/staging.yaml kubectl get namespaces ``` The **Kubeconfig** action is disabled until the cluster is **Ready**, and while it is paused. **CLI** The fastest way is to merge the cluster into your own kubeconfig and switch to it in one command: ```bash cloider cluster kubeconfig staging --merge --use ``` ```text title="Output" ✓ Added context cloider-staging to /Users/alex/.kube/config ``` Other ways to get it: ```bash # Print it to stdout cloider cluster kubeconfig staging > staging.yaml # Write it to a file, created with 0600 permissions cloider cluster kubeconfig staging -f ~/.kube/staging.yaml # Merge under a context name of your choice, without switching to it cloider cluster kubeconfig staging --merge --context acme-staging ``` | Flag | What it does | | --- | --- | | `-f`, `--file` | Write to this file instead of stdout. With `--merge`, the kubeconfig to merge into | | `--merge` | Merge into your kubeconfig: the first file in `$KUBECONFIG`, else `~/.kube/config` | | `--context` | Context name to merge as. Default `cloider-` | | `--use` | With `--merge`, make it the current context | With `--merge`, the cluster, user and context entries are all named after the context (`cloider-staging`), and an existing entry of the same name is replaced. That makes the command safe to run again, for example after recreating a cluster. The merged context becomes the current one when you pass `--use`, or when your kubeconfig has no current context yet. Without `--use` the CLI tells you how to switch: ```text title="Output" ✓ Added context cloider-staging to /Users/alex/.kube/config Use it with `kubectl --context cloider-staging`, or `kubectl config use-context cloider-staging`. ``` **Terraform** The `cloider_cluster` resource and data source expose the kubeconfig as the sensitive `kube_config` attribute. Its `raw` field is the whole file, and `host`, `cluster_ca_certificate`, `client_certificate` and `client_key` are the parts the `kubernetes` and `helm` providers take. To write a kubeconfig file for `kubectl`, use the `local` provider: ```hcl title="main.tf" resource "cloider_cluster" "staging" { name = "staging" } # KUBECONFIG=./kubeconfig-staging.yaml kubectl get pods -A resource "local_sensitive_file" "kubeconfig" { filename = "${path.module}/kubeconfig-staging.yaml" content = cloider_cluster.staging.kube_config.raw file_permission = "0600" } ``` To deploy with Terraform directly instead, see [Kubernetes and Helm with Terraform](/terraform/guides/kubernetes-and-helm). **API** `GET /api/clusters/{id}/kubeconfig` returns the kubeconfig as a file download named `config`: ```bash curl -fsS https://cloider.com/api/clusters/x8f2kq/kubeconfig \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -o ~/.kube/staging.yaml chmod 600 ~/.kube/staging.yaml ``` The body is the kubeconfig YAML shown above. Find the cluster id with `GET /api/clusters`. See the [Clusters API](/api/endpoints/clusters). **MCP** Ask your agent: ```text title="Prompt" Add the staging cluster to my kubeconfig and make it the current context. ``` It calls the `get_cluster_kubeconfig` tool: ```json title="Tool call: get_cluster_kubeconfig" {"cluster": "staging", "merge": true, "use": true} ``` Without `merge` or `file`, the tool returns the kubeconfig as text, which puts the cluster's credentials in the agent's context. Prefer `merge` or `file` (written with `0600` permissions), or let the agent run commands with [`cluster_exec`](/kubernetes/web-terminal-and-exec) instead. ### Verify the connection ```bash kubectl cluster-info ``` ```text title="Output" Kubernetes control plane is running at https://staging-x8f2kq.cloider.com ``` Check that you are cluster admin: ```bash kubectl auth can-i '*' '*' --all-namespaces ``` ```text title="Output" yes ``` And look around: ```bash kubectl get namespaces kubectl get pods -A kubectl top pods -A ``` `kubectl top` works out of the box because every cluster runs the Kubernetes metrics server. In `kube-system` you see Cloider's own pods, such as the cluster DNS and the `terminal` Deployment used by the browser terminal. Leave them in place. ### Work with several clusters and contexts Every cluster you merge with the CLI becomes its own context, so one kubeconfig can hold all of them: ```bash cloider cluster kubeconfig staging --merge cloider cluster kubeconfig production --merge kubectl config get-contexts ``` ```text title="Output" CURRENT NAME CLUSTER AUTHINFO NAMESPACE * cloider-staging cloider-staging cloider-staging cloider-production cloider-production cloider-production ``` Switch between them: ```bash kubectl config use-context cloider-production kubectl config current-context ``` Or run a single command against another cluster without switching: ```bash kubectl --context cloider-staging get pods ``` Set a default namespace for a context so you don't have to pass `-n` every time: ```bash kubectl config set-context cloider-staging --namespace shop ``` If you use [kubectx and kubens](https://github.com/ahmetb/kubectx), the merged contexts work with them too: ```bash kubectx cloider-staging kubens shop ``` #### Merge a downloaded kubeconfig by hand If you downloaded the file from the dashboard and want it in `~/.kube/config` without the CLI, flatten both files into one with `kubectl`: ```bash cp ~/.kube/config ~/.kube/config.backup KUBECONFIG=~/.kube/config:~/Downloads/config kubectl config view --flatten > /tmp/kubeconfig-merged mv /tmp/kubeconfig-merged ~/.kube/config chmod 600 ~/.kube/config kubectl config use-context staging ``` The downloaded file names its cluster, user and context after the cluster (`staging`). If your kubeconfig already has entries with that name, `kubectl config view --flatten` keeps the first one it reads, so the new cluster is silently skipped. In that case use `cloider cluster kubeconfig staging --merge`, which names everything `cloider-staging` and replaces stale entries, or keep each cluster in its own file and list them in `KUBECONFIG`: ```bash export KUBECONFIG=~/.kube/config:~/.kube/staging.yaml:~/.kube/production.yaml ``` ### Use Lens, k9s and other tools Every Kubernetes tool that reads a kubeconfig works with Cloider clusters. **k9s** is a terminal UI for Kubernetes. It reads the same kubeconfig as `kubectl`: ```bash k9s --context cloider-staging ``` **Lens** (and its open source builds such as OpenLens) reads `~/.kube/config` and the files in `KUBECONFIG` and lists each context as a cluster. After `cloider cluster kubeconfig staging --merge`, `cloider-staging` appears in Lens's cluster list; open it to browse workloads, logs and metrics. You can also add a downloaded kubeconfig file to Lens from its kubeconfig settings. **Helm** uses the current context, or the one you name: ```bash helm --kube-context cloider-staging list -A ``` **VS Code's Kubernetes extension**, **Kustomize** (`kubectl apply -k`), **Skaffold**, **Tilt** and **Argo CD's CLI** all read the kubeconfig as well. ### Use the kubeconfig in CI Store the kubeconfig in your CI system's secret store and write it to a file at the start of the job. For example in GitHub Actions, with the kubeconfig saved as the secret `STAGING_KUBECONFIG`: ```yaml title=".github/workflows/deploy.yml" jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Write kubeconfig run: | mkdir -p ~/.kube echo "${{ secrets.STAGING_KUBECONFIG }}" > ~/.kube/config chmod 600 ~/.kube/config - name: Deploy run: kubectl apply -f k8s/ ``` Alternatively, install the CLI in the job and fetch the kubeconfig with an API token stored as `CLOIDER_TOKEN`, so the kubeconfig itself is never stored in CI: ```bash curl -fsSL https://cloider.com/install.sh | sh cloider cluster kubeconfig staging -f ~/.kube/config kubectl apply -f k8s/ ``` See [CI/CD with GitHub Actions](/guides/ci-cd-with-github-actions) and [CI/CD with GitLab](/guides/ci-cd-with-gitlab) for complete pipelines. ### Troubleshooting **`the cluster's kubeconfig has no clusters` (CLI)** or **an empty file**: the cluster isn't `Ready` yet, so it has no kubeconfig. Wait for `Ready` (`cloider cluster status staging`) and try again. **`Unable to connect to the server: dial tcp: lookup staging-x8f2kq.cloider.com: no such host`**: the endpoint doesn't resolve. Check the host name in your kubeconfig against the cluster's id (`cloider cluster list -o wide`). A kubeconfig of a deleted cluster stops working for good; a new cluster has a new id and therefore a new endpoint. **`kubectl` hangs or times out**: check that the cluster is `Ready` and not `Paused`. A paused cluster's API server is stopped, so nothing answers until you [resume it](/kubernetes/pause-and-resume). Also check that your network allows outgoing HTTPS (port 443) to the endpoint. **`error: You must be logged in to the server (Unauthorized)`** or **`x509: certificate signed by unknown authority`**: the client certificate or the CA in your kubeconfig doesn't belong to this cluster, usually because the file was edited or entries from another cluster were mixed in during a manual merge. Download the kubeconfig again, or run `cloider cluster kubeconfig staging --merge`, which replaces the old entries. **`error: context "cloider-staging" does not exist`**: the context was merged into a different file than the one `kubectl` reads. `--merge` writes to the first file in `$KUBECONFIG`, else `~/.kube/config`. Run `kubectl config get-contexts` and check `echo $KUBECONFIG`. **CLI exits with code `3`**: the CLI isn't logged in or the token was revoked. Run `cloider login`. See [CLI authentication](/cli/authentication). More cases are in [Kubernetes troubleshooting](/kubernetes/troubleshooting). ### Next steps - [Deploy a workload](/kubernetes/deploy-a-workload) with `kubectl apply`. - [Install software with Helm](/kubernetes/helm). - [Use the browser terminal and `cloider cluster exec`](/kubernetes/web-terminal-and-exec). --merge. Each becomes a context named cloider- in the same kubeconfig. Switch with kubectl config use-context, or pass --context to a single command.'}, {q: 'Can I use Lens or k9s with Cloider?', a: 'Yes. Lens and k9s read the same kubeconfig as kubectl. After merging a cluster with the CLI, it appears as the context cloider- in both.'}, ]} /> --- ## Create a Kubernetes cluster You create a Cloider Kubernetes cluster by giving it a name and, optionally, a Kubernetes version. Cloider provisions its control plane and API endpoint, and the cluster moves from `Creating` to `Starting` to `Ready`, usually within a few minutes. This page shows how to do it with every tool, the naming rules, and what to do when creation is refused. ### Before you begin - A Cloider account. See [Create an account](/get-started/create-an-account). - A role that can create resources. Members with read-only access can see clusters but not create them. See [Access control](/account/access-control). - On the **Free plan**, no other cluster in the account. The Free plan allows one cluster at a time; delete the existing one or upgrade in [Billing](/account/billing). - For the CLI, Terraform, API and MCP: an API token with **Read & Write** permission. See [API tokens](/account/api-tokens). ### Choose a name The cluster name becomes part of the cluster's API endpoint (`https://-.cloider.com`) and of the context name in its kubeconfig, so pick something short and meaningful such as `staging` or `production`. A cluster name must: | Rule | Valid | Invalid | | --- | --- | --- | | Use only lower case letters, digits and `-` | `shop-api` | `Shop_API` | | Start with a letter | `web2` | `2web`, `-web` | | Not end with `-` | `web` | `web-` | | Not contain spaces | `my-cluster` | `my cluster` | | Be at least 2 characters long | `qa` | `q` | Names don't have to be unique. If the account already has a cluster called `staging`, a second one gets the **label** `staging-1`, then `staging-2`, and so on. The dashboard shows the label, and the CLI accepts either the label or the cluster id wherever a cluster is expected. ### Choose a Kubernetes version The dashboard, CLI and API offer the versions `1.34`, `1.33` and `1.32`, newest first. When you don't choose, the newest is used. The list of versions on offer is also returned by `GET /api/options` as `cluster_versions`, which is what the CLI and the Terraform `cloider_options` data source read. A cluster keeps the version it was created with: there is no in-place upgrade. To move to another version, create a new cluster and deploy into it. See [Limits and compatibility](/kubernetes/limits-and-compatibility). ### Create the cluster **Dashboard** 1. Open **Kubernetes** in the sidebar. 2. Click **Create Cluster**. If the account has no cluster yet, the same button is on the welcome card. 3. In **Cluster Name**, type `staging`. 4. In **Cluster Version**, keep the newest version or pick another one. 5. Click **Create cluster**. You are taken back to the cluster list, where the new cluster is highlighted. Its status updates on its own every 2 seconds, from **Creating** to **Starting** to **Ready**. The cluster name becomes a link once it is **Ready**. **CLI** ```bash cloider cluster create staging --wait ``` ```text title="Output" ✓ Creating cluster staging (Kubernetes 1.34) ⠋ Waiting for cluster staging ✓ Cluster staging is ready. Get its kubeconfig with `cloider cluster kubeconfig staging --merge`. ``` Pick a version with `--version`: ```bash cloider cluster create staging --version 1.33 --wait ``` | Flag | Default | What it does | | --- | --- | --- | | `--version` | the newest offered | Kubernetes version, one of the versions the API offers | | `--wait`, `-w` | off | Wait until the cluster is `Ready` | | `--timeout` | `15m` | How long `--wait` waits | Without `--wait`, the command returns as soon as creation starts and tells you to follow it with `cloider cluster status staging`. With `-q` it prints only the new cluster's id, and with `-o json` it prints the API's answer: ```bash cloider cluster create staging -o json ``` ```json title="Output" { "hashed_id": "x8f2kq" } ``` **Terraform** ```hcl title="main.tf" terraform { required_providers { cloider = { source = "cloider/cloider" } } } provider "cloider" {} resource "cloider_cluster" "staging" { name = "staging" } output "cluster_status" { value = cloider_cluster.staging.status } ``` ```bash terraform init terraform apply ``` `terraform apply` waits until the cluster is `Ready` (up to 20 minutes by default), and `kube_config` then holds what the `kubernetes` and `helm` providers need. To pin a version, set `version` to one of the values of the [`cloider_options`](/terraform/data-sources/options) data source's `cluster_versions`: ```hcl title="main.tf" data "cloider_options" "current" {} resource "cloider_cluster" "staging" { name = "staging" version = data.cloider_options.current.cluster_versions[0] # A new version replaces the cluster, so keep the one it was created with lifecycle { ignore_changes = [version] } timeouts = { create = "30m" } } ``` :::warning[Changing name or version replaces the cluster] A cluster can't be renamed or upgraded in place. Changing `name` or `version` in Terraform destroys the cluster, with everything running in it, and creates a new one. ::: See [`cloider_cluster`](/terraform/resources/cluster) for every attribute. **API** ```bash curl -X POST https://cloider.com/api/clusters \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"cluster_name": "staging", "cluster_version": "1.34", "is_ha": false}' ``` ```json title="Response" { "hashed_id": "x8f2kq" } ``` | Field | Type | Required | Description | | --- | --- | --- | --- | | `cluster_name` | string | yes | The cluster name, following the [naming rules](#choose-a-name) | | `cluster_version` | string | yes | One of `1.34`, `1.33`, `1.32` (see `GET /api/options`) | | `is_ha` | boolean | no | Must be `false`. `true` is rejected | Then poll the status until it is `Ready`. The status endpoint answers with plain text: ```bash curl https://cloider.com/api/clusters/x8f2kq/status \ -H "Authorization: Bearer $CLOIDER_TOKEN" ``` ```text title="Response" Ready ``` See the [Clusters API](/api/endpoints/clusters) for every endpoint. **MCP** Ask your agent: ```text title="Prompt" Create a Kubernetes cluster called staging and wait until it is ready. ``` It calls the `create_cluster` tool: ```json title="Tool call: create_cluster" {"name": "staging", "wait": true} ``` To pick a version, the agent passes `version`, for example `{"name": "staging", "version": "1.33", "wait": true}`. An unknown version fails with a message that names the versions on offer. See [MCP tools](/mcp/tools). ### How long does it take to create a cluster? A new cluster usually reaches `Ready` within a few minutes. During that time it goes through these states: | Status | What is happening | | --- | --- | | `Creating` | Cloider provisions the cluster's control plane and its API endpoint | | `Starting` | The control plane is up. Cloider retrieves the cluster's kubeconfig and starts the terminal that the browser terminal and `cloider cluster exec` use | | `Ready` | The cluster is ready. The **Kubeconfig** and **Terminal** actions are enabled | The kubeconfig and the terminal are only available once the cluster is `Ready`. In the dashboard, the **Kubeconfig**, **Terminal** and **Pause** actions stay disabled until then. ### Verify Check the status: ```bash cloider cluster get staging ``` ```text title="Output" Name: staging Label: staging Version: 1.34 Status: Ready Created: 2026-10-19 09:41 ``` Then connect with `kubectl` and list the namespaces of the new cluster: ```bash cloider cluster kubeconfig staging --merge --use kubectl get namespaces ``` ```text title="Output" NAME STATUS AGE default Active 3m kube-node-lease Active 3m kube-public Active 3m kube-system Active 3m ``` Or, without installing anything, run the same command in the cluster's terminal: ```bash cloider cluster exec staging kubectl get namespaces ``` ### Troubleshooting The API answers a refused creation with HTTP 400 and the reasons, by field. A general reason is under `_`: ```json title="Response" { "errors": { "cluster_name": [ "Cluster name can not be upper case", "Cluster name must not have special characters except \"-\"" ] } } ``` | Message | Cause and fix | | --- | --- | | `Cluster name must not be empty` | Give the cluster a name | | `Cluster name must not have spaces` | Replace spaces with `-` | | `Cluster name can not be upper case` | Use lower case letters only | | `Cluster name can not start with numbers` | Start the name with a letter | | `Cluster name must not have special characters except "-"` | Use only letters, digits and `-` | | `Cluster name must not start with any special character` / `must not end with any special character` | Remove the leading or trailing `-` | | `Cluster name must be at least 2 characters` | Use a longer name | | `Invalid cluster version!` | Use one of the versions returned by `GET /api/options` | | `HA clusters are not supported at the moment!` | Send `is_ha: false` or leave it out | | `Free plan users can not create more than one cluster. UPGRADE NOW!` | The Free plan allows one cluster. Delete the existing cluster or upgrade to a paid plan in [Billing](/account/billing) | | `Your account is suspended. Upgrade to a paid plan to continue using our services.` | The account is suspended, for example after the trial ended. Upgrade in [Billing](/account/billing) | | `You have read-only access to this account, you can not create clusters!` | Your role in this account is read-only. Ask an owner or admin for a role that can create resources | | `This API token is read only!` (HTTP 403) | The token is a Read token. Create a **Read & Write** token, see [API tokens](/account/api-tokens). The CLI exits with code `5` | | `version 1.30 isn't offered, pick one of: 1.34, 1.33, 1.32` | CLI only: `--version` isn't on offer. The CLI exits with code `2` | If a cluster stays in `Creating` or `Starting` for much longer than a few minutes, open a [support request](/account/support) with the cluster's id (`cloider cluster list -o wide`). ### Next steps - [Connect with kubectl](/kubernetes/connect-with-kubectl): download the kubeconfig and set up contexts. - [Deploy a workload](/kubernetes/deploy-a-workload): your first Deployment and Service. - [Use the web terminal](/kubernetes/web-terminal-and-exec): `kubectl` and `helm` in the browser. - [Pause and resume](/kubernetes/pause-and-resume): stop paying for compute when a cluster isn't in use. --- ## Delete a Kubernetes cluster Deleting a Cloider cluster permanently removes it and everything in it: the control plane, every workload, every PersistentVolumeClaim and its data, and every load balancer. Billing for the cluster stops once it is deleted. There is no undo and no backup to restore from, so save what you need first. If you only want to stop paying for compute for a while, [pause the cluster](/kubernetes/pause-and-resume) instead. ### What is deleted | Deleted with the cluster | Not deleted | | --- | --- | | The control plane and the cluster's API endpoint | Domain [targets](/kubernetes/expose-services) that routed to the cluster's Services | | All namespaces, workloads, Services, ConfigMaps, Secrets and other objects | [Registry tokens](/container-registry/tokens) you created for the cluster | | All PersistentVolumeClaims and the data on them | Your [databases](/databases/overview), [buckets](/object-storage/overview) and [registry](/container-registry/overview) images | | All LoadBalancer Services and their IPs | Your domains and DNS records, including `A` records that pointed at load balancer IPs | | The kubeconfig, which stops working | Usage already accrued this month, which is still billed | ### Before you begin - A role that can delete resources. Members with read-only access can't delete clusters. - **Save what you need**: ```bash # Export the manifests of everything in the namespaces you care about kubectl get deploy,sts,svc,cm,secret,pvc,ingress,cronjob -n shop -o yaml > shop-backup.yaml # Copy files out of a volume through a running pod kubectl cp shop/web-6d8f7b9c5d-2kq9x:/usr/share/nginx/html ./html-backup ``` Exported Secrets contain your credentials in base64; store the file safely. For larger volumes, copy the data to a [bucket](/object-storage/overview) first. ### Delete the cluster **Dashboard** 1. Open **Kubernetes** in the sidebar. 2. Click the actions button (the three dots) on the cluster's row and choose **Delete**. 3. The **Delete Cluster** dialog warns that this will permanently delete the cluster. Click **I want to delete the cluster**. 4. Type the cluster's label, for example `staging`, to confirm. 5. Click **Delete cluster**. The button is only enabled once the label matches. The cluster shows **Terminating** and disappears from the list once it is deleted. **CLI** ```bash cloider cluster delete staging --wait ``` ```text title="Output" This permanently deletes cluster staging. Type staging to confirm: staging ✓ Deleting cluster staging ⠋ Waiting for cluster staging ✓ Cluster staging is deleted ``` In scripts and CI, where nothing can be typed, confirm with `--yes`: ```bash cloider cluster delete staging --yes --wait ``` Without a terminal and without `--yes`, the CLI refuses with `deleting cluster staging needs --yes when not running in a terminal` (exit code `2`). If the typed name doesn't match, it stops with `the name didn't match, nothing was deleted`. `cloider cluster rm` is an alias of `delete`. **Terraform** Remove the `cloider_cluster` resource from your configuration and apply, or destroy it directly: ```bash terraform destroy -target=cloider_cluster.staging ``` Terraform waits until the cluster is deleted, up to 20 minutes by default (the `delete` timeout). Destroy the `kubernetes` and `helm` resources that live in the cluster in the same run or before, so Terraform doesn't try to reach a cluster that no longer exists. Changing a cluster's `name` or `version` also deletes it, because the cluster is replaced. **API** ```bash curl -X DELETE https://cloider.com/api/clusters/x8f2kq \ -H "Authorization: Bearer $CLOIDER_TOKEN" ``` The response is the account's cluster list, with this cluster now `Terminating`: ```json title="Response" [ { "hashed_id": "x8f2kq", "cluster_name": "staging", "cluster_label": "staging", "cluster_version": "1.34", "is_ha": false, "status": "Terminating", "created_at": "2026-10-19T09:41:07Z" } ] ``` Poll `GET /api/clusters/x8f2kq/status` until it answers `Terminated`, or `404` once the cluster is gone. **MCP** Ask your agent: ```text title="Prompt" Delete the staging cluster. ``` It calls the `delete_cluster` tool: ```json title="Tool call: delete_cluster" {"cluster": "staging", "wait": true} ``` `delete_cluster` is marked as destructive, so MCP clients that honor tool annotations ask you before running it, and it isn't offered when the server runs with `--read-only`. See [MCP safety and permissions](/mcp/safety-and-permissions). ### Verify ```bash cloider cluster list ``` The cluster is no longer listed. Its kubeconfig context stops working; remove it from your kubeconfig: ```bash kubectl config delete-context cloider-staging kubectl config delete-cluster cloider-staging kubectl config delete-user cloider-staging ``` ### Clean up what pointed at the cluster **Domain targets** that routed to the cluster's Services stay on their domains and show up as **Orphaned targets detected!** on the domain's page. Delete them: ```bash cloider target list shop-staging.cloider.app -o wide cloider target delete t4k9 ``` **Registry tokens** made for the cluster keep working until they expire. Delete them in **Registry**, **Tokens**, or with `cloider registry token delete staging-cluster`. **DNS records** that pointed at the cluster's load balancer IPs now point at IPs that are no longer yours. Delete or update them in [DNS records](/domains/dns-records). **Database trusted sources** that allowed the cluster can be removed. See [Trusted sources](/databases/trusted-sources). ### How is a deleted cluster billed? Billing stops when the cluster is deleted. The compute, volumes and load balancers it used up to that moment, in the current month, are still part of your next invoice. See [Billing](/account/billing). ### Troubleshooting | Problem | Cause and fix | | --- | --- | | **Delete** is disabled | Your role is read-only, or the cluster is already `Terminating` | | `You have read-only access to this account, you can not delete clusters!` (HTTP 403) | Ask an owner or admin for a role that can delete resources | | `This API token is read only!` (HTTP 403) | Use a **Read & Write** token. The CLI exits with code `5` | | **Delete cluster** stays disabled in the dialog | The typed text must match the cluster's label exactly, for example `staging-1` and not `staging` | | The cluster stays `Terminating` for a long time | Wait a few more minutes, then open a [support request](/account/support) with the cluster id | | `2 clusters are named "staging", pick one by id` (CLI) | Several clusters share the name. Use the id the message lists, or `cloider cluster list -o wide` | ### Next steps - [Create a cluster](/kubernetes/create-a-cluster) again when you need one. - [Usage](/account/usage) to check your costs. - [Resource lifecycle](/concepts/resource-lifecycle) for how deletion works across Cloider. --- ## Deploy your first workload to Kubernetes This tutorial deploys a small web application to a Cloider cluster with five standard Kubernetes objects: a Namespace, a ConfigMap, a Secret, a Deployment and a Service. You apply them with `kubectl` (or any tool in the tabs below), check that the pods are running, open the app, and finally publish it on a domain with HTTPS. Every manifest is complete and can be copied as is. ### Before you begin - A cluster in the `Ready` status. See [Create a cluster](/kubernetes/create-a-cluster). - One way to run `kubectl`: locally with the cluster's kubeconfig ([Connect with kubectl](/kubernetes/connect-with-kubectl)), or the [browser terminal or `cloider cluster exec`](/kubernetes/web-terminal-and-exec), which need nothing installed. The example app is [podinfo](https://github.com/stefanprodan/podinfo), a tiny open source web server often used to test Kubernetes clusters. It listens on port `9898`, shows a message you can configure, and has health endpoints for probes. ### What you are going to create | Object | Name | What it does | | --- | --- | --- | | Namespace | `shop` | A folder for the app's objects, so they don't mix with others | | ConfigMap | `web-config` | Non-secret settings, passed to the container as environment variables | | Secret | `web-secrets` | Credentials, passed to the container as environment variables | | Deployment | `web` | Runs 2 replicas of the container and replaces them on updates | | Service | `web` | A stable address in the cluster (`web.shop.svc.cluster.local:80`) in front of the pods | ```mermaid flowchart LR S[Service web
port 80] --> P1[Pod web-...
port 9898] S --> P2[Pod web-...
port 9898] C[ConfigMap web-config] -. env .-> P1 C -. env .-> P2 X[Secret web-secrets] -. env .-> P1 X -. env .-> P2 ``` ### Step 1: Write the manifests Save the following as `shop.yaml`. The comments explain each part. ```yaml title="shop.yaml" # 1. A namespace for everything that belongs to the app apiVersion: v1 kind: Namespace metadata: name: shop --- # 2. Plain settings. Anyone who can read the namespace can read these apiVersion: v1 kind: ConfigMap metadata: name: web-config namespace: shop data: PODINFO_UI_MESSAGE: "Hello from Cloider Kubernetes" PODINFO_UI_COLOR: "#34577c" --- # 3. Credentials. stringData takes plain text; Kubernetes stores it base64 encoded apiVersion: v1 kind: Secret metadata: name: web-secrets namespace: shop type: Opaque stringData: API_KEY: "change-me" --- # 4. Two replicas of the container, with resources and health checks apiVersion: apps/v1 kind: Deployment metadata: name: web namespace: shop labels: app: web spec: replicas: 2 selector: matchLabels: app: web template: metadata: labels: app: web spec: containers: - name: web image: ghcr.io/stefanprodan/podinfo:6.7.0 ports: - name: http containerPort: 9898 envFrom: - configMapRef: name: web-config - secretRef: name: web-secrets resources: requests: cpu: 50m memory: 64Mi limits: memory: 128Mi readinessProbe: httpGet: path: /readyz port: http initialDelaySeconds: 2 periodSeconds: 5 livenessProbe: httpGet: path: /healthz port: http initialDelaySeconds: 5 periodSeconds: 10 --- # 5. A stable address for the pods, inside the cluster apiVersion: v1 kind: Service metadata: name: web namespace: shop spec: type: ClusterIP selector: app: web ports: - name: http port: 80 targetPort: http protocol: TCP ``` A few things to note: - **Requests drive scheduling and billing.** `requests` is what each container reserves. On Cloider every running container is billed on the larger of its request and its actual use, so set requests close to what the app really needs. Here 2 replicas reserve 100 millicores and 128 MiB in total. See [How is a cluster billed?](/kubernetes/overview#how-is-a-cluster-billed). - **The memory limit protects you from leaks.** A container that goes over `limits.memory` is restarted instead of growing forever. - **Probes keep traffic away from broken pods.** A pod only receives traffic from the Service once its readiness probe passes, and it is restarted when its liveness probe fails. - **The Service is `ClusterIP`.** It is only reachable inside the cluster. You publish it on a domain in step 5, or with a public IP in [Load balancers](/kubernetes/load-balancers). ### Step 2: Apply the manifests **Dashboard** 1. Open **Kubernetes** in the sidebar and click the cluster's name to open its terminal. 2. Create the file in the terminal with a heredoc: type `cat > shop.yaml <<'EOF'`, press Enter, paste the manifest, press Enter, type `EOF` and press Enter. 3. Apply it: ```bash kubectl apply -f shop.yaml ``` ```text title="Output" namespace/shop created configmap/web-config created secret/web-secrets created deployment.apps/web created service/web created ``` The terminal's files are temporary, so keep the original `shop.yaml` on your computer or in git. **CLI** With `kubectl` and the cluster's kubeconfig: ```bash kubectl apply -f shop.yaml ``` Or without a kubeconfig, by piping the file into the cluster's terminal: ```bash cloider cluster exec -i staging -- kubectl apply -f - < shop.yaml ``` ```text title="Output" namespace/shop created configmap/web-config created secret/web-secrets created deployment.apps/web created service/web created ``` **Terraform** Use the `kubernetes` provider with the cluster's `kube_config`. This is the same app as `shop.yaml`, as Terraform resources: ```hcl title="main.tf" terraform { required_providers { cloider = { source = "cloider/cloider" } kubernetes = { source = "hashicorp/kubernetes" } } } data "cloider_cluster" "staging" { name = "staging" } provider "kubernetes" { host = data.cloider_cluster.staging.kube_config.host cluster_ca_certificate = data.cloider_cluster.staging.kube_config.cluster_ca_certificate client_certificate = data.cloider_cluster.staging.kube_config.client_certificate client_key = data.cloider_cluster.staging.kube_config.client_key } resource "kubernetes_namespace_v1" "shop" { metadata { name = "shop" } } resource "kubernetes_config_map_v1" "web" { metadata { name = "web-config" namespace = kubernetes_namespace_v1.shop.metadata[0].name } data = { PODINFO_UI_MESSAGE = "Hello from Cloider Kubernetes" } } resource "kubernetes_secret_v1" "web" { metadata { name = "web-secrets" namespace = kubernetes_namespace_v1.shop.metadata[0].name } data = { API_KEY = var.api_key } } resource "kubernetes_deployment_v1" "web" { metadata { name = "web" namespace = kubernetes_namespace_v1.shop.metadata[0].name } spec { replicas = 2 selector { match_labels = { app = "web" } } template { metadata { labels = { app = "web" } } spec { container { name = "web" image = "ghcr.io/stefanprodan/podinfo:6.7.0" port { name = "http" container_port = 9898 } env_from { config_map_ref { name = kubernetes_config_map_v1.web.metadata[0].name } } env_from { secret_ref { name = kubernetes_secret_v1.web.metadata[0].name } } resources { requests = { cpu = "50m", memory = "64Mi" } limits = { memory = "128Mi" } } } } } } } resource "kubernetes_service_v1" "web" { metadata { name = "web" namespace = kubernetes_namespace_v1.shop.metadata[0].name } spec { selector = { app = "web" } port { port = 80 target_port = "http" } } } variable "api_key" { type = string sensitive = true } ``` The provider's `data` values are plain text; it encodes them for you. See [Kubernetes and Helm with Terraform](/terraform/guides/kubernetes-and-helm) for when to create the cluster in the same configuration. **API** The API applies manifests through the cluster's exec endpoint: send `kubectl apply -f -` as the command and the manifest, base64 encoded, as `stdin` (up to 2 MiB): ```bash curl -N -X POST https://cloider.com/api/clusters/x8f2kq/exec \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"command\": [\"kubectl\", \"apply\", \"-f\", \"-\"], \"stdin\": \"$(base64 < shop.yaml | tr -d '\n')\"}" ``` ```text title="Response" : open event: stdout data: bmFtZXNwYWNlL3Nob3AgY3JlYXRlZApjb25maWdtYXAvd2ViLWNvbmZpZyBjcmVhdGVkCi4uLgo= event: exit data: 0 event: end data: 1 ``` The `stdout` data is base64; decoded, it is `kubectl apply`'s usual output. Or talk to the cluster's own Kubernetes API directly with the kubeconfig. See [Web terminal and exec](/kubernetes/web-terminal-and-exec). **MCP** Ask your agent, with `shop.yaml` in your project: ```text title="Prompt" Apply shop.yaml to the staging cluster and tell me when the web pods are ready. ``` It calls `cluster_exec` with the manifest on stdin: ```json title="Tool call: cluster_exec" {"cluster": "staging", "command": "kubectl apply -f -", "stdin": "apiVersion: v1\nkind: Namespace\nmetadata:\n name: shop\n---\n..."} ``` Then it checks the rollout: ```json title="Tool call: cluster_exec" {"cluster": "staging", "command": "kubectl rollout status deploy/web -n shop --timeout=120s"} ``` ### Step 3: Check that it is running Wait for the rollout to finish: ```bash kubectl rollout status deployment/web -n shop ``` ```text title="Output" deployment "web" successfully rolled out ``` List everything in the namespace: ```bash kubectl get all -n shop ``` ```text title="Output" NAME READY STATUS RESTARTS AGE pod/web-6d8f7b9c5d-2kq9x 1/1 Running 0 40s pod/web-6d8f7b9c5d-8hj2p 1/1 Running 0 40s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/web ClusterIP 10.43.118.27 80/TCP 40s NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/web 2/2 2 2 40s NAME DESIRED CURRENT READY AGE replicaset.apps/web-6d8f7b9c5d 2 2 2 40s ``` Check that the ConfigMap and Secret reached the container: ```bash kubectl exec -n shop deploy/web -- env | grep -E 'PODINFO_UI_MESSAGE|API_KEY' ``` ```text title="Output" PODINFO_UI_MESSAGE=Hello from Cloider Kubernetes API_KEY=change-me ``` Read the logs and the resource use: ```bash kubectl logs -n shop deploy/web --tail 20 kubectl top pods -n shop ``` If a pod isn't `Running`, see [Troubleshooting](#troubleshooting) below. ### Step 4: Open the app The Service is internal, so open it through a port forward from your computer (this needs a local `kubectl` with the kubeconfig): ```bash kubectl port-forward -n shop service/web 8080:80 ``` Then open `http://localhost:8080`. You see podinfo with the message from the ConfigMap. Press Ctrl-C to stop the port forward. From the browser terminal, call the Service from inside the cluster instead: ```bash kubectl run curl --rm -i --restart=Never --image=curlimages/curl:8.10.1 -n shop -- curl -s http://web.shop.svc.cluster.local/api/info ``` ### Step 5: Publish it on a domain To make the app reachable from the internet with HTTPS, route a domain to the `web` Service with a target. Claim a [free subdomain](/domains/free-subdomains) such as `shop-staging.cloider.app`, then point it at the Service: ```bash cloider domain create shop-staging.cloider.app cloider target create shop-staging.cloider.app --cluster staging --service shop/web ``` A few moments later the app answers at `https://shop-staging.cloider.app`. [Expose services on a domain](/kubernetes/expose-services) covers targets in every tool, custom domains, paths and HTTPS. For a raw TCP port with a public IP, use a [LoadBalancer Service](/kubernetes/load-balancers) instead. ### Step 6: Update, scale and roll back **Change the configuration.** Edit `PODINFO_UI_MESSAGE` in `shop.yaml` and apply again. Environment variables are read when a container starts, so restart the pods to pick up the change: ```bash kubectl apply -f shop.yaml kubectl rollout restart deployment/web -n shop ``` **Change the image.** Set a new image tag; Kubernetes replaces the pods one by one, so the app stays up: ```bash kubectl set image deployment/web -n shop web=ghcr.io/stefanprodan/podinfo:6.7.1 kubectl rollout status deployment/web -n shop ``` **Roll back** to the previous version if something is wrong: ```bash kubectl rollout undo deployment/web -n shop ``` **Scale** by hand: ```bash kubectl scale deployment/web -n shop --replicas 3 ``` **Scale automatically** with a HorizontalPodAutoscaler. Every Cloider cluster runs the metrics server, so autoscaling on CPU works without extra setup: ```yaml title="web-hpa.yaml" apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: web namespace: shop spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: web minReplicas: 2 maxReplicas: 6 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 ``` ```bash kubectl apply -f web-hpa.yaml kubectl get hpa -n shop ``` Utilization is measured against the CPU request, which is one more reason to set requests that match the real use. Each extra replica adds its request to the bill while it runs. ### Step 7: Clean up Delete everything the tutorial created by deleting the namespace: ```bash kubectl delete namespace shop ``` Deleting the namespace deletes the pods, the Service, the ConfigMap and the Secret, and the cluster stops billing for the pods' compute. If you created a target in step 5, delete it too with `cloider target delete ` (see `cloider target list shop-staging.cloider.app -o wide`), or delete the whole subdomain with `cloider domain delete shop-staging.cloider.app`. ### Troubleshooting | Symptom | What to check | | --- | --- | | Pod stays `Pending` | `kubectl describe pod -n shop ` and read `Events`. A request that no node can satisfy, or a PersistentVolumeClaim that isn't bound, keeps a pod pending | | `ImagePullBackOff` or `ErrImagePull` | The image name or tag is wrong, or it is in a private registry without an `imagePullSecret`. See [Pull private images](/kubernetes/pull-private-images) | | `CrashLoopBackOff` | The container starts and exits. Read `kubectl logs -n shop --previous` | | `OOMKilled` in `kubectl describe pod` | The container went over `limits.memory`. Raise the limit or fix the leak | | Pods `Running` but `0/1` ready | The readiness probe fails. Check the probe's path and port, and the logs | | The app works with port forward but not on the domain | Check the target's namespace, service and port with `cloider target list `. See [Expose services](/kubernetes/expose-services) | More cases are in [Kubernetes troubleshooting](/kubernetes/troubleshooting). ### Next steps - [Expose services on a domain](/kubernetes/expose-services) with HTTPS. - [Install charts with Helm](/kubernetes/helm) instead of writing every manifest. - [Add persistent storage](/kubernetes/persistent-volumes) for stateful apps. - [Deploy your own images](/kubernetes/pull-private-images) from the Cloider registry. --- ## Expose Kubernetes services on a domain with HTTPS To make an HTTP service in your Cloider cluster reachable on the internet, you route a domain to it with a **target**. A target says "requests for this domain and path go to this Service, on this port", and Cloider takes care of the rest: the public entry point, the routing and the HTTPS certificate. You don't create Ingress objects or install an ingress controller, and there is no load balancer charge. ### At a glance | | | | --- | --- | | **What you route** | A domain, or a path of it (`/`, `/api`, ...), to a Service's port in a cluster | | **Domains** | A free subdomain such as `shop-staging.cloider.app`, or your own domain such as `example.com` | | **HTTPS** | Automatic. Free subdomains use Cloider's certificate; custom domains get a certificate of their own | | **Your Service** | Any Service type, usually `ClusterIP`. It speaks plain HTTP on the port you route to | | **Cost** | No load balancer charge. You pay only for the pods behind the Service | | **Ingress objects** | Not served. Targets replace them | | **Tools** | Dashboard (**Domains**), `cloider target`, `cloider_target` in Terraform, `/api/targets`, MCP | ### How targets work ```mermaid flowchart LR U[Browser] -->|https://example.com/api/orders| E[Cloider HTTPS entry point
certificate for example.com] E -->|target: /api| S1[Service api
namespace shop, port 80] E -->|target: /| S2[Service web
namespace shop, port 80] S1 --> P1[api pods] S2 --> P2[web pods] ``` 1. The domain resolves to Cloider. A free subdomain does so from the start. A custom domain does once it uses Cloider's nameservers: the domain itself has an `ALIAS` record that points at Cloider from the start, and other host names need an `A` or `CNAME` record, since a target doesn't add one (see [Add a custom domain](/domains/add-a-custom-domain)). 2. Cloider terminates HTTPS for the domain. 3. It picks the target whose `path` matches the request, the longest one first. A target's path covers everything under it, so `/` catches whatever no longer path does. 4. It forwards the request as plain HTTP to the Service's port in your cluster, optionally rewriting the path. ### Before you begin - A cluster in the `Ready` status with a Service to expose. [Deploy a workload](/kubernetes/deploy-a-workload) creates the `web` Service in the `shop` namespace used below. - A domain in your Cloider account: - a free subdomain under `cloider.app`, claimed in **Domains** or with `cloider domain create shop-staging.cloider.app` (see [Free subdomains](/domains/free-subdomains)), or - your own domain, added, pointed at Cloider's nameservers and given a DNS record for each host name you serve (see [Add a custom domain](/domains/add-a-custom-domain)). - A role that can create resources. Members with read-only access can't create targets. ### Find the Service to route to Targets route to Services that Cloider sees in your clusters. List them with their namespaces and ports: ```bash cloider cluster services staging ``` ```text title="Output" CLUSTER NAMESPACE SERVICE PORTS staging shop web 80/TCP staging shop api 80/TCP staging iot mqtt 1883/TCP ``` Cloider's own services in the cluster (cluster DNS, metrics server and the terminal) aren't listed. The same list is `GET /api/clusters/services` and the MCP tool `list_cluster_services`. ### Create a target Route `shop-staging.cloider.app` to port 80 of the `web` Service in the `shop` namespace of the `staging` cluster. **Dashboard** 1. Open **Domains** in the sidebar and click the domain, for example `shop-staging.cloider.app`. 2. Under **Link your app to a domain**, click **Add target**. 3. In **Target type**, choose **Cluster**. 4. Pick the **Cluster** (`staging`), the **Namespace** (`shop`), the **Service** (`web`) and the **Port** (`80`). 5. Leave **Path** as `/` and **Target path** empty to route the whole domain as it is. 6. Click **Save**. The target appears in the domain's table with its **Type**, **Name**, **Namespace**, **Service**, **Port**, **Protocol**, **Path** and **Target path**. Click the path to open it in a new tab. If the cluster has no Services yet, the form says so: deploy one first. **CLI** ```bash cloider target create shop-staging.cloider.app --cluster staging --service shop/web ``` ```text title="Output" ✓ Routed shop-staging.cloider.app/ to staging/shop/web:80 ``` `--service` is `/`. When the Service has a single port, it is used; otherwise pick one with `--port`. | Flag | Default | What it does | | --- | --- | --- | | `--cluster` | | The cluster, by name, label or id | | `--service` | | The Service, as `/` | | `--port` | the Service's only port | The Service port to send requests to | | `--protocol` | the port's own | `TCP` or `UDP`, to pick between ports of the same number | | `--path` | `/` | The path on the domain to route | | `--target-path` | `--path` | The path the request reaches the Service with | | `--app` | | Route to a Cloider app instead of a cluster. Can't be combined with `--cluster` | List and change targets: ```bash cloider target list shop-staging.cloider.app -o wide cloider target update shop-staging.cloider.app t4k9 --port 8080 cloider target delete t4k9 ``` **Terraform** ```hcl title="main.tf" resource "cloider_cluster" "staging" { name = "staging" } resource "cloider_domain" "shop" { name = "shop-staging.cloider.app" } # https://shop-staging.cloider.app/ reaches port 80 of the web Service in the shop namespace resource "cloider_target" "shop" { domain = cloider_domain.shop.name cluster_id = cloider_cluster.staging.id namespace = "shop" service = "web" port = 80 } ``` | Attribute | Required | Description | | --- | --- | --- | | `domain` | yes | The domain to route. Changing it replaces the target | | `cluster_id` | with a cluster Service | The cluster's id | | `namespace`, `service`, `port` | with `cluster_id` | The Service and the port that speaks plain HTTP | | `path` | no | The path on the domain, and everything under it. Default `/` | | `target_path` | no | The path requests reach the Service with. Default `path` | | `protocol` | no | `TCP` or `UDP`. Default `TCP` | When the Service itself is managed with the `kubernetes` provider, reference it (`kubernetes_service_v1.web.metadata[0].name`) so the target is created after it. See [`cloider_target`](/terraform/resources/target). **API** ```bash curl -X POST https://cloider.com/api/targets \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "domain_name": "shop-staging.cloider.app", "target_type": "Cluster", "cluster_id": "x8f2kq", "cluster_name": "staging", "namespace": "shop", "service": "web", "port": 80, "protocol": "TCP", "path": "/", "target_path": "/" }' ``` ```json title="Response" { "hashed_id": "t4k9", "domain_name": "shop-staging.cloider.app", "message": "Target created successfully" } ``` | Field | Description | | --- | --- | | `domain_name` | Required. The domain, which must be in your account | | `target_type` | `Cluster` (the default) or `App` | | `cluster_id` | The cluster's id | | `cluster_name` | The cluster's name | | `namespace`, `service`, `port` | Required for a `Cluster` target | | `protocol` | `TCP` or `UDP` | | `path` | Default `/`. Starts with `/` and contains only letters, digits and `/ _ . -` | | `target_path` | Default: `path`. Same characters as `path` | List a domain's targets with `GET /api/targets?domain_name=shop-staging.cloider.app`, change one with `PUT /api/targets/{id}` (same body) and delete it with `DELETE /api/targets/{id}`. See the [Targets API](/api/endpoints/targets). **MCP** Ask your agent: ```text title="Prompt" Serve the web service of the shop namespace in the staging cluster at shop-staging.cloider.app. ``` It calls `create_domain_target`: ```json title="Tool call: create_domain_target" {"domain": "shop-staging.cloider.app", "cluster": "staging", "service": "shop/web"} ``` The agent can find the Service first with `list_cluster_services`, and change or remove the target with `update_domain_target` and `delete_domain_target`. ### Verify ```bash curl -I https://shop-staging.cloider.app ``` ```text title="Output" HTTP/2 200 content-type: text/html; charset=utf-8 ``` A free subdomain answers within moments. A custom domain answers once its DNS points to Cloider and its certificate has been issued, see [HTTPS certificates](#https-certificates). ### Route several services on one domain Each path of a domain routes to one target, so one domain can front several Services. The longest matching path wins: | Domain path | Goes to | Created with | | --- | --- | --- | | `example.com/` | `shop/web:80` | `cloider target create example.com --cluster production --service shop/web` | | `example.com/api` | `shop/api:80` | `cloider target create example.com --cluster production --service shop/api --path /api` | | `example.com/grafana` | `monitoring/grafana:80` | `cloider target create example.com --cluster production --service monitoring/grafana --path /grafana` | A request for `https://example.com/api/orders` goes to the `api` Service as `/api/orders`, and `https://example.com/about` goes to `web`. A path can be used by only one target per domain. A second target with the same path is refused with `This path is already in use on this domain. Please choose a different path.`. ### Rewrite the path By default a request reaches the Service with the path it was made with. Set a **target path** to replace the routed path: ```bash cloider target create example.com --cluster production --service docs/docs --path /docs --target-path / ``` Now `https://example.com/docs/intro` reaches the `docs` Service as `/intro`. This is useful for apps that expect to be served at the root. Remember that such an app also generates links without the `/docs` prefix unless you configure its base path. ### Use a custom domain 1. Add the domain to your account and point it at Cloider's nameservers. See [Add a custom domain](/domains/add-a-custom-domain) and [Nameservers](/domains/nameservers). 2. Add a DNS record for every other host name you serve. The domain itself comes with an `ALIAS` record that points at Cloider, but a target doesn't add records: use a `CNAME` to one of your free `cloider.app` subdomains for `www` and other subdomains. See [Add a custom domain](/domains/add-a-custom-domain#step-4-point-the-host-names-you-serve-at-cloider). 3. Create targets on it exactly as for a free subdomain. A subdomain such as `www.example.com` is a domain of its own for targets, so add it with `cloider domain create` first. ```bash cloider domain create example.com cloider dns create example.com --type CNAME --name www --value shop-staging.cloider.app. --ttl 3600 cloider target create example.com --cluster production --service shop/web cloider domain create www.example.com cloider target create www.example.com --cluster production --service shop/web ``` See [Domains and DNS](/domains/overview) for the domain side, and [Targets](/domains/targets) for targets from the domain's point of view. ### HTTPS certificates Every target is served over HTTPS: - **Free subdomains** (`*.cloider.app`) are covered by Cloider's own certificate, so HTTPS works immediately. - **Custom domains** get a certificate issued for the host name automatically, through an HTTP challenge, once the domain resolves to Cloider. Until DNS points to Cloider, the certificate can't be issued. Your Service receives plain HTTP: don't configure TLS in the app for traffic that comes through a target. See [HTTPS certificates](/domains/https-certificates). ### What about Ingress? Ingress objects you create inside a Cloider cluster aren't served: Cloider doesn't publish them, so they have no effect. Targets do the same job, routing host names and paths to Services with HTTPS, and are managed from the dashboard, CLI, Terraform, API and MCP. If you need your own ingress controller or Gateway API implementation, for routing, TLS or middleware a target doesn't offer, install it in the cluster, expose it with a [Service of type LoadBalancer](/kubernetes/load-balancers), and point a custom domain at it with a [LoadBalancer target](/domains/targets#point-a-domain-at-a-loadbalancer). Its Ingress or HTTPRoute objects are then served by your controller. You run the controller, its routes and its TLS certificates and their renewal yourself, and the domain can't have any other targets. When you install a Helm chart that can create an Ingress, disable it (usually `ingress.enabled: false`) and create a target for the chart's Service. See [Helm](/kubernetes/helm). For non-HTTP traffic, such as raw TCP or UDP, use a [Service of type LoadBalancer](/kubernetes/load-balancers), with a [LoadBalancer target](/domains/targets#point-a-domain-at-a-loadbalancer) for its domain. ### Limits - **Path characters.** `path` and `target_path` start with `/` and contain only letters, digits and `/ _ . -`. Other values are refused with `Path must start with / and may only contain letters, numbers and / _ . -`. - **Name length.** The Service name, the namespace and the cluster name together may not exceed 57 characters. Longer combinations are refused with `Service and namespace names are too long for this cluster (combined limit exceeded)`. Use shorter names. - **Plain HTTP backends.** The Service's port must speak HTTP, not HTTPS. - **One target per path.** Each path of a domain routes to exactly one target. ### Troubleshooting **`404` or a default page on the domain**: no target matches, or the target points at the wrong namespace, Service or port. Check `cloider target list ` against `cloider cluster services `. **`502 Bad Gateway` or `503 Service Temporarily Unavailable`**: the target matches but nothing answers behind it. Check that the Service has endpoints (`kubectl get endpoints web -n shop`), that the pods are ready, that the routed port is the Service port and not the container port, and that the app speaks plain HTTP. A paused cluster has no running pods, so its targets answer with errors until you resume it. **Certificate error on a custom domain**: the domain doesn't resolve to Cloider yet, so its certificate couldn't be issued. Check the nameservers and that the host name has an `A` or `CNAME` record (see [Domains troubleshooting](/domains/troubleshooting)), then wait for DNS to propagate. **Links or assets break under a path**: the app generates absolute links for the root. Configure the app's base path, or give it its own subdomain instead of a path. **`Orphaned targets detected!` on the domain page**: the targets point to a cluster, Service or app that no longer exists, for example after deleting a cluster. Delete them or point them at a Service that exists. **`cluster staging has no services to route to yet` (CLI)** or **`no service shop/web in cluster staging`**: create the Service first, or check its namespace and name with `cloider cluster services`. More cases are in [Kubernetes troubleshooting](/kubernetes/troubleshooting) and [Domains troubleshooting](/domains/troubleshooting). ### Next steps - [Targets](/domains/targets) in the Domains section. - [Load balancers](/kubernetes/load-balancers) for TCP and UDP. - [Add a custom domain](/domains/add-a-custom-domain). --- ## Install Helm charts on Cloider Kubernetes Helm is the package manager for Kubernetes: a **chart** bundles all the manifests an application needs, and `helm install` deploys it with your settings in one command. Helm works on Cloider clusters exactly as on any Kubernetes cluster, and it is already installed in every cluster's [browser terminal and `cloider cluster exec`](/kubernetes/web-terminal-and-exec). This page shows how to install, configure, upgrade and remove charts, and what to adjust in charts so their services are reachable on Cloider. ### Before you begin - A cluster in the `Ready` status. See [Create a cluster](/kubernetes/create-a-cluster). - Helm, in one of these places: - the cluster's **browser terminal** or `cloider cluster exec`, where `helm` is preinstalled and connected to the cluster, - your computer, with [Helm 3 installed](https://helm.sh/docs/intro/install/) and the cluster's kubeconfig set up as in [Connect with kubectl](/kubernetes/connect-with-kubectl), - Terraform's `helm` provider. Install Helm locally if you want it: **macOS** ```bash brew install helm ``` **Linux** ```bash curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash ``` **Windows** ```powershell winget install Helm.Helm ``` ### Helm in two minutes | Term | Meaning | | --- | --- | | **Chart** | A package of Kubernetes manifest templates, for example `grafana/grafana` | | **Repository** | A place charts are downloaded from, added with `helm repo add`. Charts can also come from OCI registries (`oci://...`) | | **Values** | The settings of a chart, given with `--set key=value` or a `values.yaml` file (`-f values.yaml`) | | **Release** | One installed copy of a chart in a namespace, with a name you choose. You can install the same chart several times under different names | The commands you use most: ```bash helm repo add # add a chart repository helm search repo # find charts helm show values # see every setting a chart accepts helm upgrade --install -n --create-namespace -f values.yaml helm list -A # installed releases helm history -n helm rollback -n helm uninstall -n ``` `helm upgrade --install` installs a release if it doesn't exist and upgrades it if it does, so the same command works for the first deploy and every update. ### Install your first chart This installs [podinfo](https://github.com/stefanprodan/podinfo), a small demo web app, into the `demo` namespace. **Dashboard** 1. Open **Kubernetes** in the sidebar and click the cluster's name to open its terminal. 2. Run: ```bash helm repo add podinfo https://stefanprodan.github.io/podinfo helm upgrade --install podinfo podinfo/podinfo -n demo --create-namespace --set replicaCount=2 ``` ```text title="Output" "podinfo" has been added to your repositories Release "podinfo" does not exist. Installing it now. NAME: podinfo LAST DEPLOYED: Mon Oct 19 10:12:31 2026 NAMESPACE: demo STATUS: deployed REVISION: 1 ``` The terminal's files, including Helm's repository list, are temporary. After the terminal restarts, run `helm repo add` again before installing from that repository. **CLI** With Helm installed locally and the cluster's context selected: ```bash helm repo add podinfo https://stefanprodan.github.io/podinfo helm repo update helm upgrade --install podinfo podinfo/podinfo -n demo --create-namespace --set replicaCount=2 ``` Or without a local Helm, through the cluster's terminal: ```bash cloider cluster exec staging "helm repo add podinfo https://stefanprodan.github.io/podinfo && helm upgrade --install podinfo podinfo/podinfo -n demo --create-namespace --set replicaCount=2" ``` To pass a values file through `exec`, send it on stdin and read it with `-f -`: ```bash cloider cluster exec -i staging -- sh -c 'helm repo add podinfo https://stefanprodan.github.io/podinfo >/dev/null && helm upgrade --install podinfo podinfo/podinfo -n demo --create-namespace -f -' < values.yaml ``` **Terraform** Use the `helm` provider with the cluster's `kube_config`. The `helm` provider 3.x takes `kubernetes` as an attribute: ```hcl title="main.tf" terraform { required_providers { cloider = { source = "cloider/cloider" } helm = { source = "hashicorp/helm" } } } data "cloider_cluster" "staging" { name = "staging" } provider "helm" { kubernetes = { host = data.cloider_cluster.staging.kube_config.host cluster_ca_certificate = data.cloider_cluster.staging.kube_config.cluster_ca_certificate client_certificate = data.cloider_cluster.staging.kube_config.client_certificate client_key = data.cloider_cluster.staging.kube_config.client_key } } resource "helm_release" "podinfo" { name = "podinfo" repository = "https://stefanprodan.github.io/podinfo" chart = "podinfo" namespace = "demo" create_namespace = true set = [ { name = "replicaCount", value = "2" }, ] } ``` See [Kubernetes and Helm with Terraform](/terraform/guides/kubernetes-and-helm) for creating the cluster and the release in one configuration. **API** The API has no Helm endpoint of its own. Run Helm through the cluster's exec endpoint, which streams the output back as server-sent events: ```bash curl -N -X POST https://cloider.com/api/clusters/x8f2kq/exec \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"command": ["sh", "-c", "helm repo add podinfo https://stefanprodan.github.io/podinfo && helm upgrade --install podinfo podinfo/podinfo -n demo --create-namespace"], "timeout": 600}' ``` See [Web terminal and exec](/kubernetes/web-terminal-and-exec) for the event format. **MCP** Ask your agent: ```text title="Prompt" Install the podinfo Helm chart from https://stefanprodan.github.io/podinfo into the demo namespace of the staging cluster, with 2 replicas. ``` It calls `cluster_exec`: ```json title="Tool call: cluster_exec" {"cluster": "staging", "command": "helm repo add podinfo https://stefanprodan.github.io/podinfo && helm upgrade --install podinfo podinfo/podinfo -n demo --create-namespace --set replicaCount=2"} ``` Check the release and its pods: ```bash helm list -n demo kubectl get pods -n demo ``` ```text title="Output" NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION podinfo demo 1 2026-10-19 10:12:31.511 +0000 UTC deployed podinfo-6.7.0 6.7.0 NAME READY STATUS RESTARTS AGE podinfo-7f9c6b8d9b-k2l8m 1/1 Running 0 30s podinfo-7f9c6b8d9b-x4w9q 1/1 Running 0 30s ``` ### Configure a chart with a values file Most charts take dozens of settings. List them with `helm show values`, copy what you want to change into your own file, and keep that file in git: ```bash helm show values podinfo/podinfo > podinfo-defaults.yaml ``` ```yaml title="values.yaml" replicaCount: 2 ui: message: "Hello from Cloider" resources: requests: cpu: 50m memory: 64Mi limits: memory: 128Mi # Cloider doesn't serve Ingress objects; route a domain to the Service with a target instead ingress: enabled: false ``` ```bash helm upgrade --install podinfo podinfo/podinfo -n demo --create-namespace -f values.yaml ``` Three settings deserve a look in every chart you install on Cloider: 1. **Resources.** Each running container is billed on the larger of its request and its use. Charts often ship with no requests (billed on use) or with generous ones (billed on the request). Set `resources` to what the app really needs. See [How is a cluster billed?](/kubernetes/overview#how-is-a-cluster-billed). 2. **Ingress.** Ingress objects inside a Cloider cluster aren't served. Leave the chart's ingress disabled and publish the chart's Service with a [target](/kubernetes/expose-services), which also gives you HTTPS. 3. **Service type and persistence.** A chart that sets `service.type: LoadBalancer` gets a public IP that is billed per load balancer ([Load balancers](/kubernetes/load-balancers)). A chart with `persistence.enabled: true` creates PersistentVolumeClaims that are billed per GiB of their size ([Persistent volumes](/kubernetes/persistent-volumes)). ### Example: Grafana with persistent storage, on your domain [Grafana](https://grafana.com/) dashboards, with its data on a 5 GiB volume and served at `https://grafana.example.com`. ```yaml title="grafana-values.yaml" persistence: enabled: true size: 5Gi resources: requests: cpu: 100m memory: 128Mi limits: memory: 256Mi service: type: ClusterIP port: 80 ingress: enabled: false ``` ```bash helm repo add grafana https://grafana.github.io/helm-charts helm repo update helm upgrade --install grafana grafana/grafana -n monitoring --create-namespace -f grafana-values.yaml kubectl rollout status deployment/grafana -n monitoring ``` Get the generated admin password: ```bash kubectl get secret grafana -n monitoring -o jsonpath='{.data.admin-password}' | base64 -d; echo ``` Route your domain to the `grafana` Service (the domain must already be in your account, see [Add a custom domain](/domains/add-a-custom-domain)): ```bash cloider target create grafana.example.com --cluster staging --service monitoring/grafana ``` Grafana now answers at `https://grafana.example.com`. Log in as `admin` with the password above. The chart's 5 GiB claim is billed as 5 GiB of volume for as long as it exists, including after `helm uninstall` if the chart leaves it behind (see below). ### Upgrade, roll back and uninstall ```bash # Change a value and upgrade helm upgrade podinfo podinfo/podinfo -n demo -f values.yaml --set replicaCount=3 # Upgrade to a newer chart version helm repo update helm upgrade podinfo podinfo/podinfo -n demo -f values.yaml --version 6.7.1 # See what changed between revisions and go back helm history podinfo -n demo helm rollback podinfo 1 -n demo # Remove the release helm uninstall podinfo -n demo ``` `helm uninstall` removes what the chart created, except objects the chart marks to keep. Many charts keep PersistentVolumeClaims so your data survives a reinstall; those claims keep being billed until you delete them: ```bash kubectl get pvc -n monitoring kubectl delete pvc -n monitoring ``` Deleting the namespace removes everything left in it: ```bash kubectl delete namespace demo ``` ### Charts with CRDs and operators Some charts install CustomResourceDefinitions (CRDs) and an operator that manages them, such as database or certificate operators. You install them with Helm the usual way; your kubeconfig is cluster admin, so Helm can create cluster-wide objects. Keep in mind that: - the operator's own pods run in your cluster and are billed like any other pods, - an operator that creates `Ingress` objects won't make anything public, since Ingress isn't served; use a [target](/kubernetes/expose-services) for HTTP, - charts built to manage nodes or cluster networking (for example node provisioners or CNI plugins) are meant for self-managed clusters. On Cloider the nodes and the network are managed for you. See [Limits and compatibility](/kubernetes/limits-and-compatibility). ### Troubleshooting **`Error: INSTALLATION FAILED: cannot re-use a name that is still in use`**: a release of that name already exists in the namespace. Use `helm upgrade --install` instead of `helm install`, or pick another release name. **`Error: INSTALLATION FAILED: ... context deadline exceeded`** with `--wait`: the chart's pods didn't become ready in time. Run `kubectl get pods -n ` and `kubectl describe pod` to see why, typically an image pull error, a pending PersistentVolumeClaim or a failing probe. **`Error: repo ... not found`** in the browser terminal: the terminal restarted and forgot its repositories. Run `helm repo add` again. **The chart's Ingress doesn't work**: Ingress isn't served on Cloider. Disable it in the values and create a [target](/kubernetes/expose-services) for the chart's Service. **The chart's LoadBalancer Service stays ``**: see [Load balancers](/kubernetes/load-balancers#troubleshooting). More cases are in [Kubernetes troubleshooting](/kubernetes/troubleshooting). ### Next steps - [Expose services on a domain](/kubernetes/expose-services) for the charts you install. - [Persistent volumes](/kubernetes/persistent-volumes) for charts with persistence. - [WordPress on Kubernetes](/guides/wordpress-on-kubernetes), a complete guide. - [Kubernetes and Helm with Terraform](/terraform/guides/kubernetes-and-helm). --- ## Kubernetes limits and compatibility on Cloider Cloider clusters run standard Kubernetes, so manifests, Helm charts and tools written for Kubernetes work on them. A few things work differently because Cloider manages the control plane, the nodes and the network for you. This page lists the versions, the limits that the platform enforces, and the differences to plan for. ### Kubernetes versions | | | | --- | --- | | **Versions offered** | `1.34`, `1.33` and `1.32`, newest first. The newest is the default | | **Where the list comes from** | `GET /api/options` returns it as `cluster_versions`. The CLI's `--version`, the Terraform `version` attribute and the MCP `version` parameter are checked against it | | **Upgrades** | Not in place. A cluster keeps the version it was created with; create a new cluster to move to another version | | **Exact version** | Run `kubectl version` to see the patch version a cluster runs | ```bash kubectl version ``` ```text title="Output" Client Version: v1.34.1 Server Version: v1.34.1 ``` #### Moving to a new version Since there is no in-place upgrade, a version change is a migration: 1. Create a new cluster, for example `production-2`, with the new version. 2. Deploy your workloads into it, ideally from the same manifests, Helm values or Terraform configuration. 3. Copy the data of your PersistentVolumeClaims if you have any. 4. Switch your [targets](/kubernetes/expose-services) to the new cluster's Services (`cloider target update --cluster production-2 --service shop/web`) and update DNS records for load balancer IPs. 5. [Delete](/kubernetes/delete-a-cluster) the old cluster. Check your manifests for APIs that were removed in the new version before you move, with the [Kubernetes deprecation guide](https://kubernetes.io/docs/reference/using-api/deprecation-guide/). ### Tool compatibility | Tool | Notes | | --- | --- | | `kubectl` | Use a version within one minor version of the cluster's (the standard Kubernetes version skew policy) | | Helm | Helm 3. Preinstalled in the [browser terminal](/kubernetes/web-terminal-and-exec) | | Terraform | The `kubernetes` and `helm` providers, configured from `cloider_cluster`'s `kube_config`. The `helm` provider 3.x takes `kubernetes` as an attribute | | Lens, k9s, kubectx, Kustomize, Skaffold, Tilt, Argo CD CLI | Work with the kubeconfig like with any cluster. See [Connect with kubectl](/kubernetes/connect-with-kubectl) | | CI systems | Use the kubeconfig as a secret, or `cloider cluster exec` with an API token | ### What works as on any Kubernetes cluster - The standard Kubernetes API: Deployments, StatefulSets, Jobs, CronJobs, Services, ConfigMaps, Secrets, ServiceAccounts, RBAC, CustomResourceDefinitions and custom resources. - **Cluster admin access** through the kubeconfig's client certificate. - **Cluster DNS**: Services resolve as `..svc.cluster.local`. - **The metrics API**: the metrics server runs in every cluster, so `kubectl top` and the HorizontalPodAutoscaler work. - **PersistentVolumeClaims** with the storage classes Cloider provides. See [Persistent volumes](/kubernetes/persistent-volumes). - **Services of type `LoadBalancer`**, which get a public IP. See [Load balancers](/kubernetes/load-balancers). - **Helm charts and operators**. See [Helm](/kubernetes/helm). ### What works differently #### Ingress objects aren't served Ingress objects you create in a Cloider cluster aren't published, so they have no effect. To serve HTTP and HTTPS on a domain, create a [target](/kubernetes/expose-services), which routes a domain and path to a Service and provides the HTTPS certificate. For TCP and UDP, use a [LoadBalancer Service](/kubernetes/load-balancers). #### Nodes are managed for you Your pods run on nodes that Cloider operates. `kubectl get nodes` lists the nodes your cluster can schedule onto, but you don't administer them: you can't add, remove, resize or log in to nodes, and there are no node pools. Nodes can change over time, so don't pin workloads to specific node names. Workloads built to administer nodes, such as node-level monitoring agents, CNI plugins, node provisioners or containers that need host access, are designed for clusters where you own the machines. On Cloider the nodes are managed for you, so design workloads that don't depend on host access. #### A single control plane Every cluster has one control plane. A highly available control plane isn't available: the API rejects `is_ha: true` with `HA clusters are not supported at the moment!`. Your workloads can still run several replicas for availability. The control plane is sized for typical applications. Very large numbers of objects, or controllers that call the API server constantly, can make it slower. #### The control plane is part of your compute bill The control plane and the cluster DNS are billed like your own pods, on the larger of their requests and their use, so a running cluster always has a small compute cost. Pausing the cluster stops it. See [How is a cluster billed?](/kubernetes/overview#how-is-a-cluster-billed). #### Cloider components in `kube-system` Every cluster has a few components that Cloider installs and relies on, in `kube-system`: the cluster DNS, the metrics server and the `terminal` Deployment used by the browser terminal and `cloider cluster exec`. Don't delete or modify them. ### Limits #### Account and plan limits | Limit | Value | | --- | --- | | Clusters on the Free plan | 1 (paused clusters count, deleted ones don't) | | Clusters on a paid plan | No fixed limit in the API | | Creating clusters on a suspended account | Refused until the account is upgraded | | Read-only members and Read API tokens | Can list clusters, check status and download kubeconfigs, but not create, pause, resume or delete clusters, or run `exec` | #### Names | Limit | Value | | --- | --- | | Cluster name characters | Lower case letters, digits and `-` | | Cluster name start and end | Starts with a letter, doesn't end with `-` | | Cluster name length | At least 2 characters | | Unique cluster names | Not required. Duplicate names get labels `name-1`, `name-2`, ... | | Target: Service name + namespace + cluster name | At most 57 characters together | | Target paths | Start with `/`; letters, digits and `/ _ . -` only | #### Terminal and exec | Limit | Value | | --- | --- | | Browser terminal tabs | Up to 4 at a time | | `exec` stdin | 2 MiB | | `exec` timeout | Default 10 minutes, at most 1 hour | | Terminal URL token (`GET /api/clusters/{id}/terminal`) | Valid for about 2 minutes | | Terminal filesystem | Not persistent | #### Operations | Operation | Limit | | --- | --- | | Rename a cluster | Not possible | | Change the Kubernetes version | Not possible in place | | Use `kubectl`, the kubeconfig, the terminal or `exec` | Only while the cluster is `Ready` | | CLI `--wait` | Waits up to `--timeout`, default 15 minutes | | Terraform | Create, update (pause and resume) and delete wait up to 20 minutes by default | Limits for other products are in [Limits and quotas](/reference/limits-and-quotas). ### Next steps - [Troubleshooting](/kubernetes/troubleshooting) for the errors these limits produce. - [Expose services on a domain](/kubernetes/expose-services), the replacement for Ingress. - [Pricing](/reference/pricing). --- ## Kubernetes load balancers on Cloider A load balancer gives a Kubernetes Service its own public IP address. On Cloider you create one the standard Kubernetes way, with a Service of `type: LoadBalancer`: the cluster assigns it an external IP, traffic to that IP on the Service's ports reaches your pods, and the load balancer is billed for the time it exists. This page shows how to create one, point a domain at it, and keep track of what it costs. ### At a glance | | | | --- | --- | | **How to create one** | A Service with `spec.type: LoadBalancer` | | **What you get** | An external IP in the Service's `status.loadBalancer.ingress`, shown as `EXTERNAL-IP` by `kubectl get svc` | | **Protocols** | The Service's ports, TCP or UDP | | **TLS** | Passed through untouched. Your app terminates TLS itself | | **Billing** | Per load balancer per month, prorated to the second while the Service has an external IP. See [pricing](https://cloider.com/#pricing) | | **Where to see the cost** | **Usage** in the dashboard, **LoadBalancers Usage** table of each cluster | ### Load balancer or domain target? Cloider has two ways to make a Service reachable from the internet. Pick by protocol, and by how much you want to run yourself: | | Domain [target](/kubernetes/expose-services) to a Service | `type: LoadBalancer` | | --- | --- | --- | | **Traffic** | HTTP and HTTPS | Any TCP or UDP port | | **Address** | Your domain or a free `*.cloider.app` subdomain | A public IP, and a custom domain with a [LoadBalancer target](/domains/targets#point-a-domain-at-a-loadbalancer) | | **HTTPS certificate** | Automatic, issued and renewed by Cloider | Yours to issue and renew | | **Routing** | By path (`/`, `/api`, ...), with an optional rewrite | Whatever you run behind it, such as your own ingress controller or Gateway API | | **Several services on one address** | Yes, by path | One IP per Service, unless your own ingress controller routes them | | **Cost** | No load balancer charge | Billed per load balancer | | **Typical use** | Websites, APIs, dashboards | Databases you run yourself, MQTT, game servers, SMTP, custom TCP protocols, your own ingress controller or Gateway API | For a website or an HTTP API, a target is simpler and cheaper. Use a load balancer when you need a raw port, or a feature a target doesn't offer, such as routing by header, traffic splits, authentication in the proxy, mTLS or certificates of your own. Then you run the ingress controller or Gateway API implementation, its routes and its TLS certificates and their renewal yourself. ### Create a load balancer This example publishes an [MQTT broker](https://mosquitto.org/) (Eclipse Mosquitto) on TCP port 1883. ```yaml title="mqtt.yaml" apiVersion: v1 kind: Namespace metadata: name: iot --- apiVersion: v1 kind: ConfigMap metadata: name: mosquitto-config namespace: iot data: mosquitto.conf: | listener 1883 allow_anonymous true --- apiVersion: apps/v1 kind: Deployment metadata: name: mqtt namespace: iot spec: replicas: 1 selector: matchLabels: app: mqtt template: metadata: labels: app: mqtt spec: containers: - name: mosquitto image: eclipse-mosquitto:2.0.18 ports: - name: mqtt containerPort: 1883 volumeMounts: - name: config mountPath: /mosquitto/config resources: requests: cpu: 25m memory: 32Mi volumes: - name: config configMap: name: mosquitto-config --- apiVersion: v1 kind: Service metadata: name: mqtt namespace: iot spec: type: LoadBalancer selector: app: mqtt ports: - name: mqtt port: 1883 targetPort: mqtt protocol: TCP ``` :::warning[Open to the internet] A LoadBalancer Service is reachable by anyone on the internet. The example allows anonymous access to keep it short; in production configure authentication in the application itself. ::: **Dashboard** 1. Open **Kubernetes** in the sidebar and click the cluster's name to open its terminal. 2. Create `mqtt.yaml` with `cat > mqtt.yaml <<'EOF'`, paste the manifest, and end with `EOF`. 3. Apply it and watch the Service until it has an external IP: ```bash kubectl apply -f mqtt.yaml kubectl get service mqtt -n iot --watch ``` ```text title="Output" NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE mqtt LoadBalancer 10.43.52.190 1883:31883/TCP 2s mqtt LoadBalancer 10.43.52.190 203.0.113.25 1883:31883/TCP 6s ``` Press Ctrl-C to stop watching. **CLI** With `kubectl` and the cluster's kubeconfig: ```bash kubectl apply -f mqtt.yaml kubectl get service mqtt -n iot ``` Or through the cluster's terminal, without a kubeconfig: ```bash cloider cluster exec -i staging -- kubectl apply -f - < mqtt.yaml cloider cluster exec staging -- kubectl get service mqtt -n iot -o jsonpath='{.status.loadBalancer.ingress[0].ip}' ``` ```text title="Output" 203.0.113.25 ``` **Terraform** With the `kubernetes` provider configured from the cluster's `kube_config` (see [Deploy a workload](/kubernetes/deploy-a-workload)): ```hcl title="main.tf" resource "kubernetes_service_v1" "mqtt" { metadata { name = "mqtt" namespace = "iot" } spec { type = "LoadBalancer" selector = { app = "mqtt" } port { name = "mqtt" port = 1883 target_port = "mqtt" protocol = "TCP" } } # Waits until the load balancer has an IP wait_for_load_balancer = true } output "mqtt_ip" { value = kubernetes_service_v1.mqtt.status[0].load_balancer[0].ingress[0].ip } ``` **API** Apply the manifest through the cluster's exec endpoint, with the file base64 encoded as `stdin`: ```bash curl -N -X POST https://cloider.com/api/clusters/x8f2kq/exec \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"command\": [\"kubectl\", \"apply\", \"-f\", \"-\"], \"stdin\": \"$(base64 < mqtt.yaml | tr -d '\n')\"}" ``` See [Web terminal and exec](/kubernetes/web-terminal-and-exec) for the response format. **MCP** Ask your agent: ```text title="Prompt" Apply mqtt.yaml to the staging cluster and tell me the external IP of the mqtt service once it has one. ``` It calls `cluster_exec` to apply the file, then to read the IP: ```json title="Tool call: cluster_exec" {"cluster": "staging", "command": "kubectl get service mqtt -n iot -o jsonpath='{.status.loadBalancer.ingress[0].ip}'"} ``` ### Test the connection From your computer, with an MQTT client such as `mosquitto_sub` and `mosquitto_pub`: ```bash mosquitto_sub -h 203.0.113.25 -p 1883 -t test & mosquitto_pub -h 203.0.113.25 -p 1883 -t test -m "hello from cloider" ``` For any TCP service, `nc` tells you whether the port is open: ```bash nc -vz 203.0.113.25 1883 ``` ### Point a domain at the load balancer The simplest way is a [LoadBalancer target](/domains/targets#point-a-domain-at-a-loadbalancer) on a custom domain of its own, such as `mqtt.example.com`. It gives the domain an `A` record to the load balancer's IP and keeps the record on the IP if the Service is recreated: ```bash cloider target create mqtt.example.com --cluster production --loadbalancer default/mqtt ``` Clients then connect to `mqtt.example.com:1883`. In the dashboard, open the domain, click **Add target** and choose **LoadBalancer**. The target takes the whole domain and can't be combined with other targets, and Cloider adds no HTTPS: TLS, its certificates and their renewal are yours. To name the IP under a domain that serves other things, give it an `A` record yourself instead. If the domain's DNS is hosted on Cloider (see [Domains and DNS](/domains/overview)): ```bash cloider dns create example.com --type A --name mqtt --value 203.0.113.25 --ttl 300 ``` See [DNS records](/domains/dns-records) for the dashboard and other tools. :::note[Keep the Service to keep the IP] The external IP belongs to the Service. If you delete the Service and create it again, it can get a different IP: a LoadBalancer target follows it, an `A` record of your own must be updated. To change ports or selectors, edit the Service in place (`kubectl apply`) rather than deleting it. ::: ### Several ports on one load balancer One Service can expose several ports on the same IP, and it is still one load balancer: ```yaml title="game-server-service.yaml" apiVersion: v1 kind: Service metadata: name: game namespace: games spec: type: LoadBalancer selector: app: game ports: - name: game-udp port: 7777 targetPort: 7777 protocol: UDP - name: query port: 27015 targetPort: 27015 protocol: TCP ``` Every additional Service of type `LoadBalancer` is an additional load balancer with its own IP and its own charge. ### How are load balancers billed? A load balancer is billed per load balancer per month, prorated to the second, for as long as its Service has an external IP. A month is counted as 30 days. The current price is on the [pricing page](https://cloider.com/#pricing), in [Pricing](/reference/pricing) and in the output of `cloider pricing`. - **The meter starts** when the Service gets its external IP, and **stops** when the Service is deleted or changed to another type. - **A Service that stays ``** has no IP and isn't billed. - **Load balancers are counted by name** (`/`). If a Service of the same name is recreated, its time keeps adding to the same entry, and the entry records how many times it was recreated. - **A load balancer is billed whatever the cluster's status**, so delete LoadBalancer Services you don't need. See [Pause and resume](/kubernetes/pause-and-resume). #### See what each load balancer costs Open **Usage** in the dashboard sidebar. Each cluster has a **LoadBalancers Usage** table, with the price per load balancer per month in its title, and one row per load balancer: | Column | Meaning | | --- | --- | | **Name** | `/` | | **IP** | The external IP | | **Created At** | When the Service was created | | **Status** | Whether it is still active, or was removed during the month | | **Usage Time** | How long it has been active this month | | **Cost** | What it cost this month so far | The same data is in `GET /api/usage` (and `cloider usage -o json`), under each cluster's `loadbalancers` (this month) and `loadbalancers_prev` (last month): ```json title="Response (excerpt)" { "clusters": [ { "cluster_name": "staging", "loadbalancers": [ { "name": "iot/mqtt", "ip": "203.0.113.25", "created_at": "2026-10-12T08:30:11Z", "is_active": true, "total_time_seconds": 604800, "recreations": 0 } ] } ] } ``` See [Usage](/account/usage) for the rest of the usage data. ### List services and stop a load balancer List every LoadBalancer Service in a cluster with its IP: ```bash kubectl get services -A | grep -E "NAMESPACE|LoadBalancer" ``` Stop paying for one by deleting the Service, or by switching it to `ClusterIP` and publishing it with a [target](/kubernetes/expose-services) instead: ```bash kubectl delete service mqtt -n iot # or kubectl patch service mqtt -n iot -p '{"spec": {"type": "ClusterIP"}}' ``` ### Troubleshooting **`EXTERNAL-IP` stays ``**: run `kubectl describe service -n ` and read the events. If nothing explains it after a few minutes, open a [support request](/account/support) with the cluster id and the Service's name. **Connection refused or timeout on the IP**: check that the Service's `selector` matches the pods' labels (`kubectl get endpoints -n ` must list pod IPs), that `targetPort` is the port the container listens on, and that the app listens on `0.0.0.0` and not only on `127.0.0.1`. **Works for TCP, not for UDP**: check that the Service port has `protocol: UDP` and that the container port matches. **The IP changed**: the Service was deleted and recreated. A LoadBalancer target follows the new IP within a minute; update an `A` record of your own, and edit Services in place from now on. More cases are in [Kubernetes troubleshooting](/kubernetes/troubleshooting). ### Next steps - [Expose HTTP services on a domain](/kubernetes/expose-services), without a load balancer charge. - [DNS records](/domains/dns-records) to name your load balancer's IP. - [Pricing](/reference/pricing). --- ## Serverless Kubernetes on Cloider A Cloider Kubernetes cluster is a fully managed, serverless Kubernetes cluster: you get your own Kubernetes API, a kubeconfig and cluster admin rights, while Cloider runs the control plane and the nodes. There are no nodes to size, patch or scale, and you pay for the CPU and memory your workloads actually use, plus the volumes and load balancers they claim. This page explains how a cluster works, what it costs, what you can and can't do with it, and where to go next. If you just want a cluster now, go to [Create a cluster](/kubernetes/create-a-cluster). ### At a glance | | | | --- | --- | | **What it is** | A Kubernetes cluster with its own API server, reached with `kubectl`, Helm and any Kubernetes tool. Cloider manages the control plane and the nodes | | **Kubernetes versions** | The dashboard and API offer `1.34`, `1.33` and `1.32`, newest first. See [Limits and compatibility](/kubernetes/limits-and-compatibility) | | **Billing** | Compute in compute units (CPU or memory, whichever is larger), volumes per GiB of provisioned capacity, load balancers per load balancer for the time they run. All metered and prorated. See [How is a cluster billed?](#how-is-a-cluster-billed) | | **Access** | A kubeconfig with a client certificate that is cluster admin, a browser terminal, and `cloider cluster exec` with `kubectl` and `helm` preinstalled | | **Networking** | Services of type `LoadBalancer` get a public IP. HTTP(S) services are published on a domain with a [target](/domains/targets), with automatic HTTPS. Ingress objects inside the cluster are not served | | **Storage** | Standard PersistentVolumeClaims, billed per GiB provisioned | | **Lifecycle** | Create, pause, resume, delete. A paused cluster keeps its state and isn't billed for compute | | **Plans** | The Free plan allows one cluster at a time. Paid accounts can create more | | **Tools** | [Dashboard](https://cloider.com/dashboard/clusters), [CLI](/cli/commands/cluster), [Terraform](/terraform/resources/cluster), [REST API](/api/endpoints/clusters), [MCP](/mcp/tools) | ### What is a serverless Kubernetes cluster? A serverless Kubernetes cluster is a Kubernetes cluster where the provider runs all the infrastructure underneath and bills you for what your pods use, not for servers. On Cloider, each cluster has its own Kubernetes API server, its own namespaces, RBAC, Secrets and objects, and is isolated from every other cluster. Your pods are scheduled onto nodes that Cloider operates, patches and scales. In a classic managed Kubernetes service you still choose node pools, machine sizes and autoscaling rules, and you pay for every node whether it is busy or idle. On Cloider you skip that layer entirely: - **No node pools.** You never pick an instance type or a node count. You declare pods with CPU and memory requests, and they are scheduled. - **No idle capacity bill.** A pod that requests 250 millicores and uses 250 millicores costs 250 millicores. Nothing is charged for spare room on a node. - **No control plane to babysit.** The API server, the cluster DNS and the metrics server are installed and run for you. - **Standard Kubernetes.** `kubectl`, Helm, Kustomize, Lens, k9s, Terraform's `kubernetes` and `helm` providers, and CI tools all work, because the cluster speaks the standard Kubernetes API. :::info[New to Kubernetes?] Kubernetes is an open source system that runs containers for you. You describe what you want (for example "three copies of my web server, reachable on port 80") in YAML files called manifests, and Kubernetes keeps it that way. The most common objects are: - a **Pod**, one or more containers that run together, - a **Deployment**, which keeps a number of identical pods running and replaces them on updates, - a **Service**, a stable network address in front of a set of pods, - a **PersistentVolumeClaim** (PVC), a request for disk space that survives pod restarts, - a **ConfigMap** and a **Secret**, configuration and credentials handed to pods. [Deploy a workload](/kubernetes/deploy-a-workload) walks through each of them with complete examples. ::: ### How a Cloider cluster works When you create a cluster, Cloider provisions a dedicated Kubernetes control plane for it and gives it a public API endpoint, such as `https://staging-x8f2kq.cloider.com`. The endpoint name is the cluster name followed by its id. The kubeconfig you download points `kubectl` at that endpoint and authenticates you with a client certificate that is cluster admin. ```mermaid flowchart LR subgraph You K[kubectl / Helm / Lens] T[Browser terminal
cloider cluster exec] end subgraph Cloider["Your Cloider cluster"] API[Kubernetes API
https://staging-x8f2kq.cloider.com] subgraph Workloads D[Deployments and pods] S[Services] V[(Persistent volumes)] end end U[Internet users] K -->|kubeconfig| API T --> API API --> D D --> S D --> V U -->|https://shop.example.com
domain target| S U -->|public IP
type: LoadBalancer| S ``` Everything you create through that API, Deployments, Services, PersistentVolumeClaims, ConfigMaps, Secrets, RBAC rules, lives in your cluster only. Pods run on Cloider's managed nodes, so `kubectl get nodes` lists nodes you can see but don't administer. Each cluster comes with: - **Cluster DNS**, so pods reach Services by name (`http://api.shop.svc.cluster.local`). - **The Kubernetes metrics server**, so `kubectl top pods` and the HorizontalPodAutoscaler work out of the box. - **A terminal Deployment** in the `kube-system` namespace that powers the [browser terminal](/kubernetes/web-terminal-and-exec). Don't delete it, or the browser terminal and `cloider cluster exec` stop working. ### What can you run on a cluster? You can run any containerized workload that a standard Kubernetes cluster runs: web apps and APIs, background workers, cron jobs, queues, and stateful services with persistent volumes. Common examples: - A web application as a Deployment and a Service, published on your domain with HTTPS. See [Deploy a workload](/kubernetes/deploy-a-workload) and [Expose services on a domain](/kubernetes/expose-services). - Off the shelf software installed with Helm charts, such as WordPress, Grafana or Redis. See [Helm](/kubernetes/helm). - Services that need a raw TCP or UDP port, published with a `LoadBalancer` Service. See [Load balancers](/kubernetes/load-balancers). - Stateful workloads with PersistentVolumeClaims. See [Persistent volumes](/kubernetes/persistent-volumes). - Private images from the [Cloider container registry](/container-registry/overview). See [Pull private images](/kubernetes/pull-private-images). Your pods can also reach your [Cloider databases](/databases/overview) over the network like any other client. See [Connect to a database](/databases/connect) for the connection details. ### What can't you do? Cloider runs the infrastructure under your cluster, so a few things that you control in a self-managed cluster are managed for you: - **You don't manage nodes.** You can't add, remove, resize, cordon or SSH into nodes, and there are no node pools to configure. - **Ingress objects are not served.** An `Ingress` you create in the cluster isn't published to the internet. To serve HTTP(S) on a domain, create a [target](/kubernetes/expose-services) that routes the domain to your Service. Cloider then handles the ingress and the HTTPS certificate. - **No highly available control plane option.** Every cluster has a single control plane. The API rejects `is_ha: true` with `HA clusters are not supported at the moment!`. - **No in-place upgrades or renames.** A cluster keeps the name and Kubernetes version it was created with. To move to a newer version, create a new cluster and redeploy into it. The full list, with versions and the errors you may see, is in [Limits and compatibility](/kubernetes/limits-and-compatibility). ### How is a cluster billed? A Cloider cluster is billed on three meters: compute, volumes and load balancers. There is no fixed fee per cluster and no charge for nodes. Prices are on the [pricing page](https://cloider.com/#pricing) and in [Pricing](/reference/pricing), and `cloider pricing` prints the current rates. #### Compute: CPU and memory, whichever is larger Compute is measured in **compute units**. Every few seconds Cloider samples every running pod in your cluster and, for each container, counts the larger of what it requests and what it actually uses, for CPU and for memory separately. The totals are converted to compute units: one compute unit is one vCPU, or the amount of memory that the price list pairs with one vCPU (the `cpu_to_mem_ratio` rate), whichever of the two gives the higher number. Compute units are priced per month and prorated to the second, with a month counted as 30 days. In practice this means: - **A pod that sets no requests is billed on what it uses.** A pod that requests more than it uses is billed on its request, because that capacity is reserved for it. - **Only pods in the `Running` phase count.** Pending, completed and failed pods cost nothing. - **The cluster's own components count too.** The control plane and the cluster DNS are metered like your own pods, so a running cluster with no workloads still accrues a small compute charge. The pricing calculator on the home page shows an estimate of it as **Control plane**. - **Paused clusters have no running pods**, so they accrue no compute. See [Pause and resume](/kubernetes/pause-and-resume). :::tip[Set requests that match your app] Because a container is billed on the larger of its request and its use, a request far above what the container really uses costs you money for nothing. Set requests close to the real steady state use, and let `kubectl top pods` tell you what that is. ::: #### Volumes: per GiB provisioned Volumes are billed on the **capacity** of every PersistentVolume bound to your cluster's claims, in GiB, per month, prorated over time. A 10 GiB claim is billed as 10 GiB whether it holds 1 GiB or 9 GiB of data. Only your own claims are billed, not the storage the control plane uses. Volumes keep being billed while a cluster is paused, because the data is kept. See [Persistent volumes](/kubernetes/persistent-volumes). #### Load balancers: per load balancer, prorated Every Service of type `LoadBalancer` that has been given an external IP is billed per load balancer per month, prorated to the second it was active. Delete the Service and the meter stops. The **Usage** page shows each load balancer by name and IP, with its usage time and cost. See [Load balancers](/kubernetes/load-balancers). #### Where to see what a cluster costs Open **Usage** in the dashboard sidebar. Each cluster shows its average CPU, memory and persistent volume use for today and this month, the cost to date, a forecast for this month and next month, and a **LoadBalancers Usage** table. The same data is available with `cloider usage` and the [`GET /api/usage`](/api/endpoints/account) endpoint. See [Usage](/account/usage). ### Cluster names, labels and ids Every cluster has three identifiers: | Field | Example | What it is | | --- | --- | --- | | `cluster_name` | `staging` | The name you chose. Lower case letters, digits and `-`, starting with a letter, at least 2 characters. Names don't have to be unique in an account | | `cluster_label` | `staging` or `staging-1` | The name the dashboard shows. It is the name, with `-1`, `-2` and so on appended when another cluster of the account already uses it | | `hashed_id` | `x8f2kq` | The cluster's id, used in API paths and in the API endpoint's host name | The CLI and MCP tools accept a name, label or id wherever a cluster is expected. When two clusters share a name, use the label or the id. ### Statuses A cluster is always in one of these states: | Status | Meaning | | --- | --- | | `Creating` | The control plane is being provisioned | | `Starting` | The control plane is up and the cluster is finishing its setup (kubeconfig and terminal) | | `Ready` | The cluster is running. You can download the kubeconfig, open the terminal and deploy | | `Pausing` | The cluster is being paused | | `Paused` | The cluster is stopped. Its objects and volumes are kept, no pods run | | `Terminating` | The cluster and everything in it are being deleted | A deleted cluster ends as `Terminated` and disappears from lists. See [Status values](/reference/status-values) for every product's states. ### Manage clusters with any tool Everything on this section's pages can be done in five ways, shown in tabs on each how-to page: | Tool | Best for | Start here | | --- | --- | --- | | Dashboard | Creating your first cluster, the browser terminal, watching usage | **Kubernetes** in the sidebar of [cloider.com/dashboard](https://cloider.com/dashboard/clusters) | | CLI | Day to day work, scripts and CI | [`cloider cluster`](/cli/commands/cluster) | | Terraform | Clusters as code, with the `kubernetes` and `helm` providers on top | [`cloider_cluster`](/terraform/resources/cluster) | | REST API | Your own automation | [Clusters API](/api/endpoints/clusters) | | MCP | Letting an AI agent create clusters and run `kubectl` | [MCP tools](/mcp/tools) | ### Next steps Create a cluster from the dashboard, CLI, Terraform, the API or an AI agent. Download the kubeconfig, merge contexts, and use Lens or k9s. A complete Deployment, Service, ConfigMap and Secret, step by step. Route a domain to a Service with automatic HTTPS. --- ## Pause and resume a Kubernetes cluster Pausing a Cloider cluster stops everything that runs in it, its control plane and all its pods, while keeping its configuration and data. A paused cluster accrues no compute charges; only its volumes (and any load balancer that still holds an IP) are billed. When you resume it, the control plane starts again and your workloads come back as they were. Pausing is ideal for development, staging and demo clusters that are only needed part of the time. ### At a glance | | Running (`Ready`) | Paused (`Paused`) | | --- | --- | --- | | **Control plane** | Running | Stopped | | **Pods** | Running | Stopped | | **Kubernetes objects** (Deployments, Services, ConfigMaps, Secrets, ...) | Kept | Kept | | **PersistentVolumeClaims and their data** | Kept | Kept | | **Container filesystems and `emptyDir` volumes** | Kept while the pod runs | Lost | | **`kubectl`, kubeconfig, browser terminal, exec** | Available | Unavailable | | **Domain targets** | Serve traffic | Don't answer, since no pods run | | **Compute billing** | Yes | No | | **Volume billing** | Yes | Yes | | **Load balancer billing** | While the Service has an external IP | While the Service has an external IP | ### What happens when you pause a cluster? 1. The cluster's status changes to `Pausing`. 2. Cloider stops the cluster's control plane and removes its running pods. 3. The status changes to `Paused`. Everything you created through the Kubernetes API stays stored with the cluster: namespaces, Deployments, StatefulSets, Services, ConfigMaps, Secrets, RBAC rules and custom resources. PersistentVolumeClaims and the data on them are kept too. What is lost is anything that only lived inside a running container: files written to the container's own filesystem and to `emptyDir` volumes, and in-memory state. ### What happens when you resume it? 1. The status changes to `Starting`. 2. Cloider starts the control plane, then waits for the cluster DNS and the terminal to be ready. 3. The status changes to `Ready`, and your workloads' pods start again. They mount their PersistentVolumeClaims with the data they had. Containers start fresh, as after a node restart, so applications should cope with starting from scratch: run migrations idempotently, reconnect to databases and rebuild caches. ### Pause a cluster **Dashboard** 1. Open **Kubernetes** in the sidebar. 2. Click the actions button (the three dots) on the cluster's row and choose **Pause**. It is enabled only while the cluster is **Ready**. 3. The **Pause Cluster** dialog asks you to confirm. Click **Pause cluster**. The cluster's status changes to **Pausing**, then **Paused**. **CLI** ```bash cloider cluster pause staging --wait ``` ```text title="Output" ✓ Pausing cluster staging ⠋ Waiting for cluster staging ✓ Cluster staging is Paused ``` Without `--wait`, the command returns once pausing has started. `--timeout` (default `15m`) sets how long `--wait` waits. **Terraform** Set `paused` on the cluster and apply: ```hcl title="main.tf" variable "staging_paused" { description = "Pauses the staging cluster, which keeps its state and isn't billed for compute." type = bool default = false } resource "cloider_cluster" "staging" { name = "staging" paused = var.staging_paused } ``` ```bash terraform apply -var staging_paused=true ``` Terraform waits for the pause or the resume to finish, up to 20 minutes by default (the `update` timeout). **API** ```bash curl -X POST https://cloider.com/api/clusters/x8f2kq/pause \ -H "Authorization: Bearer $CLOIDER_TOKEN" ``` The response is the account's cluster list, with this cluster now `Pausing`: ```json title="Response" [ { "hashed_id": "x8f2kq", "cluster_name": "staging", "cluster_label": "staging", "cluster_version": "1.34", "is_ha": false, "status": "Pausing", "created_at": "2026-10-19T09:41:07Z" } ] ``` Poll `GET /api/clusters/x8f2kq/status` until it answers `Paused`. **MCP** Ask your agent: ```text title="Prompt" Pause the staging cluster. ``` It calls the `pause_cluster` tool: ```json title="Tool call: pause_cluster" {"cluster": "staging", "wait": true} ``` ### Resume a cluster **Dashboard** 1. Open **Kubernetes** in the sidebar. 2. Click the actions button on the paused cluster's row and choose **Resume**. 3. In the **Resume Cluster** dialog, click **Resume cluster**. The status changes to **Starting**, then **Ready**. The cluster name becomes a link to its terminal again. **CLI** ```bash cloider cluster resume staging --wait ``` ```text title="Output" ✓ Resuming cluster staging ⠋ Waiting for cluster staging ✓ Cluster staging is Ready ``` **Terraform** ```bash terraform apply -var staging_paused=false ``` **API** ```bash curl -X POST https://cloider.com/api/clusters/x8f2kq/resume \ -H "Authorization: Bearer $CLOIDER_TOKEN" ``` The response is the cluster list, with this cluster `Starting`. Poll `GET /api/clusters/x8f2kq/status` until it answers `Ready`. **MCP** Ask your agent: ```text title="Prompt" Resume the staging cluster and tell me when it is ready. ``` It calls the `resume_cluster` tool: ```json title="Tool call: resume_cluster" {"cluster": "staging", "wait": true} ``` ### Verify After resuming, check that your workloads are back: ```bash kubectl get pods -A kubectl get pvc -A ``` Pods should be `Running` again, and claims `Bound` to the same volumes as before. ### How much does a paused cluster cost? A paused cluster runs no pods, so its compute meter is zero for as long as it is paused. You keep paying for: - **Volumes**, per GiB of provisioned capacity, because their data is kept. See [Persistent volumes](/kubernetes/persistent-volumes#how-are-volumes-billed). - **Load balancers**, which are metered whenever a LoadBalancer Service has an external IP, whatever the cluster's status. To be sure a load balancer isn't billed while the cluster is paused, delete its Service first and recreate it after resuming (it may get a new IP). See [Load balancers](/kubernetes/load-balancers#how-are-load-balancers-billed). The **Usage** page in the dashboard shows the effect: a paused cluster's daily compute cost drops to zero while its volume cost continues. :::tip[Pause or delete?] Pause a cluster you will need again: it comes back with all its objects and data. If you won't need it, [delete it](/kubernetes/delete-a-cluster) to stop volume and load balancer charges as well. On the Free plan, a paused cluster still counts as your one cluster. ::: ### Pause and resume on a schedule Cloider doesn't pause clusters on a schedule by itself, but the CLI makes it a two-line job in any scheduler. This GitHub Actions workflow pauses `staging` every weekday evening and resumes it every weekday morning (UTC), with an API token stored as the `CLOIDER_TOKEN` secret: ```yaml title=".github/workflows/staging-schedule.yml" name: Staging schedule on: schedule: - cron: "0 19 * * 1-5" # pause at 19:00 UTC, Monday to Friday - cron: "0 7 * * 1-5" # resume at 07:00 UTC, Monday to Friday workflow_dispatch: jobs: schedule: runs-on: ubuntu-latest env: CLOIDER_TOKEN: ${{ secrets.CLOIDER_TOKEN }} steps: - name: Install the Cloider CLI run: curl -fsSL https://cloider.com/install.sh | sh - name: Pause or resume run: | if [ "${{ github.event.schedule }}" = "0 19 * * 1-5" ]; then cloider cluster pause staging --wait else cloider cluster resume staging --wait fi ``` The token needs the **Read & Write** permission. See [API tokens](/account/api-tokens) and [CI/CD with GitHub Actions](/guides/ci-cd-with-github-actions). ### Troubleshooting | Problem | Cause and fix | | --- | --- | | **Pause** is disabled in the dashboard | The cluster isn't `Ready`, for example still `Creating`, `Starting` or already `Pausing`. Wait for `Ready` | | **Resume** is disabled | The cluster is still `Pausing`. Wait until it is `Paused` | | `kubectl` times out, the terminal and kubeconfig are unavailable | The cluster is paused. Resume it | | `This cluster is paused. Resume it to run a command!` (HTTP 409) | `cloider cluster exec` was run against a paused cluster. Resume it first | | `You have read-only access to this account, ...` (HTTP 403) | Your role in the account is read-only. Ask an owner or admin | | `This API token is read only!` (HTTP 403) | Use a **Read & Write** token. The CLI exits with code `5` | | A pod lost files after resuming | The files were in the container filesystem or an `emptyDir`. Store data that must survive on a [PersistentVolumeClaim](/kubernetes/persistent-volumes) | | Some pods don't come back | Bare pods without a controller aren't guaranteed to return. Run workloads as Deployments, StatefulSets, DaemonSets or Jobs | | The cluster stays `Starting` for a long time | Wait a few more minutes, then open a [support request](/account/support) with the cluster id | ### Next steps - [Delete a cluster](/kubernetes/delete-a-cluster) you no longer need. - [Billing model](/concepts/billing-model) for how paused resources are charged across Cloider. - [Usage](/account/usage) to watch the effect on your bill. --- ## Persistent volumes on Cloider Kubernetes A persistent volume is disk space for your pods that outlives them: when a pod restarts, moves or is replaced, its data is still there. On Cloider you get one by creating a standard Kubernetes **PersistentVolumeClaim** (PVC); the cluster provisions a volume of the requested size and binds it to the claim. Volumes are billed per GiB of provisioned size per month. This page shows how to create claims, use them from Deployments and StatefulSets, and manage their cost. ### At a glance | | | | --- | --- | | **How to get a volume** | Create a PersistentVolumeClaim; the volume is provisioned for you | | **Storage classes** | Provided by Cloider and visible with `kubectl get storageclass`. A claim without `storageClassName` uses the default class | | **Billing** | Per GiB of the volume's provisioned capacity, per month, prorated. A 10 GiB claim is billed as 10 GiB however full it is | | **While paused** | Volumes and their data are kept, and still billed | | **Where to see usage** | **Usage** in the dashboard, **Persistent Volumes** in each cluster's average usage | ### Concepts for beginners A container's own filesystem is temporary: everything written to it disappears when the container restarts. To keep data, a pod mounts a volume: - A **PersistentVolumeClaim** (PVC) is your request: "I need 10 GiB of storage". You create it in a namespace, like a Deployment. - A **PersistentVolume** (PV) is the actual disk. On Cloider it is created automatically when you create a claim, and bound to it. - A **StorageClass** says what kind of disk to provision. You pick one with `storageClassName`, or leave it out to use the default. - A pod uses the claim by listing it under `volumes` and mounting it with `volumeMounts`. Deleting a pod or a Deployment doesn't delete its claims. The data stays until you delete the PVC. ### See the available storage classes ```bash kubectl get storageclass ``` The output lists each class, which one is marked `(default)`, its `RECLAIMPOLICY` (what happens to the volume when the claim is deleted) and `ALLOWVOLUMEEXPANSION` (whether claims of that class can grow). Use the default class unless you have a reason to choose another one. ### Create a volume This claim asks for 10 GiB from the default storage class: ```yaml title="uploads-pvc.yaml" apiVersion: v1 kind: PersistentVolumeClaim metadata: name: uploads namespace: shop spec: accessModes: - ReadWriteOnce resources: requests: storage: 10Gi ``` `ReadWriteOnce` means the volume is mounted read-write by pods on one node at a time, the mode every storage class supports. Use the `Gi` suffix (gibibytes), as volumes are measured and billed in GiB. **Dashboard** 1. Open **Kubernetes** in the sidebar and click the cluster's name to open its terminal. 2. Create the file with `cat > uploads-pvc.yaml <<'EOF'`, paste the manifest and end with `EOF`. 3. Apply it: ```bash kubectl create namespace shop --dry-run=client -o yaml | kubectl apply -f - kubectl apply -f uploads-pvc.yaml kubectl get pvc -n shop ``` **CLI** ```bash kubectl apply -f uploads-pvc.yaml kubectl get pvc -n shop ``` Or through the cluster's terminal: ```bash cloider cluster exec -i staging -- kubectl apply -f - < uploads-pvc.yaml ``` **Terraform** With the `kubernetes` provider configured from the cluster's `kube_config`: ```hcl title="main.tf" resource "kubernetes_persistent_volume_claim_v1" "uploads" { metadata { name = "uploads" namespace = "shop" } spec { access_modes = ["ReadWriteOnce"] resources { requests = { storage = "10Gi" } } } # Some storage classes only bind once a pod uses the claim wait_until_bound = false } ``` **API** Apply the manifest through the cluster's exec endpoint, base64 encoded as `stdin`: ```bash curl -N -X POST https://cloider.com/api/clusters/x8f2kq/exec \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"command\": [\"kubectl\", \"apply\", \"-f\", \"-\"], \"stdin\": \"$(base64 < uploads-pvc.yaml | tr -d '\n')\"}" ``` **MCP** Ask your agent: ```text title="Prompt" Create a 10Gi PersistentVolumeClaim called uploads in the shop namespace of the staging cluster. ``` It calls `cluster_exec` with the manifest on stdin: ```json title="Tool call: cluster_exec" {"cluster": "staging", "command": "kubectl apply -f -", "stdin": "apiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n name: uploads\n namespace: shop\nspec:\n accessModes: [ReadWriteOnce]\n resources:\n requests:\n storage: 10Gi\n"} ``` Check the claim: ```bash kubectl get pvc -n shop -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,CAPACITY:.status.capacity.storage,MODES:.status.accessModes ``` ```text title="Output" NAME STATUS CAPACITY MODES uploads Bound 10Gi [ReadWriteOnce] ``` A claim can show `Pending` until a pod uses it, when its storage class provisions volumes on first use. `kubectl describe pvc uploads -n shop` shows why it is waiting. ### Use the volume in a Deployment Mount the claim in the pod template. With a `ReadWriteOnce` volume, use the `Recreate` strategy so the old pod releases the volume before the new one starts: ```yaml title="web-with-uploads.yaml" apiVersion: apps/v1 kind: Deployment metadata: name: web namespace: shop spec: replicas: 1 strategy: type: Recreate selector: matchLabels: app: web template: metadata: labels: app: web spec: containers: - name: web image: nginx:1.27-alpine ports: - containerPort: 80 volumeMounts: - name: uploads mountPath: /usr/share/nginx/html resources: requests: cpu: 25m memory: 32Mi volumes: - name: uploads persistentVolumeClaim: claimName: uploads ``` Write a file to the volume, delete the pod, and check that the file is still there in the new pod: ```bash kubectl apply -f web-with-uploads.yaml kubectl exec -n shop deploy/web -- sh -c 'echo "

stored on a volume

" > /usr/share/nginx/html/index.html' kubectl delete pod -n shop -l app=web kubectl rollout status deploy/web -n shop kubectl exec -n shop deploy/web -- cat /usr/share/nginx/html/index.html ``` ```text title="Output"

stored on a volume

``` ### Use volumes in a StatefulSet For databases, queues and other stateful software that runs several replicas, each with its own data, use a **StatefulSet** with `volumeClaimTemplates`. Each replica gets its own claim (`data-cache-0`, `data-cache-1`, ...), which follows it across restarts. ```yaml title="cache-statefulset.yaml" apiVersion: v1 kind: Service metadata: name: cache namespace: shop spec: clusterIP: None selector: app: cache ports: - name: redis port: 6379 --- apiVersion: apps/v1 kind: StatefulSet metadata: name: cache namespace: shop spec: serviceName: cache replicas: 1 selector: matchLabels: app: cache template: metadata: labels: app: cache spec: containers: - name: redis image: redis:7.4-alpine args: ["--appendonly", "yes"] ports: - name: redis containerPort: 6379 volumeMounts: - name: data mountPath: /data resources: requests: cpu: 50m memory: 64Mi limits: memory: 256Mi volumeClaimTemplates: - metadata: name: data spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 2Gi ``` ```bash kubectl apply -f cache-statefulset.yaml kubectl get statefulset,pvc -n shop ``` Scaling a StatefulSet down doesn't delete the claims of the removed replicas, so their volumes stay billed until you delete them. :::tip[Consider a managed database] For PostgreSQL, MySQL, MongoDB or Redis, a [Cloider managed database](/databases/overview) gives you backups, SSL and restores without running them in your cluster. Your pods connect to it like any other client. See [Connect to a database](/databases/connect). ::: ### Grow a volume If the claim's storage class allows expansion (`ALLOWVOLUMEEXPANSION` is `true` in `kubectl get storageclass`), raise the request in place: ```bash kubectl patch pvc uploads -n shop -p '{"spec": {"resources": {"requests": {"storage": "20Gi"}}}}' kubectl get pvc uploads -n shop -w ``` Billing follows the new capacity from the moment the volume has grown. Kubernetes doesn't shrink volumes: to make one smaller, create a new claim, copy the data, and delete the old claim. ### How are volumes billed? Volumes are billed per GiB of **provisioned capacity** per month, prorated over time, with a month counted as 30 days. Every few seconds Cloider adds up the capacity of all the volumes bound to your cluster's claims; the daily average is what you pay for. - **Capacity, not content.** A 10 GiB claim with 200 MiB of data is billed as 10 GiB. Size claims for what you need now and grow them later if the class allows it. - **Claims are billed while they exist**, whether or not a pod uses them, and also while the cluster is [paused](/kubernetes/pause-and-resume). - **Only your own claims are billed**, not the storage the cluster's control plane uses. - **Deleting a cluster deletes its volumes**, and their billing stops. See the current price per GiB on the [pricing page](https://cloider.com/#pricing) or with `cloider pricing`, and your clusters' average volume use under **Usage** in the dashboard, where **Persistent Volumes** shows the average GiB for today and for this month. ### Delete a volume Find claims you no longer need, including ones left behind by Helm releases and scaled-down StatefulSets: ```bash kubectl get pvc -A kubectl delete pvc uploads -n shop ``` Then check that the volume is gone: ```bash kubectl get pv ``` What happens to the volume depends on the storage class's reclaim policy. With `Delete`, the volume and its data are deleted with the claim and billing stops. With `Retain`, the volume stays as `Released` and keeps being billed; delete it with `kubectl delete pv ` once you are sure you don't need the data. :::danger[Deleting a claim deletes its data] With the `Delete` reclaim policy, deleting a PersistentVolumeClaim permanently deletes the data on it. Copy anything you need first, for example to a [bucket](/object-storage/overview). ::: ### Back up a volume Cloider has no snapshot or restore feature for the volumes in your cluster, so back up the data that matters yourself. A simple approach is a CronJob that archives the volume to a [Cloider bucket](/object-storage/overview) with an S3 client. See [S3 tools](/object-storage/s3-tools) for the clients and [Access keys](/object-storage/access-keys) for credentials, and [Back up a database to a bucket](/guides/back-up-a-database-to-a-bucket) for a complete example of the pattern. ### Troubleshooting **PVC stays `Pending`**: run `kubectl describe pvc -n `. `waiting for first consumer to be created before binding` means the class binds on first use; it binds when a pod mounts it. A misspelled `storageClassName` never binds; remove the field to use the default class. **Pod stays `ContainerCreating` with `Multi-Attach error`**: a `ReadWriteOnce` volume is still attached to another pod. Use `strategy: Recreate` in Deployments that mount one, or a StatefulSet. **Pod can't write to the volume (`Permission denied`)**: the container runs as a user that doesn't own the mount. Set `securityContext.fsGroup` in the pod spec to the container's group id. **The bill shows more volume GiB than expected**: list every claim with `kubectl get pvc -A` and every volume with `kubectl get pv`. Look for claims from uninstalled Helm charts, scaled-down StatefulSets, and `Released` volumes with a `Retain` policy. More cases are in [Kubernetes troubleshooting](/kubernetes/troubleshooting). ### Next steps - [Helm](/kubernetes/helm): charts with `persistence.enabled` create claims for you. - [Pause and resume](/kubernetes/pause-and-resume): what happens to volumes while paused. - [Limits and compatibility](/kubernetes/limits-and-compatibility). --- ## Pull private images from the Cloider registry To run an image from a private registry, Kubernetes needs credentials to pull it. For the [Cloider container registry](/container-registry/overview) at `registry.cloider.com`, you create a **pull-only registry token**, store it in the cluster as a Secret of type `kubernetes.io/dockerconfigjson`, and reference that Secret as an `imagePullSecret` in your pods. This page shows the whole flow, from pushing an image to running it. ### Before you begin - A cluster in the `Ready` status. See [Create a cluster](/kubernetes/create-a-cluster). - A registry project with an image in it, for example `registry.cloider.com/acme/api:1.0`. See [Create a project](/container-registry/create-a-project) and [Push and pull images](/container-registry/push-and-pull-images). - A role that can create registry tokens. ### How it works ```mermaid flowchart LR T[Registry token
pull only, project acme] --> S[Secret cloider-registry
namespace shop] S --> P[Pod spec
imagePullSecrets] P -->|pulls registry.cloider.com/acme/api:1.0| R[(Cloider registry)] ``` - A **registry token** is a username and secret that can log in to the registry. Give the cluster its own token with the **Pull only** permission, limited to the projects it needs, so a leaked cluster secret can't push or delete images. - A Kubernetes **Secret of type `kubernetes.io/dockerconfigjson`** holds that login. Secrets are namespaced, so create it in every namespace whose pods pull private images. - **`imagePullSecrets`** in a pod spec (or on the namespace's ServiceAccount) tells Kubernetes to use the Secret when it pulls. ### Step 1: Create a pull-only registry token **Dashboard** 1. Open **Registry** in the sidebar and go to the **Tokens** tab. 2. Click **Create Token**. 3. In **Name**, type `staging-cluster`. 4. In **Projects**, choose **Only specific projects** and pick `acme`. 5. In **Permission**, choose **Pull only**. 6. Choose when it **Expires** (the expiry can't be changed later). 7. Click **Create token**. The **Token created** dialog shows the **Registry host**, **Username**, **Secret**, a **Docker login** command and a ready **Kubernetes pull secret** command. Copy the pull secret command now: the secret isn't shown again. Then click **I've saved the secret**. **CLI** ```bash cloider registry token create --label staging-cluster --project acme --read-only ``` ```text title="Output" ✓ Created registry token staging-cluster Username: acme Secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Host: registry.cloider.com Projects: acme Permission: Pull Expires: never Docker: echo 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' | docker login registry.cloider.com -u 'acme' --password-stdin Kubernetes pull secret: kubectl create secret docker-registry cloider-registry --docker-server=registry.cloider.com --docker-username='acme' --docker-password='xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ! The secret is not shown again, keep it now. ``` Add `--expires-days 365` to make the token expire after a year (`0`, the default, means never). See [`cloider registry token`](/cli/commands/registry). **Terraform** ```hcl title="main.tf" resource "cloider_registry" "acme" { name = "acme" } # For the cluster to pull images with resource "cloider_registry_token" "pull" { label = "staging-cluster" projects = [cloider_registry.acme.id] read_only = true } ``` The token's `username` and `secret` are used in step 2. See [`cloider_registry_token`](/terraform/resources/registry-token). **API** Registry tokens are created with `POST /api/registry/tokens`. See the [Registry tokens API](/api/endpoints/registry-tokens) for the request and response, which includes the secret once. **MCP** Ask your agent: ```text title="Prompt" Create a pull-only registry token called staging-cluster for the acme project. ``` It calls the `create_registry_token` tool: ```json title="Tool call: create_registry_token" {"label": "staging-cluster", "projects": ["acme"], "read_only": true} ``` The answer holds the username and the secret, which is never shown again. The secret ends up in the agent's context, so prefer the dashboard, CLI or Terraform for production credentials. ### Step 2: Create the image pull secret in the cluster Run the pull secret command from step 1, adding the namespace your pods run in with `-n`. Without `-n`, the Secret is created in the `default` namespace only. **Dashboard** Open the cluster's terminal (**Kubernetes**, then the cluster's name) and paste the command, with `-n shop` added: ```bash kubectl create namespace shop --dry-run=client -o yaml | kubectl apply -f - kubectl create secret docker-registry cloider-registry -n shop \ --docker-server=registry.cloider.com \ --docker-username='acme' \ --docker-password='xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ``` ```text title="Output" secret/cloider-registry created ``` **CLI** With `kubectl` locally: ```bash kubectl create secret docker-registry cloider-registry -n shop \ --docker-server=registry.cloider.com \ --docker-username='acme' \ --docker-password="$REGISTRY_SECRET" ``` Or without a kubeconfig, through the cluster's terminal. Passing the secret on stdin keeps it out of your shell history and the process list: ```bash printf '%s' "$REGISTRY_SECRET" | cloider cluster exec -i staging -- sh -c \ 'kubectl create secret docker-registry cloider-registry -n shop --docker-server=registry.cloider.com --docker-username=acme --docker-password="$(cat)"' ``` **Terraform** With the `kubernetes` provider configured from the cluster's `kube_config`: ```hcl title="main.tf" resource "kubernetes_secret_v1" "registry" { metadata { name = "cloider-registry" namespace = "shop" } type = "kubernetes.io/dockerconfigjson" data = { ".dockerconfigjson" = jsonencode({ auths = { (cloider_registry.acme.host) = { username = cloider_registry_token.pull.username password = cloider_registry_token.pull.secret auth = base64encode("${cloider_registry_token.pull.username}:${cloider_registry_token.pull.secret}") } } }) } } ``` **API** Run the `kubectl create secret` command through the cluster's exec endpoint, with the secret passed on stdin (base64 encoded in the request): ```bash curl -N -X POST https://cloider.com/api/clusters/x8f2kq/exec \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"command\": [\"sh\", \"-c\", \"kubectl create secret docker-registry cloider-registry -n shop --docker-server=registry.cloider.com --docker-username=acme --docker-password=\\\"\$(cat)\\\"\"], \"stdin\": \"$(printf '%s' "$REGISTRY_SECRET" | base64)\"}" ``` **MCP** Ask your agent: ```text title="Prompt" In the staging cluster, create an image pull secret called cloider-registry in the shop namespace for registry.cloider.com with the username acme and the secret in my REGISTRY_SECRET variable. ``` It calls `cluster_exec`, passing the secret on stdin: ```json title="Tool call: cluster_exec" {"cluster": "staging", "command": "kubectl create secret docker-registry cloider-registry -n shop --docker-server=registry.cloider.com --docker-username=acme --docker-password=\"$(cat)\"", "stdin": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"} ``` Check it: ```bash kubectl get secret cloider-registry -n shop ``` ```text title="Output" NAME TYPE DATA AGE cloider-registry kubernetes.io/dockerconfigjson 1 10s ``` ### Step 3: Use the secret in your pods Reference the Secret in the pod template with `imagePullSecrets`: ```yaml title="api.yaml" apiVersion: apps/v1 kind: Deployment metadata: name: api namespace: shop spec: replicas: 2 selector: matchLabels: app: api template: metadata: labels: app: api spec: imagePullSecrets: - name: cloider-registry containers: - name: api image: registry.cloider.com/acme/api:1.0 ports: - containerPort: 8080 resources: requests: cpu: 100m memory: 128Mi ``` ```bash kubectl apply -f api.yaml kubectl rollout status deployment/api -n shop ``` #### Use the secret for every pod in a namespace Instead of adding `imagePullSecrets` to each Deployment, attach the Secret to the namespace's `default` ServiceAccount. Every pod that uses that ServiceAccount then pulls with it: ```bash kubectl patch serviceaccount default -n shop \ -p '{"imagePullSecrets": [{"name": "cloider-registry"}]}' ``` Pods created after the patch pick it up; restart existing Deployments with `kubectl rollout restart` if they failed to pull before. #### Helm charts Most charts take pull secrets as a value, often `imagePullSecrets`: ```yaml title="values.yaml" image: repository: registry.cloider.com/acme/api tag: "1.0" imagePullSecrets: - name: cloider-registry ``` Check the exact key with `helm show values `. ### Verify ```bash kubectl get pods -n shop -l app=api kubectl describe pod -n shop -l app=api | grep -A3 "Events" ``` A successful pull shows `Successfully pulled image "registry.cloider.com/acme/api:1.0"` in the pod's events, and the pods are `Running`. ### Rotate or revoke the token Registry tokens can't be extended once created. To rotate, create a new token, update the Secret, and delete the old token: ```bash kubectl create secret docker-registry cloider-registry -n shop \ --docker-server=registry.cloider.com \ --docker-username='acme' \ --docker-password="$NEW_REGISTRY_SECRET" \ --dry-run=client -o yaml | kubectl apply -f - ``` Running pods keep their already pulled images; the new Secret is used for the next pull. See [Registry tokens](/container-registry/tokens) for listing and deleting tokens. ### Troubleshooting **`ImagePullBackOff` or `ErrImagePull`**: run `kubectl describe pod -n shop` and read the events: | Event message contains | Cause and fix | | --- | --- | | `unauthorized` or `authentication required` | The Secret is missing, in another namespace, not referenced in `imagePullSecrets`, or the token expired or was deleted. Check `kubectl get secret cloider-registry -n shop` and the pod spec | | `not found` or `manifest unknown` | The repository or tag doesn't exist. Check `cloider registry images acme api` | | `denied` or `forbidden` | The token isn't allowed on that project. Edit the token's projects in **Registry**, **Tokens**, or with `cloider registry token update` | **The secret was created in `default`**: the pull secret command from the dashboard and CLI doesn't include a namespace. Recreate it with `-n `. **A tag was overwritten but pods run the old image**: Kubernetes reuses an image already on the node when the tag is unchanged. Use a new tag for every release (recommended, see [Immutable tags](/container-registry/immutable-tags)), or set `imagePullPolicy: Always`. More cases are in [Kubernetes troubleshooting](/kubernetes/troubleshooting) and [Registry troubleshooting](/container-registry/troubleshooting). ### Next steps - [Use the registry with Kubernetes](/container-registry/use-with-kubernetes) in the Container Registry section. - [CI/CD for the registry](/container-registry/ci-cd), to build and push images on every commit. - [Deploy a workload](/kubernetes/deploy-a-workload). --- ## Troubleshoot Cloider Kubernetes clusters This page collects the problems people actually run into with Cloider clusters, grouped by symptom, with the exact error messages and how to fix them. Start with the quick checks, then jump to the section that matches what you see. ### Quick checks Most problems are explained by one of these four commands: ```bash # 1. Is the cluster Ready? cloider cluster status staging # 2. What is wrong with the pods? kubectl get pods -A | grep -v -E "Running|Completed" # 3. Why is a pod in that state? Read the Events at the bottom kubectl describe pod -n # 4. What did the container print before it failed? kubectl logs -n --previous ``` No kubeconfig at hand? Run the same `kubectl` commands with `cloider cluster exec staging -- kubectl ...` or in the [browser terminal](/kubernetes/web-terminal-and-exec). ### Creating a cluster | Message | Fix | | --- | --- | | `Cluster name must not be empty`, `must not have spaces`, `can not be upper case`, `can not start with numbers`, `must not have special characters except "-"`, `must not start with any special character`, `must not end with any special character`, `must be at least 2 characters` | Use lower case letters, digits and `-`, start with a letter, at least 2 characters. See [Choose a name](/kubernetes/create-a-cluster#choose-a-name) | | `Invalid cluster version!` | Use a version from `GET /api/options` (`cluster_versions`) | | `HA clusters are not supported at the moment!` | Send `is_ha: false` | | `Free plan users can not create more than one cluster. UPGRADE NOW!` | Delete the existing cluster (a paused one counts) or upgrade in [Billing](/account/billing) | | `Your account is suspended. Upgrade to a paid plan to continue using our services.` | Upgrade in [Billing](/account/billing) | | `You have read-only access to this account, you can not create clusters!` | Ask an owner or admin for a role that can create resources. See [Access control](/account/access-control) | | `This API token is read only!` (HTTP 403, CLI exit code `5`) | Use a **Read & Write** API token. See [API tokens](/account/api-tokens) | | `version 1.30 isn't offered, pick one of: ...` (CLI, exit code `2`) | Pick one of the versions listed | | `no Kubernetes version is available at the moment` (CLI) | No version is on offer right now. Try again later or contact [support](/account/support) | | The cluster stays `Creating` or `Starting` for a long time | Creation usually takes a few minutes. If it takes much longer, open a [support request](/account/support) with the cluster id from `cloider cluster list -o wide` | ### Connecting with kubectl **`the cluster's kubeconfig has no clusters`** (CLI), or the downloaded kubeconfig is empty: the cluster isn't `Ready` yet. Wait for `Ready` and download it again. **`Unable to connect to the server: dial tcp: lookup staging-x8f2kq.cloider.com: no such host`**: the kubeconfig belongs to a deleted cluster, or its `server` was edited. Each cluster has its own endpoint, `https://-.cloider.com`; get a fresh kubeconfig with `cloider cluster kubeconfig staging --merge`. **`kubectl` hangs or times out**: the cluster is `Paused` (resume it), or your network blocks outgoing HTTPS on port 443 to the cluster endpoint (try from another network or use the browser terminal). **`error: You must be logged in to the server (Unauthorized)`**: the client certificate in your kubeconfig doesn't belong to this cluster, usually after a manual merge mixed entries of several clusters. Run `cloider cluster kubeconfig staging --merge`, which replaces the entries named `cloider-staging`. **`x509: certificate signed by unknown authority`**: the CA data in the kubeconfig was changed or belongs to another cluster. Download the kubeconfig again; don't add `--insecure-skip-tls-verify`. **`error: context "cloider-staging" does not exist`**: the context was merged into a different file than the one `kubectl` reads. `--merge` writes to the first file in `$KUBECONFIG`, else `~/.kube/config`. **CLI exits with code `3`**: the CLI isn't logged in or its token was revoked. Run `cloider login`. See [CLI authentication](/cli/authentication). See [Connect with kubectl](/kubernetes/connect-with-kubectl). ### Browser terminal and exec | Problem or message | Fix | | --- | --- | | **Terminal** is disabled, or the cluster name isn't a link | The cluster isn't `Ready`. Wait, or resume it | | The browser terminal stays blank | Reload the page. Check that your network and browser extensions allow WebSocket connections | | `This cluster is paused. Resume it to run a command!` (409) | Resume the cluster | | `This cluster is not ready yet!` (409) | Wait for `Ready` | | `This cluster is being deleted!` (409) | Nothing can run in a cluster that is being deleted | | `This cluster's terminal isn't running right now!` (409) | The `terminal` Deployment in `kube-system` isn't running. Restore it if you scaled it down; if it was deleted, contact [support](/account/support) | | `The command could not be started, try again in a moment!` (503) | Retry | | `Terminals need a Read & Write API token!` (403) | Use a Read & Write API token | | `You have read-only access to this account, you can not run commands!` (403) | Your role is read-only | | `a shell needs a terminal on both ends` (CLI, exit code `2`) | `cloider cluster shell` needs an interactive terminal. In scripts use `cloider cluster exec` | | `the connection dropped before the command finished, it may still be running` | Check the command's effect before running it again | | `exec` exits with `124` | The command hit `--timeout` (default 10 minutes, at most 1 hour) | | Files disappeared from the terminal | The terminal's filesystem isn't persistent; it resets when its pod restarts | See [Web terminal and exec](/kubernetes/web-terminal-and-exec). ### Pods stuck in Pending A `Pending` pod hasn't been scheduled or is waiting for something. `kubectl describe pod` shows the reason in `Events`: | Event | Cause and fix | | --- | --- | | `0/N nodes are available: N Insufficient cpu` or `Insufficient memory` | The pod requests more CPU or memory than a node can offer. Lower `resources.requests` to what the app really needs | | `0/N nodes are available: N node(s) didn't match Pod's node affinity/selector` | The pod has a `nodeSelector` or affinity that no node matches. Nodes are managed by Cloider, so remove selectors that target specific nodes | | `pod has unbound immediate PersistentVolumeClaims` | A claim isn't bound. Run `kubectl describe pvc` (see [Volumes](#volumes) below) | ### Image pull errors `ErrImagePull` and `ImagePullBackOff` mean the node can't download the image. Read the exact message in `kubectl describe pod`: | Message contains | Fix | | --- | --- | | `not found`, `manifest unknown` | The image name or tag is wrong. Check it exists, for the Cloider registry with `cloider registry images ` | | `unauthorized`, `authentication required` | The image is private and the pod has no valid `imagePullSecrets`. See [Pull private images](/kubernetes/pull-private-images) | | `denied`, `forbidden` | The registry token in the pull secret can't access that project, or expired | | `toomanyrequests` | The public registry (for example Docker Hub) rate limits anonymous pulls. Push the image to your [Cloider registry](/container-registry/push-and-pull-images) and pull it from there | | `no match for platform in manifest` | The image wasn't built for the nodes' CPU architecture. Check it with `kubectl get nodes -L kubernetes.io/arch` and build a matching or multi-platform image with `docker buildx build --platform` | ### Pods that crash or restart **`CrashLoopBackOff`**: the container starts and exits again. Read `kubectl logs --previous`. Common causes are a missing environment variable or Secret, a wrong command, or a database that isn't reachable. **`OOMKilled`** in `kubectl describe pod` (`Last State: Terminated, Reason: OOMKilled`): the container used more memory than `limits.memory`. Raise the limit, or fix the memory leak. **`CreateContainerConfigError`**: a ConfigMap or Secret the pod references doesn't exist, or a key in it is missing. Check names and namespaces; both must be in the pod's namespace. **Pod `Running` but `0/1` ready**: the readiness probe fails. Check the probe's path and port and that the app listens on `0.0.0.0`. **A database refuses the connection from the cluster**: the database's trusted sources don't include the cluster. Allow it as described in [Trusted sources](/databases/trusted-sources), and use the connection details from [Connect to a database](/databases/connect). ### Domains and targets | Problem or message | Fix | | --- | --- | | `404` on the domain | No target matches the path, or the target is on another domain. Check `cloider target list ` | | `502` or `503` on the domain | The target matches but nothing answers: the Service has no ready endpoints (`kubectl get endpoints -n `), the port is wrong, the app speaks HTTPS instead of HTTP, or the cluster is paused | | Certificate error on a custom domain | The domain doesn't resolve to Cloider yet. Check its [nameservers](/domains/nameservers), that the host name has an `A` or `CNAME` record ([Add a custom domain](/domains/add-a-custom-domain)), and wait for DNS | | Ingress has no effect | Ingress objects aren't served on Cloider. Create a [target](/kubernetes/expose-services) instead | | `This path is already in use on this domain. Please choose a different path.` | Each path of a domain routes to one target. Update the existing target or use another path | | `Service and namespace names are too long for this cluster (combined limit exceeded)` | The Service name, namespace and cluster name together exceed 57 characters. Use shorter names | | `Path must start with / and may only contain letters, numbers and / _ . -` | Fix the path or target path | | `Orphaned targets detected!` on the domain page | The targets point to a cluster, Service or app that no longer exists. Delete or update them | | `cluster staging has no services to route to yet` or `no service shop/web in cluster staging` (CLI) | Create the Service first, and check its namespace and name with `cloider cluster services` | | `shop/web has several ports (80/TCP, 9090/TCP), pick one with --port` (CLI) | Add `--port` | See [Expose services on a domain](/kubernetes/expose-services). ### Load balancers **`EXTERNAL-IP` stays ``**: read `kubectl describe service `. If nothing explains it after a few minutes, contact [support](/account/support) with the cluster id and the Service name. A pending Service isn't billed. **Connection refused or timeout on the external IP**: the Service's selector doesn't match the pods (`kubectl get endpoints` is empty), `targetPort` is wrong, or the app listens only on `127.0.0.1`. **The IP changed**: the Service was deleted and recreated. Update DNS, and edit Services in place in the future. See [Load balancers](/kubernetes/load-balancers). ### Volumes **PVC stays `Pending`**: `waiting for first consumer to be created before binding` means it binds when a pod uses it. A `storageClassName` that doesn't exist never binds: check `kubectl get storageclass`, or remove the field to use the default. **`Multi-Attach error`**: a `ReadWriteOnce` volume is still attached to another pod. Use `strategy: Recreate` for Deployments with a volume, or a StatefulSet. **`Permission denied` writing to the volume**: set `securityContext.fsGroup` in the pod spec to the group the container runs as. **Higher volume charges than expected**: volumes are billed on provisioned capacity. Look for forgotten claims (`kubectl get pvc -A`) and `Released` volumes with a `Retain` policy (`kubectl get pv`). See [Persistent volumes](/kubernetes/persistent-volumes). ### Pause, resume and delete | Problem or message | Fix | | --- | --- | | **Pause** is disabled | The cluster isn't `Ready` | | **Resume** is disabled | The cluster is still `Pausing`; wait for `Paused` | | **Delete** is disabled | Your role is read-only, or the cluster is already `Terminating` | | **Delete cluster** stays disabled in the dialog | Type the cluster's label exactly, for example `staging-1` | | `You have read-only access to this account, ...` (403) | Your role is read-only | | `deleting cluster staging needs --yes when not running in a terminal` (CLI) | Add `--yes` in scripts | | `2 clusters are named "staging", pick one by id` (CLI) | Use the id from `cloider cluster list -o wide` | | Files missing after resume | Data outside PersistentVolumeClaims doesn't survive a pause | See [Pause and resume](/kubernetes/pause-and-resume) and [Delete a cluster](/kubernetes/delete-a-cluster). ### Unexpected costs Open **Usage** in the dashboard and look at the cluster's average CPU, memory and persistent volume use and its **LoadBalancers Usage** table. Then check: - **Requests far above real use.** Each container is billed on the larger of its request and its use. Compare `kubectl top pods -A` with the requests in your manifests and Helm values. - **Replicas you forgot.** `kubectl get deploy,sts -A` lists every workload and its replica count. An HorizontalPodAutoscaler may have scaled up. - **Leftover volumes.** `kubectl get pvc -A` and `kubectl get pv`. - **LoadBalancer Services.** `kubectl get svc -A | grep LoadBalancer`. Each one with an external IP is billed, even while the cluster is paused. - **Clusters you don't use.** Pause or delete them. A running cluster with no workloads still runs its control plane. See [How is a cluster billed?](/kubernetes/overview#how-is-a-cluster-billed). ### CLI exit codes | Code | Meaning | | --- | --- | | `0` | Success | | `1` | Error | | `2` | Usage error (a missing flag, an unknown version, a shell without a terminal) | | `3` | Not logged in, or the token was revoked | | `4` | Not found (no cluster of that name) | | `5` | Forbidden (a Read API token) | | `124` | `exec` stopped by `--timeout` | With `cloider cluster exec`, the CLI exits with the command's own exit code, which can overlap with the codes above; use `-o json` to tell them apart. See [Output and scripting](/cli/output-and-scripting). ### Getting help If none of this solves the problem, open a [support request](/account/support) from the dashboard or with `cloider support create`. Include: - the cluster id (`cloider cluster list -o wide`), - what you did and what you expected, - the exact error message, and the output of `kubectl describe` for the object involved. Don't include your kubeconfig, API tokens or Secret values. ### Next steps - [Limits and compatibility](/kubernetes/limits-and-compatibility). - [Status values](/reference/status-values). - [Troubleshooting for all products](/reference/troubleshooting). --- ## Kubernetes web terminal, shell and exec Every Cloider cluster comes with a terminal that has `kubectl` and `helm` installed and already pointed at the cluster. You can use it three ways: in the browser from the dashboard, as an interactive shell with `cloider cluster shell`, or to run single commands with `cloider cluster exec`, which is what scripts, CI jobs and AI agents use. None of them needs a kubeconfig on your machine. ### At a glance | | Browser terminal | `cloider cluster shell` | `cloider cluster exec` | | --- | --- | --- | --- | | **Where** | Dashboard, **Kubernetes**, **Terminal** | Your terminal | Your terminal, scripts, CI, MCP | | **Interactive** | Yes, up to 4 tabs | Yes | No, one command per call | | **Tools** | `kubectl`, `helm` | `kubectl`, `helm` | `kubectl`, `helm` | | **Output** | A terminal screen | A terminal screen | stdout and stderr kept apart, the command's exit code, or JSON | | **Who can use it** | Members who can open the dashboard | Read & Write API tokens | Read & Write API tokens, members with write access | | **Cluster status** | `Ready` | `Ready` | `Ready` | ### How the terminal works When a cluster is created, Cloider deploys a small `terminal` Deployment into the cluster's `kube-system` namespace. Its pod holds the cluster's kubeconfig, so `kubectl` and `helm` inside it talk to your cluster's API as cluster admin. The browser terminal, `cloider cluster shell` and `cloider cluster exec` all run in that pod. A few consequences are worth knowing: - **Don't delete or scale down the `terminal` Deployment** in `kube-system`. If it is gone, the browser terminal and `exec` stop working with `This cluster's terminal isn't running right now!`. - **The terminal's filesystem is temporary.** Files you create in it are lost whenever its pod restarts, for example when the cluster is paused and resumed. Keep manifests in git or on your computer, and pipe them in with `exec -i`. - **It runs in your cluster**, so its CPU and memory use is counted in the cluster's compute like any other pod. When idle it uses very little. - **It needs the cluster to be `Ready`.** While a cluster is creating, paused or being deleted, the terminal isn't available. ### Open the browser terminal **Dashboard** 1. Open **Kubernetes** in the sidebar. 2. Click the cluster's name, or click the actions button (the three dots) on its row and then **Terminal**. 3. A terminal tab called **Terminal 1** opens, connected to the cluster. Type `kubectl get pods -A`. - Click **+** in the tab bar to open another terminal, up to 4 at a time. Each tab is its own shell session, and switching tabs keeps every session alive. - Close a tab with its **x**. When every tab is closed, click **Open a terminal** to start a new one. The cluster name is only a link, and **Terminal** is only enabled, while the cluster is **Ready**. **CLI** ```bash cloider cluster shell staging ``` This opens the same terminal in your own terminal window. Exit it with `exit` or Ctrl-D. A shell needs an interactive terminal on both ends; in a script, use `exec` instead (see below), otherwise the CLI stops with `a shell needs a terminal on both ends` and exit code `2`. **Terraform** Terraform doesn't open terminals. Use the dashboard or `cloider cluster shell`, or deploy with Terraform's `kubernetes` and `helm` providers as shown in [Kubernetes and Helm with Terraform](/terraform/guides/kubernetes-and-helm). **API** `GET /api/clusters/{id}/terminal` returns a short-lived URL for the terminal: ```bash curl https://cloider.com/api/clusters/x8f2kq/terminal \ -H "Authorization: Bearer $CLOIDER_TOKEN" ``` ```json title="Response" { "url": "https://cloider.com/terminal?name=staging&id=x8f2kq&token=...", "cluster_slug": "staging-x8f2kq" } ``` The URL carries a token that is valid for about 2 minutes, so open it straight away. It needs a **Read & Write** API token; a Read token gets HTTP 403 with `Terminals need a Read & Write API token!`. For scripted commands, use the exec endpoint below instead. **MCP** The MCP server has no interactive shell tool, since an agent can't type into a terminal. Agents run commands with the `cluster_exec` tool, described in the next section. ### Run a single command with exec `cloider cluster exec` runs one command in the cluster's terminal and exits with that command's exit code. Its stdout and stderr stay separate, so it behaves like running the command locally. This is the easiest way to use `kubectl` in a CI job or from an AI agent without handling a kubeconfig. **Dashboard** The dashboard runs commands interactively in the [browser terminal](#open-the-browser-terminal). For one-off commands from scripts, use the CLI, API or MCP tabs. **CLI** ```bash cloider cluster exec staging kubectl get pods -A ``` ```text title="Output" NAMESPACE NAME READY STATUS RESTARTS AGE kube-system coredns-6b9d8c7f5d-q2x7l 1/1 Running 0 2d kube-system terminal-7c9f6d8b4d-4mzk8 1/1 Running 0 2d shop web-6d8f7b9c5d-2kq9x 1/1 Running 0 5h shop web-6d8f7b9c5d-8hj2p 1/1 Running 0 5h ``` More examples: ```bash # helm is there too cloider cluster exec staging helm list -A # -- separates the command when it has flags of its own cloider cluster exec staging -- kubectl logs deploy/web -n shop --tail 50 # one quoted argument runs with sh -c, so pipes, && and $VARS work cloider cluster exec staging "kubectl get pods -A | grep -v Running" # apply a local manifest through stdin cloider cluster exec -i staging -- kubectl apply -f - < deploy.yaml # -o json before the cluster is cloider's, after it is kubectl's cloider cluster exec -o json staging kubectl get nodes -o name ``` ```json title="Output of the last command" { "exit_code": 0, "stdout": "node/node-a\nnode/node-b\n", "stderr": "", "timed_out": false } ``` | Flag | Default | What it does | | --- | --- | --- | | `-i`, `--stdin` | off | Send your stdin to the command, up to 2 MiB | | `--timeout` | `10m` | Stop the command after this long, up to `1h` | | `-o json`, `-o yaml` | | Print `{"exit_code", "stdout", "stderr", "timed_out"}` once the command ends | Flags for `exec` go **before** the cluster name. Everything after the cluster belongs to the command, flags included. **Exit codes.** `exec` exits with the command's own exit code. As in a shell, `127` means the command doesn't exist, `126` means it can't run, and `124` means `--timeout` stopped it. Because the command's codes can overlap with the CLI's own (`1` to `5`), use `-o json` when you need to tell them apart: the JSON is only printed when the command actually ran. **Terraform** Terraform doesn't run commands. Use `cloider cluster exec` in a `local-exec` provisioner if you must, or better, manage the objects with the `kubernetes` and `helm` providers. See [Kubernetes and Helm with Terraform](/terraform/guides/kubernetes-and-helm). **API** `POST /api/clusters/{id}/exec` runs a command and streams the result as [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events): ```bash curl -N -X POST https://cloider.com/api/clusters/x8f2kq/exec \ -H "Authorization: Bearer $CLOIDER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"command": ["kubectl", "get", "namespaces"], "timeout": 60}' ``` ```text title="Response" : open event: stdout data: TkFNRSAgICAgICAgICAgICAgU1RBVFVTICAgQUdFCmRlZmF1bHQgICAgICAgICAgIEFjdGl2ZSAgIDJkCg== event: exit data: 0 event: end data: 1 ``` | Field | Type | Description | | --- | --- | --- | | `command` | array of strings | Required. The program and its arguments. Use `["sh", "-c", "..."]` for a command line with pipes | | `stdin` | string (base64) | Optional. Bytes passed to the command's stdin, up to 2 MiB. Leave it out and the command gets no stdin | | `timeout` | integer | Optional. Seconds, up to `3600`. Default `600` | Events: | Event | Data | | --- | --- | | `stdout`, `stderr` | A chunk of output, base64 encoded | | `exit` | The command's exit code. The command finished | | `timeout` | The timeout in seconds. The command was stopped | | `error` | A message: the command couldn't be run or was cut off | | `end` | Always last. Nothing follows | Comment lines (`: ping`) keep the connection alive while a command is quiet. **MCP** Ask your agent: ```text title="Prompt" Show me the pods that aren't running in the staging cluster. ``` It calls the `cluster_exec` tool: ```json title="Tool call: cluster_exec" {"cluster": "staging", "command": "kubectl get pods -A --field-selector=status.phase!=Running"} ``` ```json title="Result" {"exit_code": 0, "stdout": "No resources found\n", "stderr": "", "timed_out": false} ``` | Parameter | Description | | --- | --- | | `cluster` | Name or id of the cluster | | `command` | The command line. With spaces it runs with `sh -c`, so pipes, `&&` and `$VARS` work | | `stdin` | Text passed to the command's stdin, up to 2 MiB, for example a manifest for `kubectl apply -f -` | | `timeout_seconds` | Stop the command after this long, 1 to 3600 seconds. Default 600 | `cluster_exec` is marked as a tool that can change or delete things, so MCP clients that honor tool annotations ask before running it, and it isn't offered when the server runs with `--read-only`. See [MCP safety and permissions](/mcp/safety-and-permissions). ### Examples Apply a whole directory of manifests from your computer: ```bash cat k8s/*.yaml | cloider cluster exec -i staging -- kubectl apply -f - ``` Watch a rollout finish in CI, failing the job if it doesn't within 5 minutes: ```bash cloider cluster exec --timeout 6m staging -- kubectl rollout status deploy/web -n shop --timeout=5m ``` Install a Helm chart without a local Helm: ```bash cloider cluster exec staging "helm repo add podinfo https://stefanprodan.github.io/podinfo && helm upgrade --install podinfo podinfo/podinfo -n demo --create-namespace" ``` Read a Secret's value: ```bash cloider cluster exec staging "kubectl get secret web-secrets -n shop -o jsonpath='{.data.DATABASE_URL}' | base64 -d" ``` Check what a pod's containers use right now: ```bash cloider cluster exec staging kubectl top pods -n shop ``` ### Who can use the terminal and exec? - **Dashboard members** can open the browser terminal for any `Ready` cluster of the account. - **API tokens** need the **Read & Write** permission for `shell`, `exec` and the terminal URL. With a Read token, the API answers HTTP 403 and the CLI exits with code `5`. - **Members with read-only access** can't run commands with `exec`: the API answers `You have read-only access to this account, you can not run commands!`. See [Access control](/account/access-control) and [API tokens](/account/api-tokens). ### Troubleshooting | Message | Cause and fix | | --- | --- | | `This cluster is paused. Resume it to run a command!` (HTTP 409) | [Resume the cluster](/kubernetes/pause-and-resume) first | | `This cluster is not ready yet!` (HTTP 409) | The cluster is still being created. Wait for `Ready` | | `This cluster is being deleted!` (HTTP 409) | The cluster is being deleted, nothing can run in it | | `This cluster's terminal isn't running right now!` (HTTP 409) | The `terminal` pod in `kube-system` isn't running. If you scaled the `terminal` Deployment down, scale it back up with your kubeconfig (`kubectl scale deployment terminal -n kube-system --replicas 1`). If you deleted it, contact [support](/account/support) | | `The command could not be started, try again in a moment!` (HTTP 503) | A temporary problem starting the command. Retry | | `Give a command to run!` | The request had no command. With the CLI, put the command after the cluster name | | `stdin is bigger than 2 MiB!` | Split the input, or apply manifests in several calls | | `The timeout can be an hour at most!` | Use a timeout of at most 3600 seconds (`--timeout 1h`) | | `Terminals need a Read & Write API token!` (HTTP 403) | Use a Read & Write token. See [API tokens](/account/api-tokens) | | `the connection dropped before the command finished, it may still be running` | The connection to the API broke. Like `kubectl exec`, the command may still be running in the cluster. Check its effect before running it again | | The browser terminal stays blank | Reload the page. If the cluster was just resumed, wait until it is `Ready`. Check that your network and browser extensions allow WebSocket connections | More cases are in [Kubernetes troubleshooting](/kubernetes/troubleshooting). ### Next steps - [Deploy a workload](/kubernetes/deploy-a-workload) by pasting manifests into the terminal. - [Install charts with Helm](/kubernetes/helm). - [Connect with kubectl](/kubernetes/connect-with-kubectl) to use your own tools locally. - [MCP tools](/mcp/tools) to let an AI agent run `cluster_exec`. --- ## Database backups and restore Cloider backs up every database automatically, with nothing to set up. Every database gets a full backup every 14 days, and every change in between is recorded as it happens, so you can restore your data as it is now or as it was at [any second of the last 14 days](#how-far-back-can-i-restore). A restore always creates a **new database**, and the original keeps running untouched. Backups are included in the database's price. ### At a glance | | | | --- | --- | | **Backups** | A full backup every 14 days, at night (UTC), plus every change recorded continuously. No setup needed | | **Restore window** | Any second of the last 14 days, up to the moment you restore. See [how far back](#how-far-back-can-i-restore) | | **Restore options** | **Newest full backup plus incremental**, your data as it is now (the default), or **Specific date & time**, any second of the window in UTC | | **What a restore brings back** | The whole server: every database, user and grant, or for Redis every key and its expiry time. See [what is restored](#what-is-restored-and-what-isnt) | | **Restore** | Into a new database (a fork) with the same name, user, password, plan, version, trusted sources and SSL mode | | **Cost** | Backups are free. A restored database is a new database, billed by its plan | | **Deleting the database** | Deletes its backups and recorded changes too | | **Managed with** | The **Backups** tab, `cloider db backups` and `cloider db restore`, the API and MCP. Not Terraform | ### How do Cloider database backups work? Cloider backs up each running database automatically. Backups are kept apart from the database's own storage, and older ones are removed automatically. #### Full backups and recorded changes Every 14 days, at night (UTC), Cloider takes a full backup of the whole server. In between, every change written to the database is recorded continuously: PostgreSQL's write-ahead log (WAL), MySQL's binary log, MongoDB's oplog and Redis's append-only file (AOF). Combined, they allow a consistent recovery to a specific point in time, called point-in-time recovery (PITR). There is no list of backups to pick from. A restore starts from the full backup taken before the moment you want and replays the recorded changes up to it, so both restore options are point-in-time restores of the whole server. #### How far back can I restore? The last 14 days, up to the moment you restore. Cloider keeps the full backup and the changes it needs to reach back that far, and removes older ones automatically. Deleting a database deletes all of its backups. A new database's window opens with its first full backup, which is taken the first night after the database is created. Until then the **Backups** tab shows "You can restore a backup after the first full backup is completed." #### Redis Redis records every write in its append-only file (AOF), with a timestamp for each second, and Cloider archives the new writes about once a minute. The full backup every 14 days is an AOF rewrite. :::note[Switching the AOF off] Your Redis user has full rights, so it can switch the AOF or its timestamps off with `CONFIG SET appendonly no` or `CONFIG SET aof-timestamp-enabled no`. Cloider switches them back on within about a minute and takes a new full backup. The restore window then starts at that backup. ::: Snapshots of a Redis database taken before it had point-in-time recovery, named `YYYY-MM-DD_HH-MM`, are still listed by `cloider db backups`, and you can restore one by name with `cloider db restore ` until they expire, around mid-November 2026. :::note[Paused databases] A paused database isn't running, so no full backups are taken and there are no changes to record. To restore it, [resume](/databases/pause-resume-and-delete) it first. ::: ### See what you can restore The **Backups** tab, `cloider db backups` and the API show the start of the restore window. **Dashboard** 1. Open the database and go to the **Backups** tab. 2. **Summary** says how the database is backed up. 3. Under **Restore a Backup**, pick **Specific date & time**. The hint under **Time (UTC)** gives the start of the window, for example "Restorable from Oct 13, 2026 at 01:31:07 UTC." A new database shows "You can restore a backup after the first full backup is completed." until its first full backup has run. **CLI** ```bash cloider db backups orders-db ``` ```text title="Output" Nothing to show. Restore the newest full backup plus every change since with `cloider db restore orders-db`, or any moment from 2026-10-13 01:31:07 UTC until now with `cloider db restore orders-db --time