# Route a domain to apps, services and buckets with targets

> Route a domain or a path of it to a Cloider app, a Kubernetes Service or a bucket's static website over HTTPS with targets, or point a custom domain at a LoadBalancer, with every field and error.

A target is a route from a domain, or a path of it, to a [Cloider app](/apps/overview), to a Service in a
[Kubernetes cluster](/kubernetes/overview), or to a bucket's [static website](/object-storage/static-websites). With
targets, `https://example.com/` can serve the `web` app, `https://example.com/api` a Service in your `production`
cluster, and `https://example.com/docs` another app, each over HTTPS, while `https://docs.example.com/` serves a
website straight from a bucket. A custom domain can also point straight at a
[LoadBalancer](/kubernetes/load-balancers) in one of your clusters, for any port and protocol. This page is the
complete reference for targets: how they route, every field, path rewrites, and the errors you can hit.

For the same topic from the other side, see [Domains for apps](/apps/domains) and
[Expose Kubernetes services](/kubernetes/expose-services).

## At a glance

| | |
| --- | --- |
| **Routes** | A host name and a path prefix, such as `example.com/api` |
| **To** | An app, a Service port in one of your clusters, a bucket's static website, or a LoadBalancer's IP |
| **Types** | `App`, `Cluster`, `Bucket` and `LoadBalancer` |
| **Domains** | Free subdomains and custom domains. A LoadBalancer target needs a custom domain |
| **Paths** | One target per path per domain. The longest matching path wins. Bucket and LoadBalancer targets always use `/` |
| **Rewrites** | Optional `target_path` replaces the matched path before the request is forwarded |
| **Protocol to your code** | Plain HTTP. Cloider terminates HTTPS. A LoadBalancer target passes every port and protocol straight through |
| **Certificates** | Automatic. See [HTTPS certificates](/domains/https-certificates). None for a LoadBalancer target: you manage its TLS |
| **Cost** | No charge. Targets replace a load balancer and an Ingress controller. A LoadBalancer target is free, the LoadBalancer Service it points at is billed |
| **Tools** | Dashboard, `cloider target`, `cloider_target`, `/api/targets`, MCP |

## How does a target route a request?

```mermaid
flowchart LR
  U[https://example.com/api/orders] --> E[Cloider HTTPS entry point]
  E -->|"path /api, longest match"| S[Service api, namespace shop<br/>cluster production, port 80]
  E -->|"path /"| A[App web]
  E -->|"path /docs, target path /"| D[App docs<br/>receives /intro for /docs/intro]
```

1. The browser looks up the host name. It must resolve to Cloider: free subdomains always do, and a custom domain does
   once you [point it at Cloider](/domains/add-a-custom-domain#step-4-point-the-host-names-you-serve-at-cloider).
2. Cloider terminates HTTPS with the certificate for that host name.
3. Among the host name's targets, the one with the longest `path` that matches the request wins. A path matches itself
   and everything below it: `/api` matches `/api` and `/api/orders`.
4. If the target has a `target_path` different from its `path`, the matched prefix is replaced.
5. The request is forwarded as plain HTTP to the app's port or to the Service's port. For a Bucket target, the
   bucket's website answers it, as described in [Static websites](/object-storage/static-websites).

Plain `http://` requests are redirected to `https://`.

A LoadBalancer target is the exception: Cloider only answers the domain's DNS lookups with the LoadBalancer's IP, and
none of the steps above happen. See [Point a domain at a LoadBalancer](#point-a-domain-at-a-loadbalancer).

## Target fields

| Field (API) | Dashboard | CLI flag | Terraform | Meaning |
| --- | --- | --- | --- | --- |
| `domain_name` | the domain's page | `<domain>` argument | `domain` | The domain to route. Its name is the host name served |
| `target_type` | **Target type** | `--app`, `--bucket`, or `--cluster` with `--service` or `--loadbalancer` | `app_id`, `bucket_id`, or `cluster_id` with `service` or `loadbalancer` | `App`, `Bucket`, `Cluster` or `LoadBalancer`. Defaults to `Cluster` in the API |
| `app_id` | **App** | `--app <name or id>` | `app_id` | The app's id. App targets only |
| `bucket_id` | **Bucket** | `--bucket <name or id>` | `bucket_id` | The bucket's id. Bucket targets only |
| `bucket_name` | set for you | set for you | set for you | The bucket's name, shown in lists. Bucket targets only |
| `cluster_id` | **Cluster** | `--cluster <name or id>` | `cluster_id` | The cluster's id. Cluster and LoadBalancer targets only |
| `cluster_name` | set for you | set for you | set for you | The cluster's name, shown in lists |
| `namespace` | **Namespace** | `--service` or `--loadbalancer <namespace>/<service>` | `namespace`, or `loadbalancer` | The Service's namespace. Cluster and LoadBalancer targets only |
| `service` | **Service** | `--service` or `--loadbalancer <namespace>/<service>` | `service`, or `loadbalancer` | The Service's name. Cluster and LoadBalancer targets only |
| `loadbalancer_ip` | set for you | set for you | `loadbalancer_ip` | The IP the domain's A record points at, empty while the LoadBalancer has none. LoadBalancer targets only |
| `port` | **Port** | `--port` (default: the Service's only port) | `port` | The Service's port, which must speak HTTP |
| `protocol` | from the port | `--protocol` (default: the port's own) | `protocol` (default `TCP`) | `TCP` or `UDP`, as the Service declares the port |
| `path` | **Path** | `--path` (default `/`) | `path` (default `/`) | The path prefix on the domain. Always `/` for Bucket and LoadBalancer targets |
| `target_path` | **Target path** | `--target-path` (default: `--path`) | `target_path` (default: `path`) | The path the request is forwarded with. Always `/` for Bucket and LoadBalancer targets |

For an app target you only choose the app: Cloider routes to the app's port itself, and the response carries the app's `app_name` and `app_label`. For a bucket target you only choose the bucket, and the response carries its `bucket_name`. Every target has an id, `hashed_id`, such as
`t4k9vq`, and a `created_at` time.

## Before you begin

- A domain in your account: a [free subdomain](/domains/free-subdomains) or a
  [custom domain](/domains/add-a-custom-domain).
- **For an app target**: an app that has deployed. The dashboard offers only `Ready` apps; the CLI, Terraform, the API
  and MCP also accept a `Paused` one.
- **For a cluster target**: a cluster with a Service to route to. List them with `cloider cluster services <cluster>`
  or in the dashboard's target dialog. See [Deploy a workload](/kubernetes/deploy-a-workload).
- **For a bucket target**: a public bucket in the **Ready** state with its static website on. See
  [Static websites](/object-storage/static-websites).
- **For a LoadBalancer target**: a custom domain with no other targets, and a
  [LoadBalancer Service](/kubernetes/load-balancers) that has an IP, in a cluster that is **Ready** or **Paused**.
- Write access to the account. Members with read-only access can see targets but not change them.

## Route a domain to an app

**Dashboard**

1. Open **Domains** in the sidebar and click the domain.
2. In the **Targets** section, click **Add target**. The **Add a new target** dialog opens.
3. Choose **App** as the **Target type**. The **App** list shows each app with its port and status, and only `Ready`
   apps can be picked. The first `Ready` app is picked for you, and the line under the list says where requests go,
   for example **routes to blog on port 3000**.
4. Keep **Path** as `/` to route the whole domain, or type a path such as `/blog`.
5. **Target path** copies what you type in **Path** until you change it yourself. Keep it equal to the path to forward
   requests as they are made, or see [Rewrite the path](#rewrite-the-path).
6. Click **Save**. The dialog closes and the target appears in the **Targets** list.

You don't choose a cluster, namespace, Service or port for an app: Cloider routes to the app's own port. If no app is
`Ready` yet, the dialog says **None of your apps are Ready yet. Wait for an app to finish deploying before adding an
App target.**

**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
```

`--app` takes the app's name, label or id.

**Terraform**

```hcl title="main.tf"
resource "cloider_target" "web" {
  domain = "example.com"
  app_id = "w4d8hz" # from `cloider app list -o wide`
}

resource "cloider_target" "blog" {
  domain = "example.com"
  path   = "/blog"
  app_id = "a7c2mq"
}
```

**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": "/"}'
```

```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, and example.com/blog to the blog app.
```

```json title="Tool call: create_domain_target"
{"domain": "example.com", "app": "web"}
```

```json title="Tool call: create_domain_target"
{"domain": "example.com", "app": "blog", "path": "/blog"}
```

## Route a domain to a Service in a cluster

**Dashboard**

1. Open the domain under **Domains** and click **Add target**.
2. Choose **Cluster** as the **Target type**. It is the type the dialog opens on when you have a cluster with a Service.
3. Pick the **Cluster**, then the **Namespace**, the **Service** and the **Port**. The lists show the Services that
   exist in the cluster right now, and each list starts on its first entry. A port reads like `80 TCP`.
4. Set **Path**. **Target path** follows it until you change it.
5. Click **Save**.

If the cluster you pick has no Services, the dialog says **The selected cluster "staging" has no Kubernetes services.
You need to deploy at least one service to this cluster before adding targets.** and the other fields stay disabled.

**CLI**

```bash
cloider cluster services production
cloider target create example.com --cluster production --service shop/api --port 80 --path /api
```

```text title="Output"
✓ Routed example.com/api to production/shop/api:80
```

`--service` is `<namespace>/<service>`. `--port` can be left out when the Service has a single port.

**Terraform**

```hcl title="main.tf"
resource "cloider_target" "api" {
  domain     = "example.com"
  path       = "/api"
  cluster_id = cloider_cluster.production.id
  namespace  = "shop"
  service    = "api"
  port       = 80
}
```

**API**

Find the cluster's id and its Services with `GET /api/clusters/services`:

```bash
curl https://cloider.com/api/clusters/services -H "Authorization: Bearer $CLOIDER_TOKEN"
```

```json title="Response"
[
  {
    "id": "c8m2pd",
    "name": "production",
    "services": [
      {"service": "api", "namespace": "shop", "ports": [{"protocol": "TCP", "port": 80}]},
      {"service": "web", "namespace": "shop", "ports": [{"protocol": "TCP", "port": 80}]}
    ]
  }
]
```

Then create the target:

```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": "Cluster",
    "cluster_id": "c8m2pd",
    "cluster_name": "production",
    "namespace": "shop",
    "service": "api",
    "port": 80,
    "protocol": "TCP",
    "path": "/api"
  }'
```

```json title="Response"
{
  "hashed_id": "t7p3xr",
  "domain_name": "example.com",
  "message": "Target created successfully"
}
```

Send `cluster_name` too: it is stored as given and shown in target lists.

**MCP**

```text title="Prompt"
Route example.com/api to the api service in the shop namespace of my production cluster.
```

```json title="Tool call: create_domain_target"
{"domain": "example.com", "cluster": "production", "service": "shop/api", "port": 80, "path": "/api"}
```

The agent can first call `list_cluster_services` to find the namespace, Service and port.

## Route a domain to a bucket's website

A Bucket target serves a bucket's [static website](/object-storage/static-websites) on the domain. It takes the whole
domain: its path and target path are always `/`, and the website maps every request path to a file in the bucket.
Turn the bucket's website on first.

**Dashboard**

1. Open the domain under **Domains** and click **Add target**.
2. Choose **Bucket** as the **Target type**. The **Bucket** list shows your buckets, and only `Ready` buckets with their
   website on can be picked: one whose website is off is marked **website off**, and one that isn't `Ready` shows its
   status. The first bucket that can be picked is picked for you, and the line under the list reads, for example,
   **serves the acme-site website**.
3. **Path** and **Target path** are set to `/` and can't be changed.
4. Click **Save**.

If no bucket has its website on, the dialog says **None of your buckets has its website on. Turn it on from a bucket's
Settings before adding a Bucket target.** Turn it on as described in
[Static websites](/object-storage/static-websites#turn-on-the-website).

**CLI**

```bash
cloider target create docs.example.com --bucket acme-site
```

```text title="Output"
✓ Routed docs.example.com/ to the bucket's website
```

`--bucket` takes the bucket's name or id, and can't be combined with `--app` or `--cluster`.

**Terraform**

```hcl title="main.tf"
resource "cloider_target" "docs" {
  domain    = "docs.example.com"
  bucket_id = cloider_bucket.site.id
}
```

The `cloider_bucket` needs `public = true` and `website = true`.

**API**

```bash
curl -X POST https://cloider.com/api/targets \
  -H "Authorization: Bearer $CLOIDER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"domain_name": "docs.example.com", "target_type": "Bucket", "bucket_id": "s3w8qn"}'
```

```json title="Response"
{
  "hashed_id": "t9b3ks",
  "domain_name": "docs.example.com",
  "message": "Target created successfully"
}
```

**MCP**

```text title="Prompt"
Serve the acme-site bucket's website on docs.example.com.
```

```json title="Tool call: create_domain_target"
{"domain": "docs.example.com", "bucket": "acme-site"}
```

The route starts answering shortly after the target is saved, once Cloider has set it up. For a custom domain, HTTPS
works once its certificate is issued.

## Point a domain at a LoadBalancer

A LoadBalancer target gives a custom domain an A record to the IP of a
[LoadBalancer Service](/kubernetes/load-balancers) in one of your clusters. Unlike the other targets, nothing is
routed through Cloider: clients connect straight to the LoadBalancer, so it carries every port and protocol the Service
exposes, TCP or UDP.

### When do you need a LoadBalancer target?

You need a LoadBalancer target when a [Cluster target](#route-a-domain-to-a-service-in-a-cluster) can't do what you
want. A Cluster target forwards HTTP to one Service port per path, with Cloider's HTTPS certificate and an optional
path rewrite, and nothing more. Use a LoadBalancer target for:

- **Traffic that isn't HTTP**, or several ports on one name: databases you run yourself, MQTT, game servers, SMTP,
  custom TCP or UDP protocols.
- **Your own ingress controller or Gateway API implementation**, such as Traefik or Envoy Gateway, for routing and
  features a Cluster target doesn't offer: routing by header or method, weighted and canary traffic splits,
  redirects, authentication, rate limits, or your controller's own middleware and annotations.
- **Your own TLS**: certificates you bring or issue yourself, client certificate authentication (mTLS), or TLS passed
  through to your pods.

For a plain website or HTTP API, a Cluster target is simpler and has no load balancer charge.

:::warning[With a LoadBalancer target, you run everything behind the domain]
Cloider only points the domain at the LoadBalancer's IP. Everything a Cluster target does for you becomes yours to set
up and keep running:

- **The ingress controller or Gateway API implementation**, if you serve HTTP: installing it in the cluster, exposing it
  with a Service of `type: LoadBalancer`, upgrading it, and keeping it healthy.
- **The routes**: the Ingress, Gateway and HTTPRoute objects (or your controller's own resources) that send each host
  name and path to your Services.
- **The TLS certificates**: issuing them for the domain and renewing them before they expire, for example with
  cert-manager and Let's Encrypt. Cloider issues no certificate for a LoadBalancer target's domain, and doesn't
  redirect `http://` to `https://`.
- **The load balancer's cost**: the LoadBalancer Service is billed per hour like any other, see
  [Load balancers](/kubernetes/load-balancers).
:::

### A LoadBalancer target can't share its domain with other targets

A LoadBalancer target takes the whole domain. Its path and target path are always `/`, and it can't be combined with
any other target on the same domain: the domain's A record sends every request to the LoadBalancer, so an App, Cluster
or Bucket target there would never be reached. Cloider refuses both orders, a LoadBalancer target on a domain that
already has targets and any target on a domain that has a LoadBalancer target. Use a domain of its own, such as
`play.example.com`, next to the domain your website uses. To send more host names to the same ingress controller,
point each of them at that domain with a `CNAME` record.

### What happens to the domain's DNS records?

- The A record replaces the `ALIAS` record the domain was created with. It shows in the domain's DNS records marked
  **LoadBalancer target**, with a TTL of 60 seconds, and can only be changed through the target.
- If the Service is recreated and gets a new IP, the record follows it within about a minute. While the LoadBalancer
  is gone or has no IP, the domain has no A record.
- Deleting the target deletes the A record and gives the domain its `ALIAS` back.

### Create a LoadBalancer target

**Dashboard**

1. Open the custom domain under **Domains** and click **Add target**.
2. Choose **LoadBalancer** as the **Target type**. The **LoadBalancer** list shows each LoadBalancer that has an IP,
   as cluster, namespace and name, with the IP next to it. The line under the list reads, for example,
   **points play.example.com at 203.0.113.25 on every port and protocol, with no HTTPS from Cloider**.
3. **Path** and **Target path** are set to `/` and can't be changed.
4. Click **Save**. The domain's **DNS records** show the new A record right away.

The **LoadBalancer** type is offered on custom domains only, and can't be picked while the domain has other targets.

**CLI**

```bash
cloider cluster services production
cloider target create play.example.com --cluster production --loadbalancer games/server
```

```text title="Output"
✓ Routed play.example.com/ to LoadBalancer games/server
```

`cloider cluster services` lists each Service's **TYPE** and **EXTERNAL-IP**. `--loadbalancer` takes
`<namespace>/<service>`, needs `--cluster`, and can't be combined with `--service`, `--port`, `--protocol`, `--app` or
`--bucket`.

**Terraform**

```hcl title="main.tf"
resource "cloider_target" "games" {
  domain       = "play.example.com"
  cluster_id   = cloider_cluster.production.id
  loadbalancer = "games/server"
}
```

`loadbalancer_ip` is the IP the domain points at.

**API**

```bash
curl -X POST https://cloider.com/api/targets \
  -H "Authorization: Bearer $CLOIDER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"domain_name": "play.example.com", "target_type": "LoadBalancer", "cluster_id": "k7m2xq", "namespace": "games", "service": "server"}'
```

```json title="Response"
{
  "hashed_id": "t2l8vd",
  "domain_name": "play.example.com",
  "message": "Target created successfully"
}
```

**MCP**

```text title="Prompt"
Point play.example.com at the server LoadBalancer in the games namespace of my production cluster.
```

```json title="Tool call: create_domain_target"
{"domain": "play.example.com", "cluster": "production", "loadbalancer": "games/server"}
```

The domain resolves to the LoadBalancer once its nameservers point at Cloider, see
[Add a custom domain](/domains/add-a-custom-domain).

## List a domain's targets

**Dashboard**

The **Targets** section of the domain's page lists each target with its **Type** (**App**, **Bucket** or
**Cluster**), **Name** (the app, bucket or cluster), **Namespace**, **Service**, **Port**, **Protocol**, **Path** (with
a link that opens it) and **Target path**. Namespace and Service are only shown for cluster targets, and Port and
Protocol not for bucket targets. Each row's **Actions** menu has **URL**, which copies the address of the route, such
as `https://example.com/shop`, **Edit** and **Delete**.

A warning icon next to a name explains, on hover, why a target won't serve as set up, for example **App is Paused -
requests will fail until it is Ready.** or **The bucket's website is off - requests will fail until it is turned on.**
A target whose app, bucket, cluster or Service no longer exists is greyed out, and its **Edit** action is disabled.

**CLI**

```bash
cloider target list example.com
```

```text title="Output"
ROUTE              TARGET                    TARGET PATH   PROTOCOL
example.com/docs   app docs                  /             -
example.com/api    production/shop/api:80    /api          TCP
example.com/       app web                   /             -
```

A bucket target shows as `website of bucket <name>`. Add `-o wide` for the `ID` column, which `update` and `delete`
take.

`cloider domain get example.com` shows the domain's type, status and creation time with the same table of targets
under it.

**Terraform**

Terraform has no data source for targets. Existing targets can be imported as `<domain>/<id>`:

```hcl title="import.tf"
import {
  to = cloider_target.api
  id = "example.com/t7p3xr"
}
```

**API**

```bash
curl -G https://cloider.com/api/targets \
  -H "Authorization: Bearer $CLOIDER_TOKEN" \
  --data-urlencode "domain_name=example.com"
```

```json title="Response"
[
  {
    "hashed_id": "t7p3xr",
    "domain_name": "example.com",
    "target_type": "Cluster",
    "cluster_id": "c8m2pd",
    "cluster_name": "production",
    "app_id": "",
    "app_name": "",
    "app_label": "",
    "bucket_id": "",
    "bucket_name": "",
    "namespace": "shop",
    "service": "api",
    "port": 80,
    "protocol": "TCP",
    "path": "/api",
    "target_path": "/api",
    "created_at": "2026-10-20T11:20:03Z"
  }
]
```

Targets are listed newest first. `domain_name` is required, and a domain that isn't yours answers `403` with
`You don't have permission to access targets for this domain`. App and Bucket targets come back with an empty
`namespace` and `service`.

`GET /api/domains/example.com` returns the domain itself with the same list in `targets`.

**MCP**

```text title="Prompt"
Which targets does example.com have?
```

```json title="Tool call: get_domain"
{"domain": "example.com"}
```

The answer is the domain with its targets, including each target's `hashed_id`, which `update_domain_target` and
`delete_domain_target` take as `target_id`.

## Route several paths of one domain

Each path of a domain routes to exactly one target, and the longest matching path wins. A typical layout:

| Path | Goes to | Requests it receives |
| --- | --- | --- |
| `/` | app `web` | everything no longer path matches: `/`, `/pricing`, `/about/team` |
| `/api` | Service `shop/api:80` in `production` | `/api`, `/api/orders`, `/api/orders/42` |
| `/docs` | app `docs`, target path `/` | `/docs/intro` arrives as `/intro` |

```bash
cloider target create example.com --app web
cloider target create example.com --cluster production --service shop/api --path /api
cloider target create example.com --app docs --path /docs --target-path /
```

A second target with a path the domain already routes is refused with
`This path is already in use on this domain. Please choose a different path.` Edit the existing target instead.

## Rewrite the path

By default a request reaches the app or Service with the exact path it was made with. Set a **target path** to replace
the matched `path` prefix before the request is forwarded:

| Path | Target path | Request | Forwarded as |
| --- | --- | --- | --- |
| `/api` | (empty, same as path) | `/api/orders` | `/api/orders` |
| `/api` | `/` | `/api/orders` | `/orders` |
| `/api` | `/` | `/api` | `/` |
| `/api` | `/v2` | `/api/orders` | `/v2/orders` |
| `/` | `/v1` | `/orders` | `/v1/orders` |
| `/docs` | `/` | `/docs/intro?lang=en` | `/intro?lang=en` |

```bash
cloider target create example.com --app docs --path /docs --target-path /
```

A trailing slash on the target path is ignored: `/v2/` and `/v2` behave the same.

:::warning[Apps under a path must know their base path]
A rewrite changes the path the app sees, not the links it generates. An app served at `/docs` with a target path of `/`
still writes links such as `/intro` or `/assets/app.js`, which the browser requests from `/`, not `/docs`. Configure
the app's base path (for example `basePath` in Next.js or `base` in Vite), or give it its own subdomain such as
`docs.example.com`.
:::

## Edit a target

Editing can change the path, the target path and what the target points at, including switching between an app, a
cluster Service and a bucket's website. Cloider replaces the route in one step. A target moved to a bucket must have
the path `/`.

**Dashboard**

Open the target's **Actions** menu, click **Edit**, change the fields in the **Edit target** dialog and click **Save**.

**CLI**

`cloider target update` keeps whatever you don't give:

```bash
cloider target list example.com -o wide                 # find the id
cloider target update example.com t7p3xr --path /v2      # move the route
cloider target update example.com t7p3xr --app api       # point it at an app instead
cloider target update example.com t7p3xr --port 8080     # another port of the same Service
cloider target update example.com t4k9vq --bucket acme-site  # serve a bucket's website at /
```

```text title="Output"
✓ Updated example.com/v2 to production/shop/api:80
```

When you change `--path` and the target path was the same as the old path, the target path moves with it.

**Terraform**

Change the `cloider_target` and run `terraform apply`. Changing `domain` replaces the target; the other attributes are
updated in place.

**API**

`PUT` replaces the target, so send every field, as for a new one:

```bash
curl -X PUT https://cloider.com/api/targets/t7p3xr \
  -H "Authorization: Bearer $CLOIDER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"domain_name": "example.com", "target_type": "App", "app_id": "k2v9wd", "path": "/api", "target_path": "/"}'
```

```json title="Response"
{
  "hashed_id": "t7p3xr",
  "domain_name": "example.com",
  "message": "Target updated successfully"
}
```

**MCP**

```text title="Prompt"
Move the /api target of example.com to the api app.
```

```json title="Tool call: update_domain_target"
{"domain": "example.com", "target_id": "t7p3xr", "app": "api"}
```

## Delete a target

Deleting a target stops its route right away. The domain, its DNS records and the app or Service stay. When a domain's
last target is deleted, its status becomes `Suspended`.

**Dashboard**

Open the target's **Actions** menu, click **Delete** and confirm **Are you sure you want to delete this target?** with
**Delete**.

**CLI**

```bash
cloider target delete t7p3xr
```

```text title="Output"
Delete target t7p3xr? Its route stops answering. [y/N] y
✓ Deleted target t7p3xr
```

Add `--yes` in scripts.

**Terraform**

Remove the `cloider_target` resource and run `terraform apply`.

**API**

```bash
curl -X DELETE https://cloider.com/api/targets/t7p3xr \
  -H "Authorization: Bearer $CLOIDER_TOKEN"
```

```json title="Response"
{"message": "Target deleted successfully"}
```

**MCP**

```text title="Prompt"
Delete the target t7p3xr of example.com.
```

```json title="Tool call: delete_domain_target"
{"target_id": "t7p3xr"}
```

## Targets when an app, cluster or bucket changes

| Event | What happens to its targets |
| --- | --- |
| App or cluster **paused** | Targets stay. Requests fail until you resume it |
| App **redeployed** | Targets stay and keep routing to the new version |
| App's **port changed** | The route follows the app's port on the next deploy. The dashboard shows **App now listens on port 8080; edit and save this target to update it.** until you save the target |
| App **deleted** | Its targets are deleted with it. See [Domains for apps](/apps/domains) |
| Cluster **deleted**, or a Service removed | The targets stay and point at nothing. The dashboard shows **Orphaned targets detected!**. Delete them or point them elsewhere. A LoadBalancer target's A record is removed within a minute |
| LoadBalancer **recreated** with a new IP | The A record follows the new IP within a minute |
| Bucket's **website turned off** | Refused while targets point at it: `Domains still point at this website, remove their targets first.` |
| Bucket **made private** | Refused while its website is on: `The bucket's website needs it public, turn the website off first.` |
| Bucket **deleted** | Its website stops and its targets are deleted with it. The domains stay |

## Limits

| Limit | Message |
| --- | --- |
| `path` and `target_path` start with `/` and contain only letters, digits and `/ _ . -` | `Path must start with / and may only contain letters, numbers and / _ . -`, `Target path must start with / and may only contain letters, numbers and / _ . -` |
| One target per path per domain | `This path is already in use on this domain. Please choose a different path.` |
| Service, namespace and cluster names together at most 57 characters | `Service and namespace names are too long for this cluster (combined limit exceeded)` |
| An app name of 50 characters or fewer always works; a longer one can be refused | `This app's name is too long to be exposed on a domain.` |
| The app has deployed (`Ready` or `Paused`) | `This app is not ready yet. Wait until it finishes deploying, then add the target.` |
| A port is required for cluster targets | `Valid port is required` |
| A bucket target takes the whole domain: `path` and `target_path` are `/` | `A bucket website is served at the root of its domain, the path must be /`, `A bucket website is served at the root of its domain, the target path must be /` |
| The bucket is `Ready` with its website on | `This bucket is not ready yet. Wait until it is created, then add the target.`, `Turn on the bucket's website first, from its Settings.` |
| The port speaks HTTP | Not checked. An HTTPS or non-HTTP port answers with errors |
| A LoadBalancer target needs a custom domain | `A LoadBalancer target needs a custom domain.` |
| A LoadBalancer target takes the whole domain | `A LoadBalancer takes the whole domain, the path must be /`, `A LoadBalancer target takes the whole domain. Delete the domain's other targets first, or use another domain.`, `This domain points at a LoadBalancer, which takes the whole domain. Delete that target first, or use another domain.` |
| The LoadBalancer has an IP | `This LoadBalancer has no IP yet. Wait until it gets one, then add the target.` |
| The domain has no `A`, `AAAA`, `ALIAS` or `CNAME` record of its own at its root, other than the one it was created with | `The domain has an A, AAAA, ALIAS or CNAME record of its own. Delete it from the domain's DNS records first, then add the target.` |
| Request bodies | Larger uploads may be refused with `413 Request Entity Too Large`. Upload big files straight to [object storage](/object-storage/overview) instead |

For raw TCP or UDP traffic, such as a database or a game server, use a
[LoadBalancer Service](/kubernetes/load-balancers) and
[point a custom domain at it](#point-a-domain-at-a-loadbalancer).

## Troubleshooting

| Problem | Cause and fix |
| --- | --- |
| **Add target** is disabled | There is nothing to route to yet: **You have no apps yet. Create one under Apps before adding an app target.**, **No clusters found. You need to create at least one cluster before adding cluster targets.** or **You have no buckets yet. Create one under Buckets and turn on its website before adding a bucket target.** |
| **None of your buckets has its website on. ...** | Turn on the [static website](/object-storage/static-websites) in a bucket's **Settings** tab, then add the target |
| `Turn on the bucket's website first, from its Settings.` | The bucket's website is off. Turn it on, then add the target |
| `A bucket website is served at the root of its domain, the path must be /` | Bucket targets take the whole domain. Leave the path as `/`, or use a domain of its own such as `docs.example.com` |
| `This path is already in use on this domain. ...` | Edit or delete the target that has the path |
| `This app is not ready yet. ...` | Wait for the app's first deploy to finish |
| `App not found!`, `Bucket not found!` | The id is wrong or belongs to another account. Check `cloider app list -o wide` or `cloider bucket list -o wide` |
| `LoadBalancer not found! ...` | No Service of type LoadBalancer has that namespace and name in the cluster. Check `cloider cluster services <cluster>` |
| `This record is managed by the domain's LoadBalancer target, ...` | The A record belongs to the target. Edit or delete the target instead |
| `404 Not Found` from the API on create | The domain isn't in your account. Add it first; `www.example.com` is a domain of its own |
| `say where to route to with --app, --bucket or --cluster` (CLI) | Add `--app <app>`, `--bucket <bucket>` or `--cluster <cluster> --service <namespace>/<service>` |
| `shop/api has several ports (80/TCP, 9090/TCP), pick one with --port` (CLI) | Add `--port 80` |
| `no service shop/api in cluster production, see cloider cluster services` (CLI) | Check the namespace and the Service name |
| The page shows `404` | The host name reaches Cloider but no target's path matches. Add a target for `/` |
| `502 Bad Gateway` or `503` | Nothing answers behind the target: the app or cluster is paused, the pods aren't ready, or the port is wrong |
| Browser certificate warning on a custom domain | The certificate isn't issued yet. See [HTTPS certificates](/domains/https-certificates) |
| The domain doesn't resolve | Targets other than LoadBalancer targets don't create DNS records. See [Point the host names you serve at Cloider](/domains/add-a-custom-domain#step-4-point-the-host-names-you-serve-at-cloider) |

More cases are in [Domains troubleshooting](/domains/troubleshooting).

## Next steps

- [HTTPS certificates](/domains/https-certificates): how targets get their certificates.
- [Expose Kubernetes services](/kubernetes/expose-services): targets from the cluster's point of view.
- [Static websites](/object-storage/static-websites): serve a bucket on a domain with a Bucket target.
- [`cloider target`](/cli/commands/target), [`cloider_target`](/terraform/resources/target) and the
  [Targets API](/api/endpoints/targets).
