Quick tour
A short walkthrough of Circloud® NEST.
Download
Download the installer, a zip for Windows, from the official download page:
Plans and Licence
A Circloud® NEST licence is for one plan. The plan sets how many servers one cluster may have and how many clusters the licence runs. Every plan includes every feature. The prices are on the download page.
| Plan | Servers per cluster | Clusters on servers | Virtual cluster |
|---|---|---|---|
| Mist™ | 1 Desktop Computer | Not included | 1, up to 3 virtual nodes |
| Cirrus™ | Up to 3 | 2 included, more in your profile | 1, as many virtual nodes as the desktop’s memory holds |
| Nimbus™ | Up to 8 | 2 included, more in your profile | 1, as many virtual nodes as the desktop’s memory holds |
| Anvil™ | Up to 25 | 2 included, more in your profile | 1, as many virtual nodes as the desktop’s memory holds |
| Supercell™ | Up to 100 | 2 included, more in your profile | 1, as many virtual nodes as the desktop’s memory holds |
- The virtual cluster: every plan has one, on one Desktop Computer, which counts as one server however many virtual nodes it runs: up to 3 on Mist™, and on the other plans as many as the desktop’s memory holds.
- Clusters on servers: Cirrus™, Nimbus™, Anvil™ and Supercell™ each include 2, and every server of a cluster counts, the control server included. More are bought in your profile. Mist™ includes none: upgrade the plan to add clusters on servers.
- Upgrade plan: in your profile. Your clusters keep running and nothing is reinstalled: the installer and your clusters pick up the new plan by themselves.
Your profile
nest.circloud.net/account, signed in with the Google account the licence was bought with, shows your plan, the clusters of each kind in use, every cluster with its status and version, and your orders with their receipts. The installer is downloaded there, and your licence key is shown there when you ask for it. Anyone with the key can install clusters of your plan: keep it private.
When every cluster is in use
The installer asks Circloud before it builds a cluster or adds to one. When every cluster of your plan is in use, Add cluster says so, with one button: Upgrade plan on Mist™, or Profile on the other plans, to buy another cluster or release one that no longer runs. When only one kind is full, the installer locks that choice and says why.
What a cluster sends
Every cluster checks in with Circloud about every 15 minutes. It sends its nodes (each one’s name, role and readiness, its CPU and memory, the label of the computer a virtual cluster runs on, and a hash of its machine’s id), the counts of its tenants, VPCs and pods, the addresses of the platform’s own portals, the platform version and a fingerprint of the cluster. It never sends a tenant’s name or domains, a log or the contents of your apps.
The connection it needs
Each cluster needs outbound HTTPS to licence.circloud.net at least once every 30 days: allow
it in your firewall. While Circloud cannot be reached nothing changes for 30 days, and the admin portal
shows a warning after 36 hours.
Beyond the plan
Running apps never stop. A cluster beyond its plan — more clusters of one kind than the licence includes, more servers in one cluster than the plan allows, or a copy of a cluster — shows a notice in its admin portal and in your profile, with what to do. Left as it is, changes on it can pause: at the earliest after 14 days over the plan within 30 days, and never sooner than 72 hours after the cluster was told. While changes pause, new resources and edits to running apps are refused; everything else keeps working: sign-in, security rules, deletes, stop, start and restart, Shrink, Destroy, updates and the console.
If the licence service ever ends
If Circloud ever stops the licence service, it publishes a final platform release in which nothing pauses for the licence.
Cloudflare Setup
Before you download the installer, add your Cloudflare account to your Circloud profile. The account needs a domain. Cloudflare's free plan is enough.
1. Copy your Account ID
Go to dash.cloudflare.com, select your domain, and copy the Account ID from the right side of the Overview page.
2. Create the API token
- Click your profile icon (top right) → My Profile → API Tokens, then Create Token
- Click Get started next to Create Custom Token, and give it a name (e.g. "Circloud NEST")
- Add one permission: Account → API Tokens → Edit. It has to be the one under Account, not User → API Tokens
- Under Account Resources, select Include > your account
- Click Continue to summary: it reads your account – API Tokens: Edit (All accounts – API Tokens: Edit if you included all of them). Click Create Token and copy the token — it is only shown once
3. Add them to your profile
On your profile, nest.circloud.net/account, open Cloudflare, paste the Account ID and the token, and click Add account.
Installation
Circloud® NEST provisions its own cluster and private networking stack on your hardware. The installer handles everything: cluster deployment, GUI/API setup and VPC/subnet routing. It asks for one of the Cloudflare accounts on your Circloud profile and a default domain (Cloudflare Setup).
Prerequisites
There are two ways to run Circloud® NEST, and they do not need the same hardware. Pick the one that matches yours — the numbers below are per path, and one set does not stand in for the other.
What the desktop needs
- Windows: Windows 10 or 11 Pro, Enterprise or Education with Hyper-V, which a virtual cluster requires on every plan. Home does not offer Hyper-V.
- Administrator: the installer is run elevated. It creates the virtual cluster’s nodes and their networking on this desktop, and stops early without it.
- Virtualization: Hyper-V, and VT-x / AMD-V enabled in the firmware. If Hyper-V is off the installer offers to turn it on and restart.
- CPU: 8 logical cores or more. Three quarters of them go to the cluster, split across the nodes; each node defaults to 4 vCPU.
- RAM: 32 GB recommended. The two default nodes ask for 8 GB each, and a quarter of the machine's memory (never less than 4 GB) is left to Windows itself.
- Disk: 200 GB free or more. Node disks default to 150 GB each and are thin-provisioned; the installer scales them down to fit the space you have, never below 50 GB a node. Preparing the node image needs about 25 GB of working room on top of that.
The machine you run the installer from
This machine has requirements of its own, and they are not the servers' requirements. It never becomes part of the cluster — it connects to the servers over SSH, builds them, and can be shut down afterwards.
- Windows: the installer
.exefrom the zip you download, run as Administrator, on a computer that reaches the servers over SSH. Hyper-V is not needed on this path; only a virtual cluster requires it. - Resources: modest — the work happens on the servers. It only has to stay on and connected until the install finishes.
The servers
- How many: two or more — one control server and one worker. Add workers whenever you like with Extend, up to your plan’s servers per cluster: 3 on Cirrus™, 8 on Nimbus™, 25 on Anvil™ and 100 on Supercell™.
- Per server: Ubuntu 22.04 or newer, 2+ CPU cores, 4+ GB RAM, and at least 50 GB of disk — 150 GB is what the platform is sized around.
- Access: SSH enabled, and a user with
sudo. - Network: all servers on the same network, each with internet access.
How far it scales
A single cluster scales to your plan’s servers per cluster, up to 100 servers on Supercell™ — one private cloud, on your own hardware. Start with one machine and add servers as you grow; each new server joins the same cluster and the same networking, with no rebuild and no migration.
- Start: one machine — a virtual cluster on a 64 GB Desktop Computer carries 2.5K concurrent users, and a Xeon 5 server (256 GB RAM) 10K
- Grow: add servers at any time with Extend
- Shrink: remove a worker with Shrink, Extend's pair in the installer
- Scale: up to your plan’s servers per cluster (3 on Cirrus™, 8 on Nimbus™, 25 on Anvil™ and 100 on Supercell™); beyond that, run a second cluster
Shrink removes one node. Choose it and the installer shows what happens before anything does: the node's pods move to the other nodes first, one at a time — a pod's persistent data is copied before it starts on its new node — then the node leaves the cluster and its machine is deleted (a physical server is reset). A VPC whose chosen nodes include it keeps the others as its nodes, even when none of its pods ran on it. You type the node's name to confirm. Only a worker can be removed, and never the node holding the platform database or the cluster's last worker; the control node and the machine that holds the Harbor registry are not removed either. For those the installer says why instead.
A server that no longer exists cannot be reset. When a physical server does not answer and the cluster no longer hears from it, Shrink offers one more confirmation: type the server's name and the node is removed without resetting it. Leave it empty if the server is only switched off: switch it on first. If a server you confirmed gone ever comes back, it joins the cluster again: wipe it before you switch it on. A server that held a VPC's persistent data cannot be removed this way: that data cannot be copied off a server that is gone, so Shrink says so and asks for no confirmation.
GPU Support (Optional)
To enable GPU workloads, mark specific worker nodes with gpu: true in the installer wizard
before you install.
The installer automatically installs NVIDIA drivers and configures the container runtime.
Pods can then request GPU access by setting gpu_access: true.
Install on Windows
Step 1: Download
Download the installer zip from
nest.circloud.net/download.
Its name carries two versions: the installer's own, and the latest platform version when
you download it. For example: circloud-nest-installer-1.5.22-platform-1.7.47.zip.
So two different installers never share a name. The folder and the .exe inside
it carry the same name. When it runs, the installer installs the latest platform version.
The download needs a Cloudflare account with a domain on your Circloud profile first
(Cloudflare Setup).
Step 2: Run as Administrator
Unzip it, right-click the .exe inside and select Run as Administrator.
The graphical setup wizard will open and guide you through:
- System Check — verifies Administrator privileges and shows this computer's resources
- Install Mode — choose Virtual Cluster on Desktop Computer or Physical Servers. Hyper-V is checked here, and only for the virtual cluster, which requires it
- Configuration — the cluster's name and the admin username, the Cloudflare account and the default domain (two lists from your Circloud profile), then the nodes: CPU cores, memory, number of nodes and network switches
- Review & Install — confirm and start deployment
The cluster's addresses. The cluster's name gives the label in every
address of the cluster: lowercase letters, digits and hyphens, at most 54 characters
(MyCluster gives mycluster). The Configuration step shows the label and the
addresses under the name — admin-<label>.<domain>,
user-<label>.<domain> and api-<label>.<domain>; the
container registry and the email service follow the same rule, registry-<label> and
email-<label>. So one domain carries many clusters, and two of your clusters cannot
have the same label: Circloud refuses the second at the install, before anything is built, and the
step says so under the name (Another of your clusters is called mycluster; pick another name.).
The two Cloudflare lists are read from your profile when the step opens; Refresh reads them again. An account the installer cannot use is listed greyed out, with why (no domain yet, a token that no longer works or lacks the permission). With no account it can use, the step says so and offers Profile, to add one. Once the install has started, press Approve: it opens your Circloud profile in your browser to approve the new cluster. Its addresses come online a few minutes after you approve it.
The installer handles everything: base image preparation, node provisioning, cluster deployment and CloudManager GUI/API/DB. If a reboot is needed (to turn on Hyper-V, which the virtual cluster requires), it saves progress and resumes automatically.
What the installer tells Circloud: after each install, Extend, node removal, Upgrade or Destroy, and when you adopt or import a cluster, it sends Circloud a short description of that cluster, under your licence: its name, status, installer and platform versions, its network ranges, and each node's name, role, size and private address. Each time it starts, it also sends any cluster on this computer that Circloud has not heard about yet, including clusters installed before this reporting existed. It never sends passwords, SSH keys, tokens or public addresses. If it cannot reach Circloud, it sends the description the next time it starts.
Step 3: Access the Platform
When the install finishes, the installer shows:
- Addresses — the admin portal, the user portal and the API on your domain:
https://admin-<label>.<domain>,https://user-<label>.<domain>andhttps://api-<label>.<domain>. They come online a few minutes after you approve the cluster. Until then the finish screen keeps Approve, while its link is valid. - Access — the same three at their direct addresses on the control node, which answer at once. Your browser warns about the certificate on these, because it is the cluster's own; the addresses on your domain carry a trusted one.
- Admin sign-in — the admin username you chose and the password the installer generated, with Show and Copy. Your first tenant has the same name and password, the whole cluster's CPU, memory and disk, and every active domain of your Cloudflare account.
The installer keeps the sign-in, with the admin API token, in a file of your Windows account,
%LOCALAPPDATA%\Circloud\clusters\<cluster id>\credentials.json,
and names it at the end of its log: open it any time to read them again.
Access URLs
Direct Access (on the control node)
API: https://<control node>:8080
Admin GUI: https://<control node>:8443
User GUI: https://<control node>:8444
Public Access (via Cloudflare)
API: https://api-<cluster>.<your-domain>
Admin GUI: https://admin-<cluster>.<your-domain>
User GUI: https://user-<cluster>.<your-domain>
<cluster> is the cluster's label, its name as an address: lowercase letters, digits and hyphens, so one domain carries many clusters. A cluster installed before addresses carried its name keeps api.<your-domain>, admin.<your-domain> and user.<your-domain>.
Upgrades. A newer release is installed from the admin portal's Version card, or with the installer's Upgrade: see Upgrading the Platform.
Getting Started
Welcome to Circloud! This guide will help you get started with deploying and managing your cloud infrastructure. Create Virtual Private Clouds (VPCs), subnets, pods, and Cloudflare tunnels to expose your services.
VPCs
Create isolated virtual networks with optional internet access. Each VPC is completely isolated from other tenants.
Subnets
Divide your VPC into logical network segments with custom CIDR blocks.
Pods
Deploy containerized applications with auto-scaling replicas and load balancing.
Tunnels
Expose your services securely to the internet via Cloudflare tunnels.
Customizing Table Views
Every table in the dashboard has a View section with column toggles. Switch a column on or off to show or hide it. This lets you focus on the information that matters most for your current task. Your choice is saved in this browser.
Admin Login
After installation, the first thing to do is log in to the CloudManager admin dashboard. This is where you manage tenants, configure the platform, monitor infrastructure, and control Cloudflare tunnels.
Getting Your Admin Credentials
The installer generates a random admin password during installation and shows the sign-in when it
finishes (Admin sign-in, with Show and Copy). It also keeps it, with the admin API
token, in a file of your Windows account,
%LOCALAPPDATA%\Circloud\clusters\<cluster id>\credentials.json, which
the end of the installer's log names. Its cloudmanager_admin section holds your
username and password.
To change the admin password, use Change password on the admin portal's Config → Admin Authentication card (below). The file keeps the password the installer generated: once you change it, the file's copy no longer signs in.
Accessing the Admin Dashboard
Open the CloudManager admin dashboard at https://admin-<cluster>.<your-domain> (or
at its direct address on the control node, https://<control node>:8443, which the
installer shows) and log in with your admin username and password.
admin-<cluster> is the admin portal's subdomain unless you chose another one on the
Domains card (admin on a cluster installed before addresses carried its
name).
Admin API Token
The same file also holds the admin API token (the
cloudmanager_admin.api_token field), which the admin portal's Admin Authentication card
shows too. This token is used for privileged
API endpoints that require the X-Admin-Token header — such as creating
tenants, managing service tunnels, and configuring global settings.
There are two types of authentication in the API:
| Header | Who uses it | Purpose |
|---|---|---|
X-Auth-Token |
Tenants | Manage VPCs, subnets, pods, tunnels within the tenant's scope |
X-Admin-Token |
Platform admin | Create/delete tenants, manage service tunnels, approve domains, configure provider settings |
AI Agent Integration
Skip the manual setup entirely. Point your AI coding agent — Claude Code, Cursor, Copilot Workspace, or anything else that speaks HTTP — at Circloud® NEST and it can provision tenants, VPCs, subnets, pods, and Cloudflare tunnels by calling the REST API directly. No clicking through dashboards, no hand-rolled infrastructure commands. The agent reads your code, builds the image, pushes it, then creates everything else automatically.
Put your credentials.json file into your project, at its root
next to your README.md. It is a small JSON file —
api_url, tenant_id and token, and
default_docker_registry: the server, username and password of your default
registry, the one the agent pushes your images to (see
Your Default Registry; without one it holds
ADD_YOUR_DOCKER_REGISTRY_URL_HERE and its two siblings, and the agent asks you
for a registry), then db_clusters: each of your DB clusters' write and read
addresses, database and login (see DB Clusters)
— and the portal makes it for you:
Config → Authentication → Download credentials.
Use it as it downloads, credentials.json, and keep it out of
version control. (The admin
portal's Admin Authentication card gives the administrator's
file the same way: api_url and admin_token, for an
agent that manages the platform itself.) The API
reference itself lives on your cluster: the agent reads
api_url from the file and fetches
<api_url>/openapi.json, the complete, machine-readable
reference the API generates from its own code — every route, who
may call it, every status it answers and why. <api_url>/docs
shows the same reference in a browser, and neither needs a token to read.
Every route there lists the token header it takes; Authorize takes
your token once and sends it with every request you try from the page. Creating a VPC,
a subnet, a pod or a topology takes the token in the body instead.
With the file and the reference, the agent has everything it needs to
provision the infrastructure your app needs and put it on the public
internet, end to end, in under a minute.
⬇ Download deployable app template (.zip)
Prefer to skip the boilerplate? The
deployable app template is a
working starter project — a Node.js HTTP service backed by a
PostgreSQL pod, plus
deploy.sh / upgrade.sh / destroy.sh
that talk to the Circloud Nest API and a topology.json
that wires VPC, subnets, pods, and a public Cloudflare tunnel. Its
CLOUD_README.md tells the agent to read
credentials.json at the root and to fetch the reference from
api_url; credentials.example.json shows the shape (a project set up
before may have the file as .credentials: the scripts read that too).
Tell your agent:
"unzip this template, put my
credentials.json at its root, adapt demo-app/ and
cloud/topology.json to build my project, then run
cloud/deploy.sh."
That's the shortest path from an empty repo to a Nest-deployed
application running on the public internet.
Example prompt
Build a gaming website with online payments
that auto-heals and auto-scales, then deploy
it on Circloud Nest with the credentials.json file
in this folder (the API reference is at
<api_url>/openapi.json), and expose it
under the subdomain games.
Create Account
Circloud® NEST is multi-tenant. Each tenant gets isolated VPCs, subnets, pods, and tunnels. Before tenants can use the platform, you need to create their accounts. You can optionally set resource limits (max pods, VPCs, subnets, CPU, RAM, storage) per tenant.
In the CloudManager admin dashboard, go to the Tenants tab and click Create Tenant. Enter a tenant name and password. You can also set resource limits. The platform generates a Tenant ID and API Token that the tenant uses to log in and interact with the API.
Create a tenant using the admin API token:
curl -k -X POST https://api.<your-domain>/tenants -H "X-Admin-Token: YOUR_ADMIN_TOKEN" -H "Content-Type: application/json" -d '{
"name": "my-tenant",
"password": "tenant-password-here",
"max_pods": 50,
"max_vpcs": 5,
"max_subnets": 10,
"max_cpu": 16,
"max_ram": 32,
"max_storage": 100
}'
The response includes the tenant's id and api_token.
| Field | Required | Description |
|---|---|---|
name | Yes | Tenant name (used for login and tenant isolation); follows the name rule |
password | No | Password for User GUI login (min 8 chars) |
max_pods | No | Maximum number of pods |
max_vpcs | No | Maximum number of VPCs |
max_subnets | No | Maximum number of subnets |
max_cpu | No | Maximum CPU cores |
max_ram | No | Maximum RAM in GB |
max_storage | No | Maximum storage in GB |
Self-Service Signup
When self-service signup is enabled in the admin settings, tenants can register directly from the User GUI login page. They choose a name and password, and receive their credentials immediately.
User Login
Tenants log in to the User GUI at https://user-<cluster>.<your-domain> (or
at its direct address on the control node, https://<control node>:8444) using the tenant name and password created
by the admin or via self-service signup. The installer makes the first one for you: a tenant
with the administrator's username (the one you chose in the installer), signing in with the
administrator's password, made once at the install — an update never makes one, and one you
delete stays deleted. Its password and the administrator's are separate from then on. A tenant changes its own password with
Change password on Config → Authentication (see
Change Password).
Setup & API Access
Each tenant has a Tenant ID and API Token for authenticating with the Circloud® NEST API. These are shown on the dashboard after logging in.
Step 1: Get Your Credentials
Log into the Dashboard
Visit the Circloud portal and log in with your tenant credentials.
Copy Your Tenant ID and API Token
Open Config → Authentication: the card shows your API URL, tenant ID and API token, each with a copy button.
Or Download credentials
Download credentials, at the foot of the same card beside
Change password, hands you all three — the API URL, your tenant ID and
your token — with your default registry and your DB clusters' logins, as the JSON
file a project reads,
credentials.json: put it at the root of your project as it is, and the app
template's scripts and your AI agent find the API from it. See AI Agent Integration.
Change Password
Change password on Config → Authentication opens a form under the card: your current password, the new one, and the new one again. The new password needs at least 8 characters (at most 128), with an uppercase letter, a lowercase letter and a digit — the rule account creation uses — and cannot be the current one. Click Save: the old password stops working at once.
- Every other browser where you are signed in is signed out: it can read and change nothing more, and its next page load asks it to sign in. The one you changed it in stays signed in. If someone else has signed in as you, changing the password ends their session.
- Your API token does not change, and neither does a
credentials.jsonfile: rotate the token on the same card if you think someone else has it. - Five attempts a minute (twenty an hour) are allowed; a wrong current password counts. Every change, and every refused one, is in the Ops Log, never with a password.
Configure API Access
Use your API token with any HTTP client (curl, Postman, Python requests, etc.):
# Set your credentials
export TENANT_ID="your-tenant-id"
export API_TOKEN="your-api-token"
export API_URL="https://api.YOUR_DOMAIN"
# Test API access
curl -k -X GET "$API_URL/tenants/$TENANT_ID/vpcs" \
-H "X-Auth-Token: $API_TOKEN"
API Roles and Tokens
Circloud exposes two authentication scopes:
| Scope | Header | Used For |
|---|---|---|
| Admin | X-Admin-Token |
Tenant lifecycle, domains, provider config, service tunnels |
| Tenant | X-Auth-Token |
VPCs, subnets, pods, tunnels, topology |
Many create endpoints also expect the token field in the JSON body. The header is still required
and is the primary authentication check.
Deploy Using Topology JSON
Create a topology.json file to define your infrastructure:
{
"token": "YOUR_API_TOKEN",
"vpcs": [
{
"name": "my-vpc",
"description": "My VPC",
"enable_internet": true
}
],
"subnets": [
{
"name": "my-subnet",
"vpc_name": "my-vpc",
"cidr": "10.10.1.0/24",
"gateway_ip": "10.10.1.1"
}
],
"pods": [
{
"name": "my-app",
"vpc_name": "my-vpc",
"subnet_name": "my-subnet",
"container_image": "nginx:latest",
"replica_count": 2,
"service_port": 80,
"external_access": true,
"auto_scaling": true,
"autoscale_max_replicas": 6,
"autoscale_cpu_target_millicores": 250
}
]
}
Deploy with a single command:
curl -k -X POST "$API_URL/tenants/$TENANT_ID/topology" \
-H "Content-Type: application/json" \
-d @topology.json
Using Private Container Registries
To pull images from private registries (Docker Hub, Harbor, etc.), add registry credentials:
{
"token": "YOUR_API_TOKEN",
"registry_credentials": [
{
"name": "dockerhub",
"server": "https://index.docker.io/v1/",
"username": "your-username",
"password": "your-access-token"
}
],
"vpcs": [...],
"subnets": [...],
"pods": [...]
}
Change Password
POST /tenants/{tenant_id}/change-password with your token in X-Auth-Token
and current_password and new_password in the body: the same rules and
limits as in the portal (see the API reference).
Virtual Private Clouds (VPCs)
A VPC is an isolated virtual network where you deploy your resources. Each VPC has its own routing table and is completely isolated from other VPCs.
Names
Every name you choose — tenant, VPC, subnet, pod, tunnel and subdomain — uses
lowercase letters, digits and hyphens, starting and ending with a letter or digit, at most 63 characters. The API answers a name outside the rule with 400 and that sentence.
The platform builds the names of the cluster objects behind a VPC, subnet, pod or tunnel from
your tenant name, a short id and the names you chose (for example team-a-469e-shop-web),
so a name that fits the rule on its own but pushes one of those past 63
characters is refused too, with a 400 that names the object, its limit and the name it would have had.
Creating a VPC
Navigate to the VPCs tab and click "Create VPC". Enter a name and choose whether to enable internet access.
curl -k -X POST https://api.YOUR_DOMAIN/vpcs \
-H "X-Auth-Token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "YOUR_TENANT_ID",
"token": "YOUR_TOKEN",
"name": "production-vpc",
"enable_internet": true
}'
name follows the name rule.
Deleting a VPC
Click the delete button (×) on any VPC card to remove it. This is a cascade delete — all subnets, pods, tunnels, DNS records, and persistent storage within the VPC are permanently removed. This action cannot be undone. The cloud storages that only this VPC's pods use go with it too: the dialog says how many and names them, and its switch (on to start with) keeps them when switched off — over the API, delete_cloud_storage=false. See Cloud Storage.
A delete that fails part way keeps the VPC in your VPC list, marked Failed: delete it, and says what failed — a pod whose data could not be removed from its node in time, for example — and what was deleted before it. Nothing new can be added to it; delete it again to carry on where it stopped.
Migrate External: Another Cluster
Migrate external moves a VPC to another cluster of the platform, into an existing tenant of that cluster. The target is one of the other clusters on this cluster's Cloudflare account, of the same Circloud profile, chosen from a list: nobody types the target's address. A cluster is listed once it is approved on the Circloud profile. The migration also needs the target tenant's ID and its API token: both are on the target tenant's Config → Authentication card, in the target cluster's user portal. No administrator of either cluster takes part. The target cluster checks the token before anything moves, and the platform keeps it, encrypted, only until the migration ends.
The Migrate external button is on the VPC's card (the administrator has it on every VPC, beside Migrate internal, which moves a VPC to other nodes of the same cluster), and opens its card in a pop-up, as +Pod does. The card asks for the Target cluster, chosen in its list — each cluster by its name and API address — the Target tenant ID and the Target tenant API token. With no other cluster on this cluster's Cloudflare account, the list says so.
Press Check to preview the migration: what is created on the target cluster, how much data is copied and how long that takes at the uplink last measured on this cluster, which addresses move, how long the VPC is split between the two clusters and what is down during the migration — or, in one sentence, why it cannot start (a wrong token, a pod name already taken, an older platform version on the target cluster). Then press Start and confirm. The card follows the migration step by step.
The clusters a VPC can be migrated to: a dry run that names no target.
curl -k -X POST "$API_URL/vpcs/$VPC_ID/migrate/external" \
-H "X-Auth-Token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"dry_run": true}'
# 200 {"vpc_id": "...", "vpc_name": "production-vpc", "plan": null,
# "clusters": [{"id": "TARGET_CLUSTER_ID", "name": "mycluster", "host": "api-mycluster.example.com"}]}
Then the plan, and the migration itself without dry_run:
curl -k -X POST "$API_URL/vpcs/$VPC_ID/migrate/external" \
-H "X-Auth-Token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to_cluster": {
"cluster_id": "TARGET_CLUSTER_ID",
"tenant_id": "TARGET_TENANT_ID",
"token": "TARGET_TENANT_TOKEN"
},
"dry_run": true
}'
cluster_id is one of clusters; any other is refused. A domain the target
tenant does not hold answers 409 with missing_domains (each domain and the
addresses on it), and "skip_missing_domains": true migrates without those addresses.
GET on the same path follows the migration (see the API reference). The
administrator's Migrate internal, to other nodes of the same cluster, is
POST /vpcs/{vpc_id}/migrate/internal.
Domains first. The target cluster creates an address only on a domain the target tenant holds. When the target tenant does not hold the domain of one of the VPC's addresses, Check says which domains and which addresses, and asks whether to migrate without them. Confirm, and the migration goes on without those addresses: they are not created on the target cluster, and they stop working when the VPC is deleted from this cluster. Cancel, add the domain to the target tenant (its administrator adds it, or the tenant requests it in its user portal), then check again.
A VPC with a pod that has both persistent storage and a cloud storage cannot be migrated to another cluster yet: the pod's persistent folder would not be copied, so the migration is refused, naming the pod, and nothing moves.
The migration runs in the platform, step by step:
- the VPC is found on the target cluster by name, or created with the same internet setting, then its subnets with the same names and ranges;
- every pod is created stopped — the same image, replicas, port, environment and options; a pod's cloud storage is attached by its UUID (both clusters use the same Cloudflare account; nothing is copied); the registry credentials its images need are copied first;
- the firewall rules are copied before any pod starts;
- each pod with persistent storage and no cloud storage, one at a time, is stopped on this cluster (its application gets to stop cleanly), its data is copied and checked on both sides, and the pod starts on the target cluster;
- each pod with cloud storage, then each stateless pod, starts on the target cluster while it still runs on this cluster;
- a check that every pod runs on the target cluster as it ran on this cluster;
- each address is stopped on this cluster and created on the target cluster with the same hostname — except the addresses left out for a missing domain;
- the VPC is deleted from this cluster. Its cloud storages are kept.
From when the first pod with persistent storage stops until the migration finishes, the VPC is split: its pods on one cluster cannot reach those on the other. The VPC, its subnets and its pods get new IDs on the target cluster; their names stay, and so do the names pods reach each other by. Its pods reach the internet from the target cluster's addresses.
A failure before a pod runs on the target cluster puts the pod back on this cluster, unchanged, and stops the migration; a pod whose data already moved runs on the target cluster and stays stopped on this cluster, and the failure says what runs where. Starting the migration again continues where it stopped.
Platform Services
Every VPC comes with the services it needs, created and maintained by Circloud:
- its own DNS (2 replicas) for internal name resolution
- tunnel connectors, when you create tunnels
- stable VIP addresses used by DNS records
Internet Access
When you enable internet access on a VPC, Circloud configures external routing so pods can be granted outbound or inbound connectivity. Internet access is controlled at the VPC level and refined per pod.
| Setting | Scope | Effect |
|---|---|---|
enable_internet |
VPC | Enables external routing so EIP, SNAT, and FIP can be allocated |
external_access |
Pod | Allocates an EIP and SNAT for outbound internet |
external_inbound |
Pod | Creates a FIP for bidirectional inbound/outbound access |
external_inbound requires external_access=true. For HTTP/HTTPS
services, Cloudflare tunnels are often simpler than direct inbound IPs.
Subnets
Subnets divide your VPC into smaller network segments. Each subnet has its own CIDR block and can host multiple pods.
Creating a Subnet
Navigate to the Subnets tab and click "Create Subnet". Select a VPC, enter a name, CIDR block, and gateway IP.
curl -k -X POST https://api.YOUR_DOMAIN/subnets \
-H "X-Auth-Token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "YOUR_TENANT_ID",
"token": "YOUR_TOKEN",
"vpc_id": "VPC_ID",
"name": "web-tier",
"cidr": "10.10.1.0/24",
"gateway_ip": "10.10.1.1"
}'
name follows the name rule.
Deleting a Subnet
Click the delete button (×) on any subnet to remove it. All pods within the subnet are deleted along with their tunnels and persistent storage — and the cloud storages that only those pods use, unless the dialog's switch is off (over the API, delete_cloud_storage=false). A subnet listed without a delete button is the platform's own and cannot be deleted.
The pods go one at a time. If one cannot be deleted — its data not removed from its node in time, for example — the delete stops there and says which pod, why, and which pods were deleted before it. The rest of the subnet stays as it was; delete the subnet again to carry on.
Security Rules
Security rules are a per-VPC firewall for your workloads. Each rule allows or
denies ingress or egress traffic between a
remote (an IPv4 CIDR — 0.0.0.0/0 is the internet, i.e. every address — or
this VPC) and a target inside the VPC (the whole VPC, one subnet,
or specific pods), for a protocol and optional ports.
Every VPC gets its own rule set, managed from the Security tab inside the VPC.
How Rules Are Evaluated
- Lowest priority wins — rules are evaluated in priority order (1–2000), lowest number first, and the first match decides.
- Deny beats allow — at the same priority, a deny rule is evaluated before an allow.
- Implied baseline — three fixed implied rules frame everything you write: always allow the platform's own services in the VPC (its DNS, tunnels and load balancers, in both directions — no rule can block them, so a deny never takes out your own DNS), and beneath your rules allow all outbound and deny all other inbound. They cannot be edited, reordered, or deleted, and the panel shows them as a static note.
- The gateway check — before a pod starts, the network pings its subnet's gateway, and the pod cannot start without the reply. That ping and its reply are always allowed, whatever your rules say, so narrowing or deleting Allow internal traffic, or denying all outbound traffic, never stops your pods from starting. Nothing else is let through: a pod's ping to its own subnet's gateway is the only traffic it adds.
- Seeded rule — every new VPC starts with one ordinary rule, Allow internal traffic (priority 2000), so pods in the VPC can reach each other. It is editable, disableable, and deletable like any other rule; deleting it leaves you with exactly the implied baseline.
Managing Rules
Open the VPCs tab, select a VPC, and switch to its Security tab. Click +Rule to add a rule — pick a direction, action, priority, protocol and ports, remote, and target. Use the pencil to edit a rule, the × to delete it, and the Enabled switch to keep a rule while pausing its enforcement. To reorder rules, edit their priorities — the lowest number is evaluated first. The direction dropdown filters the table to ingress or egress rules.
| Column | Meaning |
|---|---|
| Priority | Evaluation order (1–2000). The lowest number is checked first; the first matching rule wins. |
| Direction | ingress (traffic into your pods) or egress (traffic out of your pods) |
| Action | allow or deny the matched traffic |
| Protocol / Ports | tcp, udp, icmp, or all. Ports (e.g. 443, 8000-9000, or a comma list) apply only to tcp and udp; blank means all ports. |
| Remote | The other end of the connection: an IPv4 CIDR (0.0.0.0/0 is the internet) or this VPC, which covers traffic between its own subnets |
| Target | What the rule protects inside the VPC: all pods in the VPC, one subnet, or specific pods |
| Enabled | A disabled rule is kept but not enforced |
All security-rule endpoints authenticate with your tenant token
(X-Auth-Token), and all enum values (direction,
action, protocol, remote_type,
target_type) are lowercase. List a VPC's rules in evaluation order:
curl -k -X GET "https://api.YOUR_DOMAIN/security-rules?vpc_id=VPC_ID" \
-H "X-Auth-Token: YOUR_TOKEN"
Create a rule — for example, allow inbound HTTPS from the internet to the whole VPC:
curl -k -X POST https://api.YOUR_DOMAIN/security-rules \
-H "X-Auth-Token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"vpc_id": "VPC_ID",
"name": "allow-https-from-internet",
"direction": "ingress",
"action": "allow",
"priority": 100,
"protocol": "tcp",
"ports": "443",
"remote_type": "cidr",
"remote_cidr": "0.0.0.0/0",
"target_type": "vpc"
}'
Every mutation saves the rule and then applies it to the VPC's network, and the response
reports the result: applied: true means the rule is in force,
applied: false means it is saved but not yet in force (apply_error says
why) — the platform retries automatically every few minutes.
The full reference — get, update, delete, and bulk reorder, with every parameter, validation rule, and error shape — is the API's own, at https://api.YOUR_DOMAIN/docs under Security Rules.
Admin Read-Only View
Admins get the same table as a support view: in the CloudManager dashboard, open Tenants, select a tenant, open one of its VPCs, and switch to the Security tab. The view is read-only — no add, edit, or toggle — and is rendered from the same payload the tenant sees, so admin and tenant can never disagree about what is enforced.
DNS & Service Discovery
Each VPC includes a dedicated DNS service (dnsmasq), run by the platform. The platform configures pod DNS settings so service names resolve inside the VPC, while external domains are forwarded to the provider DNS servers.
Resolution Flow
pod -> the VPC's DNS -> external DNS forwarder
Record Format
Pods and VIP services are registered automatically. Names are stable even when pods restart.
web.my-tenant
web
HA for DNS
DNS runs two replicas. Pods are configured with both DNS IPs when available, providing resilience if one replica is unavailable.
Provider DNS Settings
External DNS servers and the ndots setting come from provider configuration. For self-hosted labs,
these map to pod_dns_nameservers and pod_dns_ndots in your configuration.
Registry Credentials
Registry credentials let your pods pull private images. Credentials are stored in the tenant database
and synchronized to a platform credential named <tenant>-registry-creds,
which is attached to your pods automatically. The platform registry is in it already, after yours
(see The Platform Registry).
nginx:latest, postgres:16), or on the platform registry, no registry
credentials are needed. You only need to add registry credentials when pulling from private registries
like GitHub Container Registry (ghcr.io), AWS ECR, or private Docker Hub repositories.
Adding Registry Credentials
Navigate to the Registry tab and click "Add Registry". Enter a name, server URL, username, and password or token.
curl -k -X POST https://api.YOUR_DOMAIN/tenants/TENANT_ID/registry-credentials \
-H "X-Auth-Token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "dockerhub",
"server": "https://index.docker.io/v1/",
"username": "myuser",
"password": "mytoken"
}'
Reusing a credential name updates the existing entry. Listing credentials hides passwords in responses.
Pods use these credentials when use_registry_credentials is true (default). The name
platform is the platform registry's, and the registries the platform itself uses cannot be
added: the API refuses them.
Your Default Registry
One of your registries is your default: the one your credentials.json download carries as
default_docker_registry, with its server, username and password, for the app template and
your AI agent to push your images to. No other registry's password is in the file. Deleting your
default leaves you with none.
"default_docker_registry": {"server": "https://index.docker.io/v1/", "username": "myuser", "password": "mytoken"}
Without a default the file carries placeholders, so the agent knows to ask you for a registry before it pushes anything:
"default_docker_registry": {"server": "ADD_YOUR_DOCKER_REGISTRY_URL_HERE",
"username": "ADD_YOUR_DOCKER_REGISTRY_USERNAME_HERE",
"password": "ADD_YOUR_DOCKER_REGISTRY_PASSWORD_HERE"}
Press Set default on its row of the Registry card (it is badged Default from then on), then download the file again.
The same request that adds a registry sets the default: "is_default": true, and for a
registry you have, its name and "is_default": true are the whole request
— its server, username and password are kept.
curl -k -X POST https://api.YOUR_DOMAIN/tenants/TENANT_ID/registry-credentials \
-H "X-Auth-Token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "dockerhub", "is_default": true}'
The Platform Registry
Your images can live on the platform itself, before you have a registry anywhere else: the platform
registry, at registry-<cluster>.<your-domain> once your administrator publishes it. It is a
registry of its own for tenants — not where the platform keeps its own images — and each
tenant has its own space in it, registry-<cluster>.<your-domain>/<tenant name>/, with its
own login. Nobody else can read, pull or overwrite your images, and you cannot see anyone else's.
- Your login — the username is your tenant's name; the password is on the
Registry card, on its last row,
platform(its copy button). It is not in yourcredentials.jsondownload, which carries your default registry only. It changes when your API token is rotated: log in again. - Push — tag your image with your space and push it:
docker login registry.example.com -u TENANT_NAME # the password from the Registry card
docker tag web:1 registry.example.com/TENANT_NAME/web:1
docker push registry.example.com/TENANT_NAME/web:1
- Run — create a pod (or a topology) with
"container_image": "registry.example.com/TENANT_NAME/web:1". Your pods pull it with the login they already have; an image in someone else's space is refused. Every pod pulls its image again whenever it starts. - The app template pushes to your default registry
(
default_docker_registry); without one, itsdeploy.shasks for a registry — the platform registry among them — keeps it in the file and makes it your default.
Pods
Pods are the deployable units that run your containerized applications. Each pod can have multiple replicas for high availability and automatic load balancing.
Pod Features
When creating a pod, you can configure these capabilities:
- Auto-healing — When enabled, Circloud automatically restarts pods that crash or become unresponsive. The platform monitors pod health and recreates failed containers without manual intervention.
- Auto-scaling — Pods can scale horizontally based on CPU or memory usage. Set minimum and maximum replica counts, and the platform adjusts automatically based on load.
- Persistent storage — Pods can request node-local persistent storage. Storage is created under
/storage/<tenant-id>/<pod-id>and mounted into the container. Persistent pods are pinned to a single worker node. - Cloud storage — A pod with persistent storage can also have a cloud storage: files kept off-site under a UUID that any of your pods can use, for exports, shared files and backups on your own schedule. See Cloud Storage.
- Replicas — Run multiple identical copies of a pod for redundancy and load distribution. Traffic is automatically balanced across replicas via the internal load balancer.
- Internet access — Controlled at pod level.
external_accessenables outbound internet,external_inboundenables bidirectional access with a floating IP. - GPU access — Pods can request GPU resources on nodes that have GPUs configured. Set
gpu_access: trueto schedule the pod on a GPU-enabled worker.
Creating a Pod
Navigate to the Pods tab and click "Create Pod". Select a subnet, enter a name and an image, set the replicas and the service port, then switch on what the pod needs: outbound or inbound internet access, persistent storage — and cloud storage, which needs it — auto-healing, auto-scaling, and a tunnel. Every option is a switch; a switch that cannot be used yet is greyed out, and hovering it says why (Enable Cloud Storage stays greyed out until Enable Persistent Storage is on).
Pod Actions (Actions Column)
Actions appear on the first row of each pod group.
| Icon | Action |
|---|---|
| ▶ | Start a stopped pod workload, or recreate a missing pod (A Missing Pod) |
| ■ | Stop the pod workload |
| ↻ | Restart the pod workload |
| + | Add a replica (up to 10) |
| × | Remove a replica (minimum 1) |
| 📋 | View pod logs with colorized output, copy, and download |
| 🗑 | Delete the pod and all its resources (tunnels and persistent storage are removed in cascade). Its cloud storage goes with it when no other pod uses it — switch off Also delete its cloud storage in the dialog to keep it; see Cloud Storage |
The amber × is for replica removal; the red × is for deleting the pod.
When autoscaling is enabled, manual replica changes are ignored and the platform controls scaling.
Viewing Logs
Click the logs button on any pod to open the colorized logs viewer. You can copy logs to clipboard or download them as a text file. Logs are fetched in real-time from the running container.
Service VIPs and Replicas
Circloud creates a VIP service for each pod group. For single pods, the VIP provides a stable IP. For replicas, one VIP load-balances traffic across all replicas.
The Service IP column in the GUI shows this VIP address.
replica pods: api-0, api-1, api-2
VIP service: api -> 10.x.x.x (stable VIP)
curl -k -X POST https://api.YOUR_DOMAIN/pods \
-H "X-Auth-Token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "YOUR_TENANT_ID",
"token": "YOUR_TOKEN",
"vpc_id": "VPC_ID",
"subnet_id": "SUBNET_ID",
"name": "web-server",
"container_image": "nginx:latest",
"replica_count": 2,
"service_port": 80,
"external_access": true,
"external_inbound": true,
"auto_scaling": true,
"autoscale_max_replicas": 6,
"autoscale_cpu_target_millicores": 250
}'
name follows the name rule.
Pod Options
| Option | Description |
|---|---|
replica_count |
Replica count (1-10). If auto_scaling is true, this is the initial replica count (min 2) |
service_port |
VIP service port for load balancing and tunnels (default 80) |
use_registry_credentials |
Use tenant registry secret for image pulls (default true) |
external_access |
Enable outbound internet via SNAT |
external_inbound |
Enable inbound access with external IP |
auto_healing |
Auto-restart pods on failure |
auto_scaling |
Enable automatic scaling (CPU-based) |
autoscale_max_replicas |
Maximum replicas when autoscaling (default 10) |
autoscale_cpu_target_millicores |
Target average CPU usage per pod in millicores (default 200) |
persistent |
Persistent storage for the pod, mounted at persistent_mount_path (default /storage). Cloud storage needs it |
cloud_storage |
Give this pod a cloud storage (needs persistent: true). Your container gets the S3 variables — S3_URL, STORAGE_UUID and its own key pair — see Cloud Storage |
cloud_storage_id |
The UUID of a cloud storage to attach instead of creating a new one: one of your storages, or one you do not hold yet, which the platform finds on the Cloudflare account and assigns to you (a storage made by another cluster, an earlier install or a tenant deleted since). Implies cloud_storage: true and needs persistent storage. Not found anywhere: 404 |
start |
API only. false creates the pod without running it — its record, its
addresses inside the VPC, its node, the place for its persistent folder, its cloud storage
— and it reads as stopped; POST /pods/{pod_id}/start runs it. A pod with
persistent storage created this way can first be filled through
POST /pods/{pod_id}/storage/import, which is how Migrate external copies each
pod's data to the target cluster (default true) |
Automatic Scaling
Enable auto_scaling to have the platform add and remove replicas automatically, based on
average CPU usage. Auto-healing is implied (the platform manages the replica set for you), and manual
replica changes are disabled. The
replica_count is treated as the initial value and clamped to the autoscale min/max. When
autoscaling is enabled, the minimum replica count is always 2.
Edit Pod Configuration
You can modify pod settings after creation by clicking the edit icon in the flags column. This opens the Pod Configuration modal where you can toggle auto-healing, persistent storage, auto-scaling and cloud storage (and choose which storage) without recreating the pod. Cloud storage needs persistent storage: its switch opens once persistent storage is on, and persistent storage cannot be turned off while cloud storage is on — turn cloud storage off first.
When a Pod Would Go Over Your Quota
Your administrator gives your account a quota — CPU, memory, storage, pods, VPCs and subnets; the strip at the top of the Monitoring tab shows where you stand. A request that would start a pod past your CPU or memory quota — creating one, starting, restarting or scaling it, changing its configuration, a tunnel or cloud storage — is refused with one sentence that counts the whole request, every replica included, and a pod the request had begun to create is removed again:
When the request alone needs more than your whole quota, it says that instead: This needs more than
your whole quota: … Ask the administrator for more. Over the API the answer is
409 with detail.code QUOTA_EXCEEDED, the sentence in
detail.message, the clause it is built around in detail.usage, and the
figures — what was asked for, in use and allowed, per resource — in
detail.quota. A change that had already
stopped the pod when it was refused says so and leaves the pod stopped, and its probe says which request
stopped it: start it once there is room. A
pod with auto-healing or auto-scaling is started after the answer, so the request succeeds and a replica
over your quota stays not started. The administrator sees the same refusal naming your account and the
limit to raise (Max CPU or Max RAM).
A Missing Pod
A pod can stop running without anyone stopping it — the server it ran on restarted, for example. When the platform has not seen a pod running for ten minutes, and the pod was not stopped, it marks the pod Missing (⭕) and says since when. It can be started again or deleted:
- Start recreates it from its record: its image, environment, storage and network, its name in the VPC and its tunnels, and the address it had in its subnet if no other pod has taken it. Its tunnels come back once their VPC's internet path is confirmed, as for any tunnel started (Tunnels): when it cannot be, the pod is not made again and the Start says so — start it again in a minute.
- Delete removes it, as for any pod.
The pod's row reads Missing, its probe says since when, and Start and Delete are on the row.
The pod reads lifecycle_status "missing" with missing_since.
POST /pods/{id}/start recreates it (a restart does the same) and answers
"recreated": true; DELETE /pods/{id} deletes it.
Each missing pod raises one Pod missing alert that says since when; it clears as soon as the pod runs again or is deleted. A pod is never marked missing while it is being created, started, stopped, restarted, changed, scaled, migrated or deleted, nor when a migration that did not finish left it for the next migration of its VPC to start again — until the pod runs again.
A pod the cluster refuses to create — over your quota, for example — is not missing: it reads Failed, and its probe gives the reason.
A Stuck Update
A restart of a pod with auto-healing or auto-scaling, on a new image or not, is an update, and so is a migration that moves it: a new copy of the pod is made beside the one that runs, and the old copy is retired once the new one is ready. While the update is under way, the pod shows the new copy. When the new copy has made no progress for ten minutes, the update is stuck: the pod shows the old copy, which keeps serving and reads Ready, and the update is marked stuck, with since when, why, and what to do:
- Cannot download image — no image has that name and tag, or the registry refuses the registry credentials. Once the image can be downloaded, the update finishes by itself.
- Cannot be created — the quota has no room for the new copy, for example; the probe gives the figures. It is created once there is room.
- No node can take it — it is placed once a node has room for it.
- Keeps stopping, or did not become ready in time.
The Update stuck mark beside the pod's name opens since when, why, and what to do, and its probe names the previous image when it was another one. Restart in the portals keeps the pod's image: to undo the update, or to try another image, use the API.
To undo the update, or to try another image, restart the pod on that image:
POST /pods/{id}/restart?new_image=<image>. GET /pods/{id}/status
reads the copy that serves, so a stuck update reads ready there; the alert is what says it:
GET /alerts?category=pod-update-stuck.
A pod with persistent storage is updated the other way round: its copy stops first, and the new copy starts once it has stopped, so two copies never use its data at the same time. The pod is down while it stops and starts again, and an update that cannot finish leaves it down: its status says why, and the update is undone the same way.
Each stuck update raises one Pod update stuck alert, dated by when it began; it clears as soon as the update completes or is undone. An update in progress is never marked: a copy still downloading its image or starting, and a download that failed once, are not stuck — only ten minutes without progress is.
Cloud Storage
Cloud storage keeps your pod's files off-site: exports, uploads, and copies of what the pod keeps on its
persistent storage — its backups. Each storage has a UUID. It outlives the pod that
created it, any of your pods can use it by that UUID, and a pod reads and writes it over standard
S3, at https://cloud-storage, with a key pair the platform gives the pod: no
account to manage.
What it needs
- Persistent storage on the pod. A pod gets cloud storage only together with persistent storage, and persistent storage cannot be turned off while cloud storage is on.
- Internet access on the pod's VPC.
- Cloud storage enabled by your administrator — see When cloud storage is not available.
Turning both off in one change is fine: the pod is detached and the storage and its files are kept.
Enable Cloud Storage stays greyed out until Enable Persistent Storage is on — hover it to see why.
A request that breaks the persistent-storage rule is refused before anything is created or changed,
with one sentence: Cloud storage needs persistent storage — turn persistent storage on for
this pod as well., or Persistent storage cannot be turned off while this pod has cloud
storage — turn cloud storage off first. The answer is 409 on create and on a
configuration change, and 400 for a topology document, with the pod named first.
Turning it on
On the create-pod form, switch on Enable Persistent Storage, then Enable Cloud Storage, and leave Storage on New storage — or pick one of yours.
The Storage tab has two tables. Persistent Storage lists each of your pods with persistent storage: how much its folder holds, the cluster, its VPC and subnet, the node the folder is kept on, and its cloud storage. Cloud Storage (S3) lists your storages with their UUIDs (each with a copy button), the pods using each one, the DB clusters whose backups are in it, and how much it holds. Assign on a storage gives it to one of your pods with persistent storage, which restarts with it; a storage no pod uses has a × to delete it. Both tables list your own storages and pods only. A storage marked Temporary was made to carry a pod's persistent storage to another cluster while its VPC migrates to it: no pod is given it, it cannot be deleted while the migration runs, and it goes when the migration ends. A VPC's Cloud Storage tab lists the storages its pods use and yours that no pod uses yet. A pod with cloud storage carries a C chip in the Flags column of the Pods table, and its storage is named in the table's Cloud Storage column.
Send persistent: true and cloud_storage: true with the pod; the storage's
UUID comes back as cloud_storage_id, and each pod's cloud_storage_id
(GET /pods) names the storage it uses.
"persistent": true,
"cloud_storage": true
# 201 {"id": "...", "persistent": true, "cloud_storage": true,
# "cloud_storage_id": "3f6d2c1e-8b4a-4e1f-9c2d-7a5b6e8f9a01", ...}
Reading and writing files
Your container gets standard S3 settings: any S3 client works with them, and current AWS SDKs and the
AWS CLI need nothing else — they read AWS_ENDPOINT_URL_S3 (boto3 1.28, AWS CLI 1.29
and 2.13, or newer). Give an older one the endpoint yourself: --endpoint-url "$S3_URL".
| Variable | Value |
|---|---|
S3_URL |
https://cloud-storage — your VPC's cloud storage, the same name in every VPC. |
STORAGE_UUID |
The storage's UUID: S3 clients address the storage by it (s3://$STORAGE_UUID/<key>). |
S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY |
This pod's key pair. It reaches this pod's storage and nothing else. |
S3_REGION |
auto |
S3_CA_BUNDLE |
/etc/cloud-storage/ca.crt — the certificate authority https://cloud-storage
is checked against. |
AWS_ENDPOINT_URL_S3, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY,
AWS_REGION, AWS_DEFAULT_REGION, AWS_CA_BUNDLE |
The same, for the AWS SDKs and the AWS CLI. Given only when your own variables name no
AWS_ variable, so a pod that talks to AWS keeps its own. |
NODE_EXTRA_CA_CERTS |
The same certificate file, for Node.js — unless you set it. |
# Store a file, list what you have, fetch it back, remove it.
aws s3 cp june.csv "s3://$STORAGE_UUID/reports/june.csv"
aws s3 ls "s3://$STORAGE_UUID/reports/"
aws s3 cp "s3://$STORAGE_UUID/reports/june.csv" june.csv
aws s3 rm "s3://$STORAGE_UUID/reports/june.csv"
# The same from Python: boto3 reads the variables by itself.
python3 -c 'import boto3, os; boto3.client("s3").upload_file("june.csv", os.environ["STORAGE_UUID"], "reports/june.csv")'
A client you give S3_URL yourself uses path-style requests
(https://cloud-storage/<UUID>/<key>; boto3 does on its own) and the certificate in
S3_CA_BUNDLE. The key pair is never among the pod's environment variables as the dashboard and
the API show them. A presigned URL works inside the VPC only.
Keys, operations and refusals
- One storage per key pair: S3 clients address the storage by its UUID, and the pod's key
pair reaches that storage alone — naming another is
403 AccessDenied. Refusals are S3 error documents, as any S3 client reads them. - A key is a path you choose —
reports/june.csv, at most 900 characters. It must not start with/, contain\, a.or..segment, an empty segment or a control character, and it is refused rather than tidied up (400 InvalidArgument). A trailing/(a folder marker) is fine. - What works: get (ranges and conditions included), head, put, copy within the storage,
delete and the multi-object delete, listings, multipart uploads and presigned URLs. Anything else
— ACLs, tagging, versions, object locks, encryption with your own keys — is
501 NotImplemented. - Checksums CRC32, SHA-1 and SHA-256 are checked as the bytes arrive: a file that fails
one is not stored (
400 BadDigest). CRC32C and CRC64NVME are refused, and so is a checksum of a whole multipart upload: each part's is checked instead. - Sizes: up to 5 GiB in one upload, larger as a multipart upload whose parts — all but the last — are the same size, at least 5 MiB.
- A wrong key pair is
403, and so is the key pair of a storage removed from you; a key you have not stored is404 NoSuchKey. - When a node fails,
https://cloud-storagekeeps its address and answers again within seconds. A request in flight at that moment fails, and S3 clients retry on their own.
Sharing a storage between pods
A storage is yours, not the pod's. Give another pod the same storage and both pods read and write the same objects — for handing files from one service to another, a shared download cache, a replacement pod picking up where the old one stopped, or anything else. The pods can be in any of your VPCs, and there is no limit on how many share one storage.
A UUID is a capability: one you
do not hold yet is looked for on the same Cloudflare account — a storage made by another cluster
connected to it, an earlier install or a tenant deleted since — and, when found, is assigned to you
on the spot and marked Assigned on your Storage tab. A UUID found nowhere is refused with
404. Giving one of your storages to another tenant of the same cluster is not available,
to keep tenants isolated (such a UUID is refused as that tenant's).
A tenant of another cluster uses it by its UUID, or once that cluster's administrator assigns it
(below). Each pod has its own key pair: a pod reaches the storage it is
attached to and nothing else.
Turning cloud storage off detaches the pod and keeps the storage and its objects. Changing a pod's storage, or turning it on or off, restarts the pod, because the S3 variables reach the container through its environment.
Using a storage on another cluster
A storage's UUID works on another cluster connected to the same Cloudflare account too. Pass it as
cloud_storage_id when you create a pod on that cluster (or change one): the platform finds
it on the account and assigns it to you on the spot — or the administrator of that cluster assigns
it to you from the UUID on your Storage tab. Either way a storage is found once it holds at least one
file (an empty one cannot be found). It then appears on your Storage tab on that cluster, marked
Assigned (its date is when it was assigned), and in the Storage choice
of the pod form. You use it exactly like a storage of your own: pick it, or pass its UUID as
cloud_storage_id, in any of your VPCs, and your pods read and write the same files the pods
of the other cluster do. It needs persistent storage on the pod, like every cloud storage. Migrate
external attaches the pods' cloud storages on the target cluster this way.
Right after such a storage is attached, a request can be answered 503 with a
Retry-After header while the storage is being connected: wait the seconds it says and send it
again.
A cluster never deletes the files of a storage it did not create. Deleting an assigned storage (from its
row, over the API, or with its last pod: Deleting)
removes it from that cluster only — its files are kept, and the dialog and the answer
("files": "kept") say so. To empty it, delete its files from a pod that uses it before you
remove it. Deleting it on the cluster that created it (if that cluster still exists) deletes its files
for every cluster using it, so check that none still needs it first.
Once a storage is removed from you — you remove it, or the administrator gives it to another tenant — none of your pods can reach it any more, even one that still has its key pair.
Backing up with cloud storage
There is no separate backup feature: a backup is a copy your pod puts into its cloud storage, when and as often as you decide — after every export, once a night, before a risky migration. Your application knows when its data is consistent, so it is the one to make the copy. Write it to the pod's persistent storage first, then upload it under a name that says when it was taken:
#!/bin/sh
# backup.sh — runs inside the pod: from a cron job, after an export, or by hand.
set -e
STAMP=$(date -u +%Y%m%dT%H%M%SZ)
mkdir -p /storage/backups
# 1. A consistent copy on the pod's persistent storage (here, a PostgreSQL dump).
pg_dump -Fc -f /storage/backups/app.dump app
# 2. Keep it off-site, named by when it was taken.
aws s3 cp /storage/backups/app.dump "s3://$STORAGE_UUID/backups/app-$STAMP.dump"
# The copies you have:
aws s3 ls "s3://$STORAGE_UUID/backups/"
# Restore one — in this pod, or in any pod with the same cloud_storage_id:
aws s3 cp "s3://$STORAGE_UUID/backups/app-20260930T020000Z.dump" /storage/backups/restore.dump
pg_restore --clean -d app /storage/backups/restore.dump
# Retire one you no longer need:
aws s3 rm "s3://$STORAGE_UUID/backups/app-20260901T020000Z.dump"
- The schedule is yours. Run the script from a cron job in the pod, from your application's own timer, or at the end of the job that changes the data.
- So is retention. Nothing is deleted for you: list
backups/and delete the copies you no longer want. - Restore anywhere. The copies belong to the storage, not to the pod: the pod that made
them, a replacement for it, or a pod in another of your VPCs reads them with the same
cloud_storage_id.
Deleting
A storage goes with the delete of its last pod. Deleting a pod, a subnet, a VPC or a tenant deletes every storage that no other pod uses any more — unless you say otherwise. A storage that another of your pods still uses — in any of your VPCs — is kept until that pod goes. A stopped pod counts as using its storage, and so does a pod whose own delete failed half-way. A storage no pod uses stays until you delete it, and cannot be deleted while any pod is attached.
Every delete dialog carries Also delete its cloud storage, switched on to start with: it says how many storages go and names them, and switching it off keeps them all. When a pod still uses a storage, the dialog says it stays. A storage no pod uses is deleted from the Storage tab, with the × on its row.
The delete's delete_cloud_storage is true by default:
# The pod's storage goes with it (when no other pod uses it):
curl -k -X DELETE "https://api.YOUR_DOMAIN/pods/POD_ID" -H "X-Auth-Token: YOUR_TOKEN"
# Keep it, for another pod to use by its UUID:
curl -k -X DELETE "https://api.YOUR_DOMAIN/pods/POD_ID?delete_cloud_storage=false" \
-H "X-Auth-Token: YOUR_TOKEN"
A storage no pod uses is deleted with DELETE /cloud-storages/{storage_id}, which answers
202, and 409 while any pod is attached. When cloud storage is not set up on
the cluster, a delete that would take a storage along is refused before anything is deleted
(409, R2_NOT_CONFIGURED): delete with
delete_cloud_storage=false to keep the storage.
Deleting a storage this cluster created deletes every file in it and cannot be undone — also when a tenant of another cluster has it assigned: the cluster that made a storage is the one that deletes its files, on its own or with the last pod on that cluster that uses it, so check that no other cluster still needs it first. An assigned one keeps its files (above). The operations log entry of every delete says what happened to the storages.
A delete takes effect at once: the storage leaves the Storage tab and no pod can be given it any more. For a storage this cluster created, its files are removed right after, in the background, and that is retried until none is left — a storage with many files can take a few minutes to empty, and nothing needs to be done while it does.
A pod's key pair is for the storage it was given and does not expire. When a pod moves to another storage it is restarted with a new key pair; a copy of the OLD pair made before that (logged, pasted into another system) still reaches the old storage. Treat the key pair like a password and do not copy it out of the pod.
When cloud storage is not available
Cloud storage needs your administrator to have set it up on the cluster.
Until then Enable Cloud Storage and the Storage choice are greyed out — hover one to see why.
Until then a request that asks for cloud storage is refused with 409 and this body,
and nothing is created or changed:
{"detail": {"code": "R2_NOT_CONFIGURED",
"message": "Cloud storage is not available on this cluster yet — ask your administrator to enable it."}}
The same answer comes from creating a pod, changing a pod's configuration, and deleting a storage
this cluster created (removing an assigned one is not refused for this), so a script can test
detail.code once for all of them.
DB Clusters
A DB cluster is a PostgreSQL service in one of your VPCs, made in one step: a primary that takes reads and writes, standbys that follow it and take over when it fails — each instance on a node of its own, with a volume of its own — and continuous backups into a cloud storage of yours from the moment it is created. Every VPC card reads Pods · DB Clusters · Cloud Storage: open a VPC, then its DB Clusters tab. Each row shows the DB cluster's status, its primary, how many of its instances are ready, its write and read addresses, its latest backup and the window a restore can reach.
Creating a DB cluster
On the VPC's DB Clusters tab, click +DB cluster and fill in the form:
- Name — follows the name rule. Applications reach the
DB cluster as
<name>-rwand<name>-ro. - Subnet — one of the VPC's subnets: its instances get their addresses there. Applications anywhere in the VPC reach it by its names.
- Instances — 2 to 5: one primary, the rest standbys. Each runs on a node of its own, so the VPC needs as many usable nodes as instances.
- Size per instance (GiB) — each instance's volume, 1 to 1024. It can grow later, never shrink.
- Backups — on to start with, into the chosen Cloud storage: a new one, or one of yours (each DB cluster writes under a folder of its own). Schedule (cron, UTC) sets when base backups are taken (five cron fields, at most one an hour; empty: every day at 02:MM UTC) and Retention (days) how long they are kept (1 to 365; empty: 14). Backups need cloud storage set up by your administrator and internet access on the VPC.
- Restore from — left on None for a new, empty database (see Backups and restore).
Create makes it: its status reads Creating, then Ready, about a minute later, and a first base backup is taken as soon as the primary is up.
POST /db-clusters takes your tenant ID and token in the body, like a pod's create, and
answers the DB cluster with status creating; read
GET /db-clusters/{db_cluster_id} until it is ready.
curl -k -X POST https://api.YOUR_DOMAIN/db-clusters \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "YOUR_TENANT_ID",
"token": "YOUR_TOKEN",
"vpc_id": "VPC_ID",
"subnet_id": "SUBNET_ID",
"name": "scores",
"replica_count": 2,
"storage_size": "1Gi",
"backup": true
}'
replica_count (2 by default), storage_size ("1Gi"),
database_name (app), backup (true),
cloud_storage_id, backup_schedule and backup_retention_days
are optional. Every refusal comes before anything is made, with its code and a sentence:
DB_CLUSTER_NODES when the VPC has fewer usable nodes than instances (the sentence says
how many it can use), QUOTA_EXCEEDED when your limits cannot hold its instances and
volumes, DB_CLUSTER_EXISTS for a name already taken in the VPC. The full reference is
under DB Clusters in the API's own reference (/docs).
The instances and one more pod, the DB cluster's manager, count against your quota. They are in the VPC's Pods table, managed by the DB cluster: a pod's row names its role (Primary, Standby or Manager) and opens its DB cluster, and stop, restart, change and delete are the DB cluster's own, never a single pod's. An instance's Logs button shows its database log: PostgreSQL's own lines, each with its time and severity. A subnet that holds a DB cluster cannot be deleted on its own, and a VPC that holds one cannot be migrated yet.
Connecting
Applications in the VPC connect on port 5432, with TLS (sslmode=require), to two names:
<name>-rw— writes and reads, on the primary. It follows the primary through a failover and a switchover.<name>-ro— reads, on the standbys.
The login lives with the DB cluster: the platform keeps the password, so it is there whenever you need it. A new password is taken by the database within seconds, and the previous one still works meanwhile; then only the new one does. Open connections stay open; new ones need the new password, so update your applications' configuration.
Details on its row opens its Connection: the write and read
addresses, the database, the username and the password — hidden until you show it — each
with a copy button, and a ready-made connection string. The credentials.json download
(Config → Authentication) lists every DB cluster's login after your default
registry.
Rotate gives the login a new password; Password state reads Applying until the database has taken it.
GET /db-clusters/{db_cluster_id}/connection answers the login: the write and read
addresses, the database, the username, the password and ready-made connection strings
(uri, read_uri). POST /db-clusters/{db_cluster_id}/rotate-password
gives it a new password: password_state reads applying until the database has
taken it, then in_force.
When the primary fails
A standby becomes the primary on its own, and <name>-rw follows it; the write address
answers nothing for the moment that takes.
The Instances table shows each standby's replication: how far behind the primary it is, measured about every minute — what a failover at that moment would lose. The Operation section names the last change of primary: when, from which instance to which, and whether it was a failover or a switchover.
Changing a running DB cluster
One operation runs at a time, on a ready DB cluster, and the Ops Log records it.
The details' Operation section follows an operation until it is done.
- Switchover, on a standby's row — that standby becomes the primary. The primary stops cleanly first, so nothing is lost; writes pause for a few seconds, and applications reconnect. The old primary goes on as a standby.
- Rebuild, on a standby's row — the instance and its volume are deleted and a new one is copied from the primary, on a node with no other instance of the DB cluster: for a damaged volume, or an instance whose node will not come back. Never the primary: switch over first. The DB cluster has one standby fewer until the new one is ready.
- Size per instance and Apply — every instance's volume grows: the instances are replaced one at a time at the new size, the standbys first, then a switchover, then the old primary. Writes pause for a few seconds once. A size never shrinks.
- Instances and Apply — adds standbys, each on a node of its own, or removes the newest ones with their volumes; never the primary.
- Backups — the switch turns backups off (the backups already taken stay restorable, and so does the storage) or on again; Save, at the section's foot, keeps a new schedule and retention.
POST /db-clusters/{db_cluster_id}/instances/{pod_id}/promote— a switchover: that standby becomes the primary.POST /db-clusters/{db_cluster_id}/instances/{pod_id}/rebuild— a standby rebuilt from the primary.PATCH /db-clusters/{db_cluster_id}/configwith one ofstorage_size,replica_countandbackup, or the backup settings.
Backups and restore
A DB cluster backs up continuously to its cloud storage, with base backups on a schedule. When backups have failed for a few minutes, the DB cluster reads Degraded and an alert says so. A point in time can be restored up to the last transaction the backups hold. A DB cluster nobody has written to since its first base backup has no restorable window yet: its backups restore as they were taken.
A restore always makes a new DB cluster: the source is only read. It can come from one of your DB clusters here — restored to the latest its backups hold, as a backup was taken, or to a point in time inside its restorable window — or from the backups of a DB cluster made on another installation connected to the same Cloudflare account, or of one deleted since, by its cloud storage's UUID and its DB cluster ID.
The details' Backups section shows the cloud storage the backups go to, with its UUID; whether the continuous backup is working; the schedule and the retention; the restorable window (the points in time a restore can reach); the DB cluster's ID; and every base backup, with when it was taken, its size and whether it can be restored. Back up takes a base backup now.
Restore on the Backups section opens the create form with this DB cluster as the source and the last moment its backups reach as the point in time; Restore on a backup's row opens it with that backup chosen. Give the new DB cluster a name and a subnet, and Create. In the form, Restore from takes:
- This installation — one of your DB clusters here, restored to the Latest its backups hold, as A backup was taken, or to A point in time (UTC) inside its restorable window.
- Another installation — the backups of a DB cluster made on another installation connected to the same Cloudflare account, or of one deleted since: its cloud storage's UUID and its DB cluster ID, both shown on its Backups section.
POST /db-clusters/{db_cluster_id}/backups takes a base backup now. A restore is
POST /db-clusters with restore: true:
"restore": true,
"cloud_storage_id": "STORAGE_UUID",
"source_db_cluster_id": "SOURCE_DB_CLUSTER_ID",
"recovery_target_time": "2026-10-21T14:30:00Z"
# or "backup_id": "20261021T020000"; neither restores everything the backups hold
The restored DB cluster backs up into a folder of its own of the same storage, so it can be restored in turn, and its details say what it was restored from. A restore its backups cannot reach — no such backup, a time outside the window — fails the new DB cluster with a sentence that says so: delete it and restore again.
Deleting
Deleting a DB cluster removes its instances and their volumes: the database is gone, and it cannot be undone.
Delete is in the details. Switch on Also delete its cloud storage in the dialog to delete the storage too, when nothing else uses it.
DELETE /db-clusters/{db_cluster_id} keeps the storage;
?delete_cloud_storage=true takes it.
A kept storage goes on naming the deleted DB cluster in its DB clusters column, on the Storage tab and each VPC's Cloud Storage tab: when it was deleted, its ID, its backups as last read and its restorable window, and for a restored DB cluster which backup it came from. To restore them, choose Restore from Another installation with the storage's UUID and that ID.
Deleting a VPC or a tenant deletes its DB clusters with it, and that delete's own switch decides for their storages too: left on, as it starts, it deletes them and their backups.
Mail for your own domains. The platform receives the mail of the addresses you add, keeps it in a database of yours, and sends from those addresses, signed with your domain's own key. The Email tab, after VPCs, has your email domains on the left with +Email domain; a domain selected shows its addresses with +Address, and an address selected shows its mail, in Inbox and Sent.
Email domains
+Email domain opens the form: choose one of your domains (Config → Domains). Your first email domain also asks for the VPC and the subnet of the DB cluster the platform makes to keep your mail (Where your mail is kept). Add sets the domain up at Cloudflare, all at once:
- Email Routing, turned on when it was off: Cloudflare adds the domain's MX and SPF records and locks them.
- One rule for the whole domain, its catch-all: every address's mail goes to the platform. Rules the domain already has for single addresses keep working as they did.
- A DKIM key of the domain's own, its record at
<selector>._domainkey.<domain>: everything sent from the domain is signed with it. - A DMARC record,
v=DMARC1; p=none, only when the domain has none.
The domain's card shows its status (Setting up, Active, Failed or Removing), the records the platform added — Catch-all, DKIM, DMARC — and the DB cluster that keeps your mail.
POST /email-domains takes your tenant ID and token in the body, like a pod's create,
and answers the domain active. vpc_id and subnet_id are for your
first email domain only.
curl -k -X POST https://api.YOUR_DOMAIN/email-domains \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "YOUR_TENANT_ID",
"token": "YOUR_TOKEN",
"domain": "example.com",
"vpc_id": "VPC_ID",
"subnet_id": "SUBNET_ID"
}'
Every refusal comes before anything is made, with its code and a sentence:
EMAIL_DOMAIN_NOT_YOURS, EMAIL_DOMAIN_ZONE (not on the Cloudflare account, or
not active on it yet), EMAIL_DOMAIN_EXISTS, EMAIL_DOMAIN_MAIL_ELSEWHERE (its
mail goes to another provider), EMAIL_STORE_PLACE. When Cloudflare refuses a step, what
was set up is taken back and the answer (EMAIL_CLOUDFLARE) says so.
An email domain is the domain itself, not a subdomain, and its DNS is on the platform's Cloudflare account. A domain whose mail goes to another provider cannot be an email domain too. A received message is at most 25 MiB. Mail is read in the portal or through the API: there is no access for mail apps.
Where your mail is kept
In a DB cluster of yours named mail, made with
your first email domain in the VPC and subnet you chose: two instances on two nodes of the VPC, with
backups into a new cloud storage of yours. Each message as it arrived and each attachment are kept in that
DB cluster's cloud storage too, under email/. Your later email domains keep their mail
in the same DB cluster.
Mail is taken once the DB cluster is ready, about a minute after the first email domain is added; until then senders are asked to try again later, as mail servers do. A message a sending server delivers twice is kept once. On its VPC's DB Clusters tab the DB cluster carries an Email badge, and it cannot be deleted while an email domain uses it.
Addresses
An address is the part before the @ — lowercase letters, digits, dots, hyphens and underscores,
starting and ending with a letter or a digit, at most 64 characters — with, optionally, a
name: the name its mail is sent with. It takes mail at once; adding one changes nothing
at Cloudflare. Mail to name+anything@ your domain arrives at name@. Mail to an
address you have not added is refused to its sender: Address does not exist.
An address can be turned off — its senders are told it does not exist, and it sends nothing; its mail is kept — and on again. Removing an address deletes it with all its mail, received and sent.
+Address adds one. Each address shows how many messages it holds and how many are unread; its switch turns it off or on, and removing it asks first.
GET /email-domains,GET /email-addresses?domain_id=...— your email domains, and their addresses with their counts and what each sent today.POST /email-addresses(your tenant ID and token in the body),PATCH /email-addresses/{address_id}withenabledorname, andDELETE /email-addresses/{address_id}.
Inbox, Sent and the reader
An address's mail is in two folders, Inbox and Sent, newest first. Deleting a message deletes it with its attachments.
Each folder lists who sent a message (in Sent, who it went to), the subject, the date, the size and an attachment mark; unread mail is in bold. Opening a message marks it read, and Unread marks it unread again. The tab checks for new mail every half minute, and its refresh button checks at once. Delete asks first.
GET /email-addresses/{address_id}/messages?folder=inbox(orsent) — a page of mail;GET .../messages/{message_id}— one message whole;GET .../messages/{message_id}/attachments/{index}— an attachment, as a download.PATCH .../messages/{message_id}with{"seen": true}orfalse;DELETE .../messages/{message_id}.
The reader shows the message's headers, then the message itself:
- text as text;
- HTML cleaned first — no scripts, forms, frames or event handlers — and shown in a sandboxed frame that runs no script and loads nothing by itself;
- images inside the message, from its own attachments, are shown; images from the internet stay hidden until Show images, and then only over https;
- links open in a new tab, without telling the site where they came from;
- attachments download: the browser never opens one in place.
Writing, replying and forwarding
A message goes to at most 50 recipients, To, Cc and Bcc together, and carries at most 20 attachments and 15 MiB of them; as it goes out it is at most 25 MiB. Bcc recipients get the message and are never shown to the others.
New, Reply, Reply all and Forward open the same form: From (the address, with its name), To, Cc, Bcc, Subject, the message and its attachments. Reply goes to the sender, or to its Reply-To when it has one, and quotes the message; Reply all adds everyone else it went to, never your own address. A reply threads with the message it answers in every mail program. Forward carries the message's attachments, with a switch to leave them out. The portal writes text.
POST /email-addresses/{address_id}/messages sends one; a message can carry an HTML
version as well (html). Read the message again to follow its delivery.
Delivery
Send keeps the message in Sent and queues it. The platform signs it with the domain's DKIM key and delivers it straight to each recipient's mail servers, encrypted whenever the server offers it. In Sent its state reads Queued, Sending or Retrying while it is on its way, then Sent, Partly sent or Not delivered; Not sent means it could not be queued, and it is kept to send again. Opened, it shows each recipient — On its way, Delivered or Not delivered — with the receiving server's answer.
A server that asks to try later — a greylisting, a timeout, a server away — is tried again after 1, 4, 10, 20 and 40 minutes, then after 1, 2 and 4 hours and every 4 hours after that, until 24 hours have passed. A server that refuses the message ends that recipient at once, with the server's words. Nothing is dropped silently: how each recipient's delivery ended is written into the message in Sent. Deleting a message still on its way stops whatever has not gone out yet.
Daily limits
Every tenant sends from the cluster's one internet address, so sending has daily limits that keep that
address in good standing: your tenant sends at most 200 emails a day and each address 100, unless your
administrator set other limits. Each recipient counts as one email, and the day is the UTC day. The form
says how many the address has left today. A send that would pass a limit is refused, with nothing sent
and nothing counted, and the sentence says which limit and when sending opens again, for example:
[email protected] reached its limit of 100 emails today (each recipient counts as one). Your other
addresses can still send. Sending opens again at 00:00 UTC (in 3 h 12 min). Over the API the answer
is 409 with EMAIL_DAILY_LIMIT.
Removing an email domain
Exactly what the platform added at Cloudflare is taken away: the domain's rule (the one it had before is
put back), Email Routing when the platform turned it on, the DKIM key and its record, and the DMARC
record it added. The DB cluster mail stays: delete it on its VPC's DB Clusters tab once you
have no email domain left. A removal that stops part-way leaves the domain Removing,
taking no mail; removing it again carries on.
The × on the domain's card removes it, and asks first.
DELETE /email-domains/{domain_id} removes it. The full reference, with every refusal,
is under Email in the API's own reference (/docs).
Console Access
You can access a terminal inside your running pods using the console feature. Click the green terminal icon (>_) next to any running pod to open a web-based terminal. Console access is only available when the pod is in a Ready state.
The terminal opens an interactive /bin/sh session via a WebSocket connection.
If your image does not include /bin/sh, use a base image that provides a shell.
# Example commands inside the console
whoami
df -h
ls -la /storage
Pod Logs
A pod's logs are its container's output, the latest lines first to read: for debugging an application, following a startup, or looking at what a pod did. A DB cluster's instance shows its database log: PostgreSQL's own lines, each with its time and severity.
Click the logs icon (document icon) next to a pod in the Pods table. The logs panel opens and streams the container's output as it is written; scroll back to read earlier lines.
# The last 100 lines of a pod's log, as plain text
curl -k "https://api.YOUR_DOMAIN/pods/POD_ID/logs?lines=100" -H "X-Auth-Token: YOUR_TOKEN"
lines is how many of the last lines to return (1–5000, default 100). The answer is
the log as plain text, from a ready replica when there is one.
Monitoring
Pod status is refreshed by a monitoring service that polls the cluster and writes status records to the CloudManager database. The dashboard displays per-replica status, IP address, and restart count.
The top of the Monitoring tab stays in view whatever you open below it: a status line (live, when the figures last updated, and an Internet summary of every VPC that opens the Internet sub-tab when clicked), then one strip of tiles, each a large figure against your quota. CPU and memory show what your pods have reserved and a line of the last hour (5-minute averages); storage shows a meter; pods, VPCs and subnets show a dot for each one in its state: green running, amber not all running or stopped, red failed, and an empty ring for a VPC or subnet without pods. The worst state is named beside the figure (1 failed), and pointing at the dots lists them all. A figure at 75% of its quota or more is marked ▲, and at 90% or more ⚠; the line turns amber and red past the same marks. Below the strip are three sub-tabs: Internet (Internet and DNS with its timeline, beside the speed test), Pods (every pod with its live usage) and Trends. The tab reopens on the sub-tab you last chose in that browser.
Live Metrics
The Pods sub-tab lists every pod — when it was created, its VPC and subnet — with its real-time usage, sorted by the figure you choose under Sort by:
- CPU — usage in millicores, beside what the pod reserved, with a bar
- RAM — memory in use, beside what the pod reserved, with a bar
- Storage — the disk the pod uses
- Network — what it receives and sends right now, with the totals under it
Metrics are collected by the resource monitor service running inside the cluster and updated every few seconds in the dashboard.
Internet and DNS
Each VPC has a small monitor that checks, every minute, that the VPC's own DNS answers and that the internet can be reached. The Internet sub-tab shows the last verdict per VPC, one line each: whether the internet is reachable and its round-trip time, whether DNS is resolving, and in the Loss column when the internet was last lost and for how long — or that the loss is still going on. VPCs with a problem are listed first. The line above the table says how many VPCs need attention and how long ago they were checked; hover a verdict for its details — the address probed, the nameserver and name looked up, and when that VPC was checked. A VPC created without internet access shows Not monitored for the internet, and a verdict older than about twenty minutes reads as unknown rather than being repeated. A new VPC's first verdicts reach the page within about fifteen minutes; until then its DNS reads Not checked and its timeline no data.
Internet Over Time
Under the table, Internet over time draws each VPC as two lanes: its internet (the thick lane) and its DNS (the thin lane under it), as the VPC's monitor reported them — up, down, or no data; a VPC without internet access reads internet off. A loss is a stretch of the lane as long as the loss lasted, so an outage shows how long it was, not only that it happened. Choose the window under Range — the last hour, 6, 12 or 24 hours, 7 or 30 days; the choice is remembered in that browser. The line above the chart sums up the window. Hover a stretch, or focus the chart and walk the stretches with the arrow keys, to read when each one began and how long it lasted.
Speed Test
Pick a VPC under From and press GO in the middle of the dial to measure
the cluster's own connection. A short-lived platform pod in that VPC measures the round trip to
speed.cloudflare.com, then downloads from it and uploads to it, each for at least 5 seconds
and at most 10: past 5 seconds it stops once it has moved its budget — 250 MB down, 100 MB up
— and it never moves more than 1 GB down or 500 MB up, in requests of at most 25 MB, so only a
link faster than 1.6 Gbps down or 800 Mbps up is measured over less. The rate is what it moved over
the time it spent moving it, not counting
the round trip each request's answer takes to come back. That is the path your workloads in that VPC
actually get, through the VPC's own resolvers and its internet routing, not your browser's. The dial is
live while it measures: it says
Starting the test… until the pod reports, then names the phase it is in —
Ping, Download, Upload — with the rate measured
so far in Mbps (megabits per second; Kbps below one, Gbps above a thousand), and the needle follows it. When the run
ends the needle settles on the Downlink; press the Downlink or Uplink figure to put that
one on the needle. A run that stops reporting is said to have stopped rather than shown as still live.
Beside the figures:
- Ping — the network round trip to the test server, in milliseconds, as measured on the test's own connection. It leaves out the time the test server spends preparing its answer, so it is close to what a ping from the same network reports.
- Jitter — how much that round trip varies from one request to the next.
A run that did not record its round trip — one from before Ping was measured this way — shows a dash for Ping and Jitter; hover it for the figure it did record and why that is not a round trip. One run at a time in the whole cluster — every VPC reaches the internet through the cluster's one address, so two runs at once would each measure part of it — and three minutes between the end of one run and the start of the next, whichever VPCs ran them, since Cloudflare refuses bursts from one address: until then GO waits on every VPC and says A speed test ran from this cluster a moment ago and until when; the last ten runs of each VPC are shown, drawn as a small chart once there are three and listed in a table, and a run that could not complete is listed with its reason. Administrators can run it on any VPC from the admin dashboard.
Limited. Cloudflare's test server limits the internet address a test comes from once
it has served it enough, and every VPC of the cluster comes from the same one. When it refuses a
request (it answers 429 Too Many Requests), that phase reads Limited — on the
dial, in the figures and in the table, in words, never as a number — and the test stops there.
Until the time the server asked for (15 minutes when it named none), the whole cluster starts no new
run: GO waits on every VPC, and the panel and the refusal say Try again
after that time, in UTC. A phase that could not be measured for another reason — another answer from the server,
a broken connection, or a transfer the server ended before it could be a measurement — reads
Failed, and a phase the test did not reach reads Not run; what it measured before
that is still shown. Over the API the same is in the operations log entry of the run:
latency_state, down_state and up_state in its parameters.
Resource Trends
The Trends sub-tab charts CPU, memory and network traffic (received and sent, in Kbps or Mbps) per VPC over the last hour, 6 hours, 12 hours, 24 hours, 7 days, 30 days or year. The charts are drawn on black; a chart with four or more series draws lines only, so they stay apart. Every line is solid and unbroken: where a sample is missing, the line is drawn straight across it. Hover a chart to read every series at that instant. The scale beside a chart is marked in round steps, each with its exact value; a network chart's reaches 1 Kbps at least, so a nearly idle VPC's traffic runs along its bottom. Only the VPCs that exist are drawn: a deleted VPC leaves the charts with it, in both portals, even while its readings are still kept.
Node Monitoring
The admin dashboard's Nodes sub-tab shows node-level metrics including CPU, RAM, and disk usage per node, and how many tenants' pods each node runs. Node status probes indicate whether the monitoring data is fresh, stale, or offline.
Status Probes
Probes are shown as colored indicators in the Pods and Nodes tables. Click a probe to see detailed status information including phase, IP, reason, and error messages.
| Probe | Meaning |
|---|---|
| 🟢 | Ready / Fresh (healthy) |
| 🟡 | Pending / Not Ready / Stale |
| 🔴 | Failed / Offline |
| 🟥 | Stopped |
| ⭕ | Missing: no longer running, although it was not stopped (what to do) |
| ⚪ | Unknown (no data yet) |
Node probes are based on the last metrics update time: ≤30s = Fresh (green), ≤60s = Stale (yellow), >60s = Offline (red).
Pod Status States
| Status | Meaning |
|---|---|
ready | Pod is running and Ready condition is true |
not_ready | Pod is running but not Ready |
pending | Pod is being scheduled or pulling images |
failed | Pod has failed or is in an Unknown phase |
unknown | Status has not been observed yet |
These are the states of each replica. The pod itself reads Stopped while it is
stopped, and Missing — lifecycle_status "missing"
— when it is no longer running although it was not stopped: Start recreates it, Delete removes it
(A Missing Pod). A pod whose update cannot finish keeps
serving from its previous copy and carries the Update stuck mark
(A Stuck Update).
Auto-Healing & Auto-Scaling
When auto_healing is enabled, the platform manages your pods as a supervised workload, so they are
automatically recreated on failure. Restart counts in the UI help identify unstable workloads.
When auto_scaling is enabled, the platform manages the replica count for you. The dashboard shows
the live replica count as it scales up and down based on CPU usage.
High Availability
For pods with replica_count greater than 1, a VIP service load-balances across replicas.
DNS points to the VIP, so clients are insulated from individual pod restarts.
Alerts
The Alerts tab lists what the platform found wrong, for you to fix or to know about: an attack being tried, a VPC’s internet or DNS down, a pod that keeps restarting, a quota nearly used, something the platform set up that has drifted, something stuck being deleted. You see the alerts about your own VPCs and pods; the administrator sees every tenant’s, and the platform-wide ones besides. The same tab, with the same chips and the same order, is in both portals, and every VPC card has an Alerts sub-tab with that VPC’s own.
One alert per condition
An alert is one condition — this resolver not answering, this source address scanning, this node’s disk filling up — never one line per occurrence. Its last seen is the newest sign of the condition itself: for an attack, the time of the newest request, so a scan at 01:00 still reads 01:00 in the afternoon. Its count is how many of the platform’s checks found the condition happening: a check counts once, and only when it found something newer than the alert already had — the number of requests or failures is in the alert’s detail. A condition seen again never adds a row.
The platform resolves an alert once the condition has not been seen for a while: half an hour for a steady condition (a pod, a disk, a quota), a day for a network condition that comes and goes (a resolver that stops answering for a few seconds once a day stays one open alert instead of blinking open and shut), and a day after the last sign of an attack, so a scan in the night is still there in the morning. If the condition comes back, the same alert opens again. An alert about a condition on a VPC or a tenant that has been deleted is resolved too, since nothing will observe it clear; an attack alert is not — it stays its day, as evidence.
Reading an alert
An alert has a severity — critical, high, medium, low or info — and a kind, the condition in words (“DNS not answering”, “Endpoint scan”), of a family: attacks, the network, workloads, capacity, the platform’s own parts, or drift. The list is ordered for action: open alerts first (unread before acknowledged), then by severity, then the most recent activity.
Each row carries two chips. The severity says how urgent it is, in a pill filled with its level's gradient: crimson for critical, orange for high, amber for medium, blue for low, slate for info. The kind is a quieter tag whose mark is coloured by its family: red for attacks, blue for the network, violet for workloads, amber for capacity, grey for the platform’s own parts, pink for drift. Open the title for the detail, the evidence, where it is (tenant, VPC, subnet, pod, each a link) and who acknowledged it. The Alerts tab’s badge is the number of open alerts.
GET /alerts lists the open alerts (?status=all, resolved or
dismissed for the others), each with its severity, kind and
kind_label, family, the detail and evidence, where it is, and
can_dismiss. GET /alerts/counts answers how many are open, how
many in each status, and how many open ones in each severity.
Acknowledge, dismiss, resolved
- Acknowledge — “I see it, keep tracking it”. The alert stays open and keeps counting. If the platform then resolves it and the same condition returns within a day of your acknowledgement, it reopens as acknowledged rather than as new, so a condition that comes and goes does not keep paging you.
- Dismiss — “not this time”. A dismissed alert stays hidden, and keeps counting, while the condition goes on; if the condition is not seen for a day and then comes back, that is a new occurrence and the alert opens again. An alert the platform raises once for something it did — an internet path it put back, a pod restarted while it took its VPC’s node rule — opens again at the next one, even after a dismissal. A dismissal never silences a source or a name for good. A tenant cannot dismiss an attack alert about its own resources: it is the administrator’s evidence too — acknowledge it instead.
- Resolved — the platform saw the condition clear. Resolved and dismissed alerts stay in the archive.
Ack and Dismiss are on an open alert's row and in its detail; Dismiss asks first, and is not offered where you cannot dismiss. The status filter shows the archive.
POST /alerts/{alert_id}/ack and POST /alerts/{alert_id}/dismiss answer the
alert; a tenant dismissing an attack alert is refused with 409. Both are recorded in the
operations log.
The kinds
| Family | Kind | What it means | Severity | Clears |
|---|---|---|---|---|
| Attacks | New administrator account | An administrator account exists that was not there at the last approved baseline. | critical | when acknowledged into the baseline, or gone |
| Repeated failed sign-ins | Eight or more failed sign-ins for one name within an hour, in the last six hours. | high | a day after the last sign of it | |
| Failed sign-ins for many names | More names than the scanner lists one by one failed to sign in. | critical | a day after the last sign of it | |
| Sign-in burst | Five or more failed sign-ins for one name inside a minute. | high | a day after the last sign of it | |
| Sign-in bursts for many names | More names than the scanner lists one by one had a sign-in burst. | critical | a day after the last sign of it | |
| Platform-wide sign-in failures | Fifteen or more failed sign-ins across every name in five minutes: a spray. | high (critical from 40 failures in five minutes) | a day after the last sign of it | |
| Sign-ins to names that do not exist | Three or more failed sign-ins to a name no account has. | high | a day after the last sign of it | |
| Administrator name guessed | A sign-in tried a name from the usual attacker word list (admin, root...). | critical | a day after the last sign of it | |
| Another tenant's resources probed | A tenant's requests were refused three or more times on another tenant's resources. | high | a day after the last sign of it | |
| Many deletes in a minute | Five or more successful deletes by one actor inside a minute. | high | a day after the last sign of it | |
| Many changes in a minute | Ten or more successful changes by one actor inside a minute. | medium | a day after the last sign of it | |
| Administrator operation tried with a tenant token | A tenant token was used on an administrator-only operation. | high | a day after the last sign of it | |
| Path traversal attempt | A request carried a path-traversal payload (such as ../) in the last day. | critical | a day after the last sign of it | |
| Injection attempt | A request carried an injection payload in the last day. | high | a day after the last sign of it | |
| Injection attempts from many sources | More sources than the scanner lists one by one sent such payloads. | critical | a day after the last sign of it | |
| Endpoint scan | Twelve or more refused requests over five or more paths from one source in an hour. | medium (high from 30 probes in an hour) | a day after the last sign of it | |
| Endpoint scans from many sources | More scanning sources than the scanner lists one by one. | high | a day after the last sign of it | |
| Path traversal request at a portal | A path-traversal request reached one of the portals. One alert per portal and path, whichever replica answered it. | high (critical when the request was answered 200) | a day after the last sign of it | |
| Network | Internet path missing | A VPC's or subnet's internet path is not in the network's own table, and the platform could not put it back on its own yet. | high | half an hour after it was last seen |
| Internet path put back | A VPC's or subnet's internet path had gone missing and the platform put it back. The tenant's copy is low: it is for knowing, not for doing. | medium | a day after the last sign of it | |
| Internet path repair failed | The platform tried to put a missing internet path back and the network still does not show it. | high | as soon as its check finds it in order | |
| Firewall rules put back | The firewall rules on a VPC's subnets no longer matched its security rules, and the platform wrote them again. The tenant's copy is low: it is for knowing, not for doing. | medium | a day after the last sign of it | |
| Internet lost in every VPC | Every VPC that reported lost the internet in the same minutes: the site's own connection was down. The VPCs' own internet alerts are not raised for those minutes. | high | a day after it was last seen | |
| Tunnel down | A tunnel is failed or inactive, so its public address does not reach its service. | high (medium when the tunnel is only inactive) | half an hour after it was last seen | |
| DNS not answering | One of the VPC's resolvers did not answer the name check three times in a row. | high | a day after it was last seen | |
| DNS answer hijacked | A public name resolved to a private address inside the VPC. | critical | a day after it was last seen | |
| DNS replicas disagree | The VPC's two resolvers gave different answers for the same name. | high | a day after it was last seen | |
| Address changed hardware identity | An address on the VPC's network answered from a different hardware identity. | critical | a day after the last sign of it | |
| Unknown neighbour on the network | An address from outside the VPC answered on its network. | high | a day after the last sign of it | |
| Platform service unreachable | One of the VPC's own platform services did not answer three times in a row. | high | a day after it was last seen | |
| Internet unreachable | The VPC's monitor could not reach the internet three checks in a row. | medium | a day after it was last seen | |
| DNS upstreams unreachable | None of the public resolvers the VPC's resolvers forward to answered, three checks in a row. Names already in use keep answering from the resolvers' memory. | medium | a day after it was last seen | |
| Internet reachable where it should not be | A VPC created without internet access can reach the internet. | medium | a day after it was last seen | |
| Monitor error | The VPC's monitor hit an error in one of its checks. | medium | a day after the last sign of it | |
| Monitor flooding | The VPC's monitor reported more distinct findings in one pass than it can honestly. | high | a day after the last sign of it | |
| Monitor silent | The VPC's monitor has not reported in; nothing is watching that VPC's network. | medium | half an hour after it was last seen | |
| Monitor missing | The VPC's monitor is gone although the platform expects it. | high | half an hour after it was last seen | |
| Workloads | Pod unhealthy | A pod is not ready, keeps restarting, cannot pull its image, or cannot be created at all (over the quota, for example). | medium (high for a crash loop, an out-of-memory kill or a failed pod) | half an hour after it was last seen |
| Pod missing | A pod is no longer running, although it was not stopped. Start recreates it from its record; Delete removes it. | high | as soon as the pod runs again or is deleted | |
| Pod update stuck | A pod's update cannot finish while its previous copy still serves: the new copy cannot download its image, cannot be created or placed on a node, keeps stopping, or did not become ready in time. The alert says why and what to do. | medium | as soon as the update completes or is undone | |
| Pod stuck deleting | A pod has been deleting for more than an hour; its name and quota slot are held. | medium | half an hour after it was last seen | |
| Pods outside the VPC's nodes | Pods of a VPC run on nodes that are not the VPC's. The platform does not move them by itself: Migrate internal with the same nodes moves only them. | medium | half an hour after it was last seen | |
| DB instance unreachable | A PostgreSQL instance of a DB cluster runs without its address in the VPC, so applications cannot reach it. The platform recreates it on the same volume. | high | as soon as the instance has its address again, or is gone | |
| DB cluster backups failing | A DB cluster's write-ahead log has not reached its cloud storage for several minutes: its backups stop at that moment, and a restore cannot reach past it. | high | as soon as the write-ahead log reaches the cloud storage again | |
| Capacity | Node disk filling up | A node's disk is 80% full or more. | high (critical from 90%) | half an hour after it was last seen |
| Quota nearly used | A tenant is at 80% or more of a quota. | low (medium from 90%, high at 100%) | half an hour after it was last seen | |
| Platform | Node readings stale | No fresh usage readings for a node in five minutes: the figures shown for it are old. | medium (high after an hour, critical after a day) | half an hour after it was last seen |
| NAT rule stuck deleting | A NAT rule has been deleting for longer than the platform allows. The tenant's copy is low: it is for knowing, not for doing. | high | when the object is gone | |
| Address stuck deleting | An external address has been deleting for longer than the platform allows. The tenant's copy is informational. | medium | when the object is gone | |
| Address left in place | An external address could not be freed when its NAT rule went, because a router still carried it or could not be read, and has waited longer than the platform allows. The platform frees it on its own once nothing carries it. | medium | when the object is gone | |
| Licence notice | The cluster's licence needs attention: more servers or clusters than the plan allows, no confirmation from Circloud for 36 hours, a licence about to run out, or none at all. Running apps are never affected. | medium (high while changes are paused) | as soon as its check finds it in order | |
| Cloud storage not served | Pods of a VPC use cloud storage, but no storage service is on record for that VPC, or the one on record does not answer: their storage calls fail. | high | half an hour after it was last seen | |
| Cloud storage on one proxy | A VPC's cloud storage answers through one proxy only, because another has not answered for ten minutes (its node is down, it waits for a node, or it keeps restarting). If the remaining one or its node fails too, the VPC's storage calls fail until a proxy answers again. | medium | half an hour after it was last seen | |
| Cloud storage CA missing | The certificate authority the platform made for cloud storage is gone while a storage service still uses a certificate it issued. It is never made again: until it is put back, a pod with cloud storage that is started, made again or changed starts without its storage settings, and creating one is refused. | high | half an hour after it was last seen | |
| VPC placement needs attention | New pods of a VPC cannot be kept on its chosen nodes as they should: the platform lacks a permission on the cluster's nodes or could not set them, none of the VPC's nodes takes new pods, or a pod is held while it takes its VPC's node rule. A pod restarted while it took that rule keeps the alert open until it is dismissed; until then no other pod takes the rule in place. | high | as soon as its check finds it in order | |
| Router tables unreadable | The platform cannot read the network's own table of internet paths, so it neither checks nor repairs them. Running apps are not affected by this alone. | medium | as soon as its check finds it in order | |
| Network update halted | A network change the platform rolls out one VPC at a time failed its checks on a VPC, was undone there, and stopped. Acknowledging the alert lets it try that VPC once more. | high | as soon as its check finds it in order | |
| Network update waiting on a VPC | A network change has skipped a VPC for a day because the VPC was not healthy, was being moved, or was running a speed test. | low | as soon as its check finds it in order | |
| Network updates paused | The platform's network changes that roll out one VPC at a time have been paused for an hour or more by an update of the platform or by the administrator. Repairs still run. It clears when the pause ends. | low | as soon as its check finds it in order | |
| Firewall update halted | A new way of writing firewall rules failed its health check on the first VPC; that VPC's previous rules were put back and no other VPC was changed. | critical | as soon as its check finds it in order | |
| Platform repairs paused | The platform's self-repairing checks have not run for ten minutes, because an update of the platform is still in progress or cannot be read. | high | as soon as its check finds it in order | |
| Platform update refused or rolled back | An update of the platform was refused before it changed anything, or was undone because tenant traffic got worse during it. | high | as soon as its check finds it in order | |
| Platform update not verified | An update of the platform stopped before its last steps because the site's own internet was down, so tenant traffic could not be checked; nothing was undone. It clears on the next update that passes its checks. | medium | as soon as its check finds it in order | |
| Cloudflare tokens not deleted | The platform could not delete some of its Cloudflare API tokens. The alert names them and their account: delete them in the Cloudflare dashboard, then dismiss it. | low | when the platform deletes them after all | |
| Cloudflare renewal failed | This version of the platform needs its Cloudflare access renewed, and the renewal failed. The alert says why and when it runs again; until then, the features that need it do not work. | medium | as soon as a renewal succeeds | |
| Cloudflare token on your profile not working | The Cloudflare token on your Circloud profile for this cluster's account does not work for this cluster. The alert says why: replace the token on your profile, or save a working one on the Cloudflare card. | medium | as soon as a working token is saved, on your profile or on the Cloudflare card | |
| Tunnel name taken on Cloudflare | On the Cloudflare account, a tunnel this cluster did not create carries the name the platform gives one of its tenants' addresses. It is never used: a new address is not published, and an existing one keeps the name it had and keeps working. The alert names the tunnel: if nothing uses it, delete it in the Cloudflare dashboard. | medium | when no tunnel the cluster did not create carries the name | |
| Address on a shared tunnel | One Cloudflare tunnel carries two addresses: two of this cluster's, or one of this cluster's and one of another cluster on the same account. Only one of them is served, and its visitors can reach the other's connector. The alert names both: delete the address that should not use the tunnel and publish it again; it gets a tunnel of its own, and the other address is left as it is. | high | when the tunnel carries one address | |
| Drift | Unexpected namespace | A namespace the platform did not create and the baseline does not know. | medium | when acknowledged into the baseline, or gone |
| Unexpected service account | A service account the platform did not create and the baseline does not know. | medium | when acknowledged into the baseline, or gone | |
| Unexpected cluster role | A cluster role that was not there at the last approved baseline. | high | when acknowledged into the baseline, or gone | |
| Unexpected cluster role binding | A cluster role binding that was not there at the last approved baseline. | high | when acknowledged into the baseline, or gone | |
| Tenant not created by the platform | A tenant exists with no record of the platform creating it. | low | when acknowledged into the baseline, or gone | |
| Unexpected platform secret | A secret in the platform's own namespace that no record of the platform accounts for. | high | when acknowledged into the baseline, or gone |
Three checks of the per-VPC monitor are no longer alerts, because what they measured was never a problem: the router reachability check, which no router answers (it failed on every VPC every minute); the internal name check, which asked the VPC’s resolver for a name it is built not to know; and the large-packet check, whose answer was never large. The monitor asks for one public name now, and calls the internet unreachable only after three failed checks in a row. Alerts of retired kinds, and alerts that were once raised one per hour or per replica, are resolved by the platform and stay readable in the archive.
Operations Log
Every mutating operation on the platform is recorded in the Ops Log — creating, deleting,
starting, stopping, restarting pods, VPCs, subnets, tunnels, and configuration changes.
The log tracks the source (GUI, API, or System), actor (admin or tenant), status (success or failed),
and error details for failed operations. A VPC's migration and each of its steps read
Migrate internal or Migrate external, and a step of a Migrate external
names the step too (Migrate external: Verify); hover one for the operation id the API
filters on (vpc_placement, vpc_placement_move).
The Ops Log is available at three levels:
- Global — all operations across all VPCs (main Ops Log tab)
- VPC-level — operations within a specific VPC (Ops Log tab inside each VPC)
- Tenant-level — all operations for a tenant (admin dashboard only)
Use the filter dropdowns (Source, Status, Resource Type, Operation) to narrow results. Filters are applied server-side for fast queries even with thousands of entries. Click Load More to fetch the next page of results.
Issues
The Issues tab is where you tell your administrator that something is wrong. Press +Ticket, say what you expected, what happened instead and when it started, and press Report to admin. You do not have to gather anything yourself, and you do not have to describe the problem in technical terms: the diagnostics of what the ticket is about go with it.
Writing a ticket
+Ticket opens the ticket card. Pick what the ticket is about, in three optional steps; each step narrows the next, and decides how much is collected:
- VPC — or leave it on None for your whole account
- Subnet — only once a VPC is chosen; None covers the whole VPC
- Pod — only once a subnet is chosen; None covers the whole subnet
Then write what is wrong and press Report to admin, at the card’s bottom right. Your tickets are listed under +Ticket, so you can see that yours arrived.
Report to Circloud
Your administrator reads the tickets in the admin portal’s Issues tab; a click on a ticket opens it with everything that was collected alongside it. Each ticket’s row has a Report to Circloud button, which sends it on to Circloud for help, and a Circloud column with what Circloud answered: Sent with Circloud’s reference, the one to quote when writing to support, or the reason it did not take the ticket.
Tickets are written in the user portal: the API has no route that writes one. Reporting a ticket
to Circloud is an administrator’s route; REPORT_ID is the ticket’s id,
the one the admin portal opens it at (?report=REPORT_ID), and $API_URL
your cluster’s API address:
curl -k -X POST "$API_URL/reports/REPORT_ID/send" \
-H "X-Admin-Token: YOUR_ADMIN_TOKEN"
# {"outcome": "sent", "reference": "1d272297-5c1e-4b7a-9f3e-0a6b2c8d4e1f",
# "updated": false, "detail": "Sent to Circloud.", "sent_at": "2026-10-29T10:05:00Z"}
reference— Circloud’s reference of the ticket; reported again,updatedistrueand the reference stays the same409 CLUSTER_IDENTITY_MISSING— the cluster does not have its licence key yet; the Circloud installer hands it over when it installs or upgrades the cluster502 CIRCLOUD_REFUSED— Circloud did not take it, and says why inmessage;502 CIRCLOUD_UNREACHABLE— it could not be reached or was busy: try again later. Either way the outcome is kept on the ticket
The API’s own reference (/docs, Reports) has every answer.
What is collected with a ticket
- What you picked — its settings and current state, so the ticket still says what it was about even after the pod is gone
- Pod state — ready or not, restart counts, and the reason a container last stopped
- Recent usage — CPU, memory and network for the last few hours, because a problem that has already passed is invisible in a live reading
- Your recent operations — the Ops Log entries for that scope, which is usually where “it worked yesterday” is answered
- Your open alerts — in case the platform already noticed
- The pod’s last log lines — only when you pick a pod, and only that pod’s own output
- Whether the platform was watching — how many nodes were reporting and how fresh their readings were, so a stale reading is not mistaken for an idle pod. Counts only: no node names and no addresses
Only your own VPCs, subnets and pods can be chosen or collected, and the platform’s own parts of a VPC are never included. Your API token, pod environment variables and registry credentials are never collected. Everything gathered is size-capped, and the ticket records anything that had to be trimmed.
What goes to Circloud
With Report to Circloud, the ticket goes as you sent it, with the cluster’s health at that moment — its nodes and VPCs, the internet and name-service checks, speed-test results, the state of every pod and of the platform’s own services, the tunnels, open alerts, the operations log around the time of the ticket, and the platform’s versions — together with the cluster’s licence, so Circloud knows whose cluster it is. The same rule holds as for the ticket itself: no API token, no environment variables, no tunnel tokens and no registry credentials ever go. Reporting it again later updates the copy Circloud has, under the same reference.
Cloudflare Tunnels
Tunnels provide secure external access to your pods without opening inbound ports. Traffic is routed through Cloudflare's global network, providing DDoS protection, automatic SSL/TLS, and zero-trust access.
When you create a tunnel, Circloud deploys a cloudflared connector pod in the VPC, allocates outbound access, and registers DNS records in Cloudflare. The tunnel origin targets the pod DNS name or VIP service.
Tunnel status is tracked as active, inactive, or failed in the dashboard.
Creating a Tunnel
Navigate to the Tunnels tab and click "Create Tunnel". Select a pod and configure the tunnel hostname and port settings.
Using Custom Domains
If custom domains have been assigned, you can use them:
curl -k -X POST https://api.YOUR_DOMAIN/pods/POD_ID/tunnel \
-H "X-Auth-Token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tunnel_name": "web-app",
"subdomain": "app",
"protocol_type": "http",
"service_port": 80
}'
# Access at: https://app.yourdomain.com
# Supported protocols: http, https, ssh, tcp, udp
tunnel_name and subdomain follow the name rule.
Custom Domain via API
curl -k -X POST https://api.YOUR_DOMAIN/pods/POD_ID/tunnel \
-H "X-Auth-Token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tunnel_name": "custom-tunnel",
"subdomain": "portal",
"protocol_type": "http",
"service_port": 80,
"domain": "example.com"
}'
# Access at: https://portal.example.com
Tunnels in Topology JSON
You can also define tunnels inline when deploying pods via the Topology API:
{
"pods": [
{
"name": "web-server",
"vpc_name": "my-vpc",
"subnet_name": "my-subnet",
"container_image": "nginx:latest",
"service_port": 80,
"tunnels": [
{
"subdomain": "www",
"protocol_type": "http",
"service_port": 80
},
{
"subdomain": "ssh",
"protocol_type": "ssh",
"service_port": 22
}
]
}
]
}
- HTTP: Websites, REST APIs, webhooks, microservices, admin panels
- HTTPS: Services with internal SSL, legacy apps, mTLS requirements
- SSH: Remote administration, SFTP file transfers, Git over SSH, dev access
- TCP: Databases (PostgreSQL, MySQL, MongoDB, Redis), RDP, custom protocols
- UDP: Game servers (Source, Minecraft), VoIP (SIP), DNS, QUIC, media streaming
Managing Tunnels
The Tunnels tab on your dashboard lists your tunnels. Click the delete button (×) on a tunnel row to remove it.
# List your tunnels
curl -k https://api.YOUR_DOMAIN/tunnels \
-H "X-Auth-Token: YOUR_TOKEN"
# Delete a tunnel
curl -k -X DELETE https://api.YOUR_DOMAIN/tunnels/TUNNEL_ID \
-H "X-Auth-Token: YOUR_TOKEN"
Deleting a Tunnel
Deleting a tunnel cleans up its Cloudflare tunnel, DNS record, and access policy automatically. The cloudflared pod running the tunnel is also removed from the cluster.
A tunnel reaches the internet through its VPC's own internet path, so creating or deleting one never touches that path: the VPC keeps its DNS and internet whatever happens to its tunnels. Before a tunnel is created or started, the platform confirms the VPC's internet path; if it cannot, the request changes nothing and says so, and the administrator is alerted. Tunnels made before this each had a NAT rule of their own; the platform tidies those away one VPC at a time, checking the VPC's internet after every step, and each step is in your operations log. A NAT rule or an address that stays stuck being deleted is on the Alerts tab instead of going unnoticed; an alert raised earlier for one names the tunnel and what it means for your VPC, for example Tunnel web: NAT rule left behind or Tunnel web: NAT rule stuck deleting. There is nothing for you to do — the administrator gets an alert of their own — and both alerts clear themselves once it is gone. A NAT rule whose VPC is already gone can never finish deleting on its own, so the platform releases it itself, without an alert. Dismissing yours hides only yours.
Custom Domains
A tunnel needs a domain of your own. Your domains are yours alone — the platform's default
domain carries the platform's portals and is never used for your addresses. There are two ways to
get one: request it from the Domains tab (Config → Domains) and the administrator
approves it, or the administrator adds it to your tenant. The first domain you get becomes your
default: new tunnels go on it unless you pick another of your domains in the tunnel
form. Until you have one, the tunnel forms say so and Create Tunnel is disabled: Request a domain
first (Config → Domains), or ask your administrator to add one. The API answers the same
sentence (409, DOMAIN_NOT_CONFIGURED).
Domain Workflow
- Request — the tenant submits a domain request from the Domains tab in the User GUI
- Pending — the request appears in the admin dashboard awaiting approval
- Approval — the admin approves the domain from the CloudManager Tenants section
- Active — the tenant can now create tunnels using the approved domain; the first one approved is the default
A domain is assigned to one tenant only. The administrator approves a request only when the domain is on the platform's Cloudflare account, active there (a domain still pending at Cloudflare waits until its nameservers are switched), and no other tenant holds it, and may reject a request. A rejected request reads Rejected, with the administrator's reason under the domain's name when one was given. Remove it with ×, or request the domain again: the new request replaces the rejected one.
Set default on another of your domains makes it the default for new tunnels and moves the addresses on the previous default domain to it; addresses on your other domains stay where they are. Adding a later domain changes nothing until you set it.
To delete a domain, click the × next to it. Deleting your default domain makes another of your domains the default — the one most of your addresses sit on, else your oldest — so you always have one while you have any; no address moves. Addresses already on the deleted domain keep working until they are deleted. Deleting your only domain leaves you with none: Create Tunnel is disabled until you have another, and the next domain you get becomes your default.
Topology (Infrastructure-as-Code)
The Topology feature allows you to define your entire infrastructure in JSON and deploy it with a single API call. This is ideal for version control, reproducible deployments, and automation.
The Topology tab displays a visual diagram of your infrastructure and lets you export or import your entire setup as JSON.
Exporting Your Topology
From the Topology tab, you can view your current infrastructure as JSON and download it for backup or version control.
Deploying a Topology
curl -k -X POST https://api.YOUR_DOMAIN/tenants/TENANT_ID/topology \
-H "Content-Type: application/json" \
-d '{
"token": "YOUR_TOKEN",
"vpcs": [
{
"name": "production",
"enable_internet": true
}
],
"subnets": [
{
"name": "web",
"vpc_name": "production",
"cidr": "10.10.1.0/24",
"gateway_ip": "10.10.1.1"
},
{
"name": "api",
"vpc_name": "production",
"cidr": "10.10.2.0/24",
"gateway_ip": "10.10.2.1"
}
],
"pods": [
{
"name": "nginx",
"vpc_name": "production",
"subnet_name": "web",
"container_image": "nginx:latest",
"replica_count": 2,
"service_port": 80,
"external_inbound": true,
"auto_scaling": true,
"autoscale_max_replicas": 6,
"autoscale_cpu_target_millicores": 250
}
]
}'
Every name, tunnel_name and subdomain in the document follows the name rule; a document with a name outside it is refused whole, and the answer names the field by its place in the document.
A deployment that fails part way takes back what it built, and the answer says which step failed. A VPC it could not remove stays in your VPC list marked Failed: delete it; delete it like any other VPC.
CloudManager (Admin Portal)
The CloudManager GUI is the admin control plane for the entire cluster. It provides global configuration, tenant lifecycle management, monitoring, and system visibility across every tenant.
Access the Admin GUI at its direct address on the control node, https://<control
node>:8443, or publicly at the admin
portal's address: https://admin-<cluster>.<default domain>
(https://admin.<default domain> on a cluster installed before addresses carried its
name), or the subdomain you gave it on the Domains card.
The Config tab holds the platform's settings as cards, in this order: Admin Authentication, Licence and Tenant Defaults; Cloudflare, Domains and Service Tunnels; then the Version. Each card is as wide as its content, and a row holds as many cards as the screen allows; a card's Save is always its last line, at its bottom right, in every card of both portals. On a narrow screen each row of the Domains and Service Tunnels tables is a block of labelled lines, and on a phone the cards are one column. The tenant portal's Config page lays its cards out the same way. The provider's network settings are not a card: they are the Provider strip under the page's header, on every tab.
Admin Authentication Card
Displays the admin username and the admin API token used for privileged endpoints. Use this token
as the X-Admin-Token header when creating tenants or managing global settings. Once the
API is published (the Domains card), the card shows its address, the
API URL, too. At the card's foot:
- Download credentials — the administrator's
credentials.jsonfor a script or an AI agent that manages the platform: JSON withapi_urlandadmin_token(the tenant's file carriesapi_url,tenant_idandtoken). Keep it out of version control — the admin token reaches every tenant. Until the API is published there is no address to put in it, and the card says so. - Change password — the same form and rules as a tenant's
(Change Password), for the password you sign in to
this portal with. The installer's copy of the first password, in its
credentials.json, no longer signs in after a change.
Click Change password at the card's foot: your current password, the new one, and the new one again, then Save.
curl -k -X POST https://api.YOUR_DOMAIN/admin/change-password \
-H "X-Admin-Token: YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"current_password": "<current password>", "new_password": "<new password>"}'
It answers 204, with no body: the old password stops working at once.
Cloudflare Card
Shows the Cloudflare account the platform uses and the portals' addresses:
admin-<cluster>.<default domain>, user-<cluster>.<default
domain> and api-<cluster>.<default domain> (on a cluster installed before
addresses carried its name, admin.<default domain> and so on), then the container
registry's and the email service's, or not published; while you change the Domains card they
follow as a preview, and after a save they show what was saved. A cluster installed with an account from
your Circloud profile needs
nothing entered here once you approve it; until then the card says Waiting for your approval on your
Circloud profile. To change its token, replace the token on your profile.
On a cluster installed without an account from the profile, enter the Account ID and the token here (Cloudflare Setup) and save. The token is not shown again once saved. The domains are on the Domains card below, where the default is chosen. Moving the platform to another Cloudflare account is confirmed first, and every domain in use has to be on the new account too; when the new account does not have the default domain, the card lists its domains and asks which one becomes the default. While cloud storage uses the account, a token for another account is refused.
Domains Card
Every domain on the Cloudflare account, as last fetched from Cloudflare — the page opens without asking Cloudflare. The platform fetches the list by itself every 5 minutes; click Refresh to fetch it at once, for example right after adding a domain to the account. When a fetch fails, the line above the table says when and why, and the list stays as the last good fetch left it. A domain that disappears from the account is marked Missing rather than dropped while a tenant or a tunnel still uses it; hover a domain's name to see what uses it.
- Tenant — the tenant the domain is assigned to; a dash when no tenant holds it. A domain is assigned to one tenant only. On a cluster installed before this rule, a domain may still be assigned to more than one tenant: the card names them in the warning colour, and nothing is taken from any of them until you remove the domain from all but one (Tenants, Config → Domains). The platform's own addresses are not a tenant's: the default domain, and the portals and API published on it, may be a domain a tenant holds; an address already in use is refused, whichever side asks for it.
- Default — the platform's default domain: where the platform's own addresses are published. It is the platform's alone. Tenants' domains are independent of it: setting or changing the default touches no tenant's domains and no tenant address. A tenant gets its domains by requesting them (you approve) or from its card, Config → Domains, and its first one becomes its default; a tenant with no domain cannot publish an address, and tunnels published on the platform's domain before this keep their address. Only a domain that is active on the account can become the default.
- Admin portal, User portal, API — the platform's own three portal addresses,
published on the default domain. Switch a service on to publish it and set its subdomain
(
admin-<cluster>,user-<cluster>andapi-<cluster>unless you change them, where <cluster> is the cluster's label;admin,userandapion a cluster installed before addresses carried its name). A box left empty stands for that default, which it shows as its placeholder. Every control is greyed out except on the default's row, and a subdomain box stays greyed out until its service is switched on. - Container registry — the platform registry tenants push their images to
(
registry-<cluster>unless you change it,registryon an older cluster; see The Platform Registry). It is published only with the API, which checks its logins. On a cluster installed before it, it shows switched on beside a published API, with a ! until the next Save publishes it. - Email — the email service, where every message of the tenants'
email domains reaches the platform
(
email-<cluster>unless you change it,emailon an older cluster). The platform publishes it by itself with the cluster's first email domain, unless you switched it off on this card. While an email domain exists, switching it off is refused: Email cannot be unpublished: the mail of … arrives through it. Those email domains are removed first, by their tenants.
Nothing changes until you click Save. A save publishes what is switched on and not published yet, removes what is switched off, and moves what changed address — a new subdomain, or a new default domain. A move or a removal is confirmed first, in a dialog that says exactly which addresses change. While the admin portal's own address moves, the page is locked and shows each portal moving in turn (the admin portal last, because it serves the page), then sends you to the new admin address once it answers. A ! beside a service means an earlier save did not complete for it; hover it for what is left, and save again to finish.
The card publishes one address for each portal. A second address for the admin portal is added over the API (the API tab); it then shows on the Service Tunnels card, below, with a remove button (×).
Copy the admin portal's Origin URL from the Service Tunnels card, below, then publish
a second address on the default domain, here ops:
curl -k -X POST https://api.YOUR_DOMAIN/service-tunnels \
-H "X-Admin-Token: YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"subdomain": "ops", "protocol_type": "https", "origin_url": "ORIGIN_URL"}'
See Platform Domains and Tunnels in the API reference.
Service Tunnels Card
Under the Domains card: the tunnels the platform's own portals are published through, the container registry's and the email service's among them (the tenants' tunnels are on the Tunnels tab, below). Each row shows the service, the tunnel name, the protocol, the public hostname and a status probe. A tunnel published by a save appears straight away with an orange Starting probe, which turns green Active once its connector answers; click a probe for its detail. A probe reads Unknown when the monitor has not reported the connector for a couple of minutes. An older tunnel that shares a connector with the one a service is published through now reads Not managed, and saves leave it alone.
The Domains card's own tunnels are removed by switching them off there. Every other row — an older tunnel, a second address added through the API — has a remove button (×) at the end of its public hostname. Removing one never stops a connector another tunnel still uses, never deletes a Cloudflare tunnel that is not this platform's alone or that sits on a domain it does not use (only the row goes then), and never takes the admin portal's last working address.
Provider Strip
Under the page's header, on every tab, one compact line shows the provider's networking settings: its name, the external subnet CIDR, its gateway, the management network and the excluded IP ranges. These values define how external access is allocated for pods. On a narrow screen the line wraps.
Tenant Defaults Card
Sets default resource limits for new tenants (pods, storage, VPCs, subnets, CPU, RAM). These defaults are applied when a tenant is created without explicit resource caps. Click Save to update.
Version Card: Upgrading the Platform
Beside the Licence card, the Version card shows the platform version the cluster runs, and upgrades it from this page: no installer, and no sign-in to a node. Circloud names its newest release at each of the cluster's check-ins, about every 15 minutes; the Licence card's Check now asks within a minute.
What the card shows:
- Up to date — <version> · Up to date, and when the cluster last checked with Circloud.
- A newer release — <version> · <newer> is available, and Upgrade. It asks once, Upgrade to <newer>: the portals restart during the upgrade, which takes 10 to 20 minutes, and tenants' workloads keep running.
- Upgrading — Upgrading to <newer> · started 14:02 UTC by the administrator who asked, the step in flight under it, and six steps, each Done, Running or Waiting: Download, Registry, Images, Services, Finish and Checks. The portals restart halfway: the page keeps asking, and shows the progress again once they are back.
- Done — Upgraded to <newer> at 14:19 UTC, with how many of the platform's checks passed, and Reload while the page itself is still the old version.
- Stopped — a warning with the reason and its code, followed by Every step is safe to run again. Upgrade is offered again. An upgrade that stops reporting for 10 minutes, because the control node restarted for example, is shown as stopped: The upgrade stopped reporting at 14:10 UTC.
Nothing in the API starts an upgrade: upgrade from the Version card or with the installer. The API answers the version the platform runs:
curl -k https://api.YOUR_DOMAIN/version
The upgrade runs on the control node by itself — the same upgrade the installer runs — so neither the portals' restart nor a closed page stops it. One operation changes the platform at a time: an upgrade, from either side, the installer's Extend or a node's removal. One started while another runs stops at once, nothing changed, with Another platform operation is running on this cluster (E506): upgrade again once the other has ended. A finished upgrade checks in with Circloud at once, so your profile shows the new version within a minute.
The other way: the installer. On the cluster's card in the installer, click Upgrade: it checks for the newest release and runs the same upgrade over SSH. The card reads the version from the cluster when it connects, so an upgrade made from the admin portal shows there too.
Monitoring Tab
Cluster overview for tenants, VPCs, pods, and node utilization. A status line stays in view on every sub-tab: live, last update, the monitor's state and the node it runs on, and an Internet summary of every VPC that opens the Internet sub-tab. Three sub-tabs sit below it:
- Nodes — in two columns. First the cluster's tiles, each a large figure — CPU and memory in use against capacity, each with what pods have reserved against what the nodes can allocate and a line of the last hour; storage; network traffic received and sent by all nodes with its last 60 seconds; and the number of tenants, VPCs and pods, each with a dot for each one in its state and the worst of them named — beside per-node metrics with how many tenants' pods each node runs. Under them, the highest-usage pods with the node each runs on, to find hotspots, beside each tenant's quota with what its pods have reserved and what they actually use. On a narrower screen the four stack in that order.
- Internet — the Internet and DNS verdict of every VPC on the cluster, each named with its tenant, and every VPC's internet and DNS over time, beside the in-cluster speed test (runnable on any tenant's VPC).
- Trends — resource trends per node and per VPC, from the last hour up to a year, on the same black charts as the tenant's.
The board shows the tenants' workloads, not the services the platform runs on: a VPC's cloud storage service is listed with the VPC's own services, on its Core Services tab.
The tab reopens on the sub-tab last chosen in that browser.
Nodes Sub-tab
On the Monitoring tab, and the one it opens on. The cluster's tiles beside the health and utilization of every node, including CPU, RAM, storage, GPU metrics, and network traffic; then the highest-usage pods beside each tenant's quota. Use this to confirm node capacity, detect failures and see who is near a limit.
- Pods (nodes table) — how many tenants' pods the node runs. Every running replica counts (one still starting, failed or finished does not); the platform's own services are not counted.
- Node (top pods table) — the node the pod runs on; a pod with replicas on several nodes lists each of them.
Tenants Tab
Create, search, and delete tenants. The Allow tenant self-signup toggle controls whether the CloudUser login page shows the Create Account option.
- Create Tenant: Provide name + password, optionally set resource caps.
- Delete Tenant: Click the × on a tenant card to remove the tenant and all resources — its cloud storages and its whole off-site space too, unless the dialog's switch is off (see the Storage tab, below).
- Resources: Set per-tenant limits (pods, storage, VPCs, subnets, CPU, RAM), and the two daily email limits (below).
- Domains: Add domains to a tenant and approve or reject its requests; its first domain becomes its default for new tunnels, and deleting its default makes another of its domains the default (the one most of its addresses sit on, else its oldest). A tenant with none cannot publish an address: its card's tunnel form says so and opens the moment you add or approve its first domain. A domain is assigned to one tenant only: adding a domain another tenant holds is refused, naming that tenant. Approve is available only when the requested domain is on the Cloudflare account, active there, and no other tenant holds it — otherwise it is greyed out, and the reason shows under the domain. Adding a domain follows the same rule, with the same sentence. A domain still pending at Cloudflare (its nameservers not switched yet) waits until it is active: the domain list refreshes every 5 minutes and shows when it is. Reject asks for confirmation and an optional reason, which the tenant sees beside the rejected request.
- Registry: Manage tenant registry credentials.
- Topology: View or apply tenant topology JSON.
- VPCs / Pods: Admin view of tenant networks and workloads, including pod actions, and each VPC's Migrate internal and Migrate external (below).
- Email: the tenant's email, read-only (below).
A Tenant's DB Clusters
A tenant's VPC card reads Pods · DB Clusters · Cloud Storage here too. The DB Clusters tab lists the VPC's DB clusters as the tenant sees them, read-only: no create form, no operation, and never a password — the connection shows the addresses, the database and the username. Details shows its settings, instances, operation, backups and operations log, and its open Alerts:
- DB instance unreachable — an instance runs without its address in the VPC, so applications cannot reach it. The platform recreates it on the same volume.
- DB cluster backups failing — its write-ahead log has not reached its cloud storage for several minutes: its backups stop at that moment, and a restore cannot reach past it.
Both are on the Alerts tab too, and the tenant has its own copy. The changes are the tenant's to make, from its portal or the API.
A Tenant's Email
A tenant's card has an Email tab, read-only: the tenant's email domains, each with its status and the records the platform added at Cloudflare (Catch-all, DKIM, DMARC), their addresses, on or off, and what each address sent and received today. Never what the mail says. The line above the table gives the day's total against the tenant's limit, and the DB cluster that keeps its mail.
Every tenant sends from the cluster's one internet address, which the receiving mail servers judge for all of them, so sending has two daily limits: the tenant's (200 emails a UTC day) and each of its addresses' (100), each recipient counting as one. An address's limit is never more than its tenant's.
Set them under Resources → Set resources: Emails a day and Emails a day per address, empty for the defaults. The Resources cards show Emails today against the tenant's limit.
curl -k -X PATCH https://api.YOUR_DOMAIN/tenants/TENANT_ID/resources \
-H "X-Admin-Token: YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"max_daily_emails": 200, "max_daily_emails_per_address": 100}'
Only the limits sent change.
Migrate Internal: Other Nodes of This Cluster
Every VPC on a tenant's VPC list has two buttons: Migrate internal, to other nodes of this cluster, and Migrate external, to another cluster (the tenant has the second one on its own VPC).
Migrate internal opens its card in a pop-up, with the cluster's nodes as switches, on the Nodes: line. The ones switched on are the nodes chosen for the VPC the last time it was migrated — or, for a VPC never migrated, the nodes it runs on now. A node pods cannot be moved to (not ready, not taking new pods, or taking no tenant pods) is greyed out with the reason beside it, and the control node is labelled as such. Switch on the nodes the VPC should run on and switch off the ones it should leave: the card shows What would move — only the tenant's pods that have to move, each with its steps, where it is now and the nodes it goes to — or, in one sentence, why it cannot. Each node in the table has a bar for its CPU and one for its RAM, showing what the node's pods reserve of what it can allocate (what decides whether a pod fits on it), with a mark inside for what is in use, and a bar for its disk in use of its size; a line under the table says so, and hovering a node gives the figures. A pod already on one of the chosen nodes is not touched, not even restarted. With the nodes recorded for the VPC, the table is empty, says so, and Start stays greyed out; for a VPC whose nodes were never chosen, Start records them: nothing moves, the table says that from then on the VPC stays on those nodes, and a warning above it says what that means — its new pods start only on those nodes, and wait while none of them can take one. What the platform runs for the VPC itself (its name service, its monitor, its cloud storage access and its addresses' connectors) follows it and is not listed. Nothing moves until you press Start and confirm; the confirmation says which pods move and what is interrupted during the migration — cloud storage access pausing for a moment, or the VPC's public addresses reconnecting, included.
Administrator only. dry_run changes nothing: it answers the cluster's nodes, where the
VPC runs now and, with nodes, what would move. Without it the migration starts, and
GET on the same path follows it step by step.
# What would move
curl -k -X POST https://api.YOUR_DOMAIN/vpcs/VPC_ID/migrate/internal \
-H "X-Admin-Token: YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"nodes": ["w1", "w2"], "dry_run": true}'
# Start it
curl -k -X POST https://api.YOUR_DOMAIN/vpcs/VPC_ID/migrate/internal \
-H "X-Admin-Token: YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"nodes": ["w1", "w2"]}'
# Follow it
curl -k https://api.YOUR_DOMAIN/vpcs/VPC_ID/migrate/internal \
-H "X-Admin-Token: YOUR_ADMIN_TOKEN"
See Nodes and VPC Placement in the API reference.
The migration runs in the platform, one pod at a time, each running again before the next one goes. A pod with persistent storage is stopped while its data is copied to the new node and checked; it starts on that node, and its old copy is removed only once it is ready. Stopped pods and tunnels stay stopped. If a step fails before its pod has switched over, that pod is put back where it was and the migration stops; pods that had already moved stay on their new nodes. Only when nothing had moved does the VPC go back to its previous nodes. The table stays on the card and follows the migration step by step, a bar under each step's state showing how far it is; the card cannot be closed until the migration has finished. A step that does not finish says which pod is not ready on which node, and why. Once the migration has set the nodes — the first thing it does — every new pod of the VPC (a copy its autoscaling adds, one that replaces a copy on a failed node, a pod started) goes to the chosen nodes, and pods that stay are not restarted. Of a pod with several copies, only the copies on other nodes move. The one exception is a pod with auto-healing or autoscaling last started or restarted before the platform's current version: until it is started or restarted again, a new copy of it may start outside the nodes (the administrator is told: Pods outside the VPC's nodes), and the first time one of its copies has to move, all of its copies may restart onto the nodes, once, one at a time. Removing a node from the cluster (the installer's Shrink) moves its pods with this same migration.
Migrate external, the second button, opens the card that migrates the VPC to another cluster of the platform, into an existing tenant of that cluster: the target cluster, from the list of the other clusters on this cluster's Cloudflare account, the target tenant ID and the target tenant API token, Check, Start — the same card a tenant has on its own VPC (see Migrate External). No administrator of either cluster takes part, and when the migration finishes the VPC is deleted from this cluster.
Tunnels Tab
Every tenant's tunnels in one table — tenant, VPC, pod, hostname, protocol, status and when it was created — with a filter by tenant above it; the table refreshes with the page, and the filter stays as you set it. A tunnel is stopped, started or deleted on its tenant's card (Tenants → the VPC's Tunnels tab); the platform's own addresses are the Config page's Service Tunnels card.
Storage Tab
Two tables, as on a tenant's Storage tab, for every tenant and with a Tenant column. Persistent Storage lists every pod with persistent storage: how much its folder holds, the cluster, its tenant, VPC and subnet, the node the folder is kept on, and its cloud storage. Cloud Storage (S3) lists every cloud storage of every tenant, with its UUID, the pods using it, its DB clusters, how much it holds and when it was made (or assigned), and every one on the Cloudflare account that no tenant of this cluster holds: made by another cluster connected to the same account, by an earlier install, or by a tenant deleted since. Those show No tenant. A space on the account that the platform did not make is one cloud storage too, marked External. A storage shows once it holds at least one file. The platform looks at the account every 15 minutes and every time the Cloudflare card is saved.
Refresh, in the Cloud Storage table's title bar, looks at the account at once. A storage no tenant holds has Assign: a pop-up with the cluster — this one, or another cluster on the same Cloudflare account — that cluster's tenants, and the name the tenant sees it by. A storage is deleted with the × on its row; while cloud storage is not configured the delete of a storage this cluster created is greyed out, and hovering it says what to do: save a token with API Tokens: Edit under Account in Config → Cloudflare, and the platform sets cloud storage up from it. An assigned storage has Take over on its row.
Administrator only. POST /provider-config/storages/refresh looks at the account at once
and answers the cloud storages no tenant of this cluster holds — each with its
id, name, kind, how many files it holds and whether it is
assignable and deletable — how much each tenant's storage holds, and
the tenants. POST /cloud-storages/{storage_id}/assign gives a storage to a tenant,
DELETE /cloud-storages/{storage_id} deletes one no pod uses (one no tenant holds with its
name as confirm), and POST /cloud-storages/{storage_id}/take-over takes one
over, its name as confirm. The refresh also answers the other clusters on the account
with their tenants (clusters): an Assign with cluster_id gives the storage
to a tenant of that cluster, and answers 202.
# Look at the account now
curl -k -X POST "$API_URL/provider-config/storages/refresh" \
-H "X-Admin-Token: YOUR_ADMIN_TOKEN"
# Give a storage no tenant holds to a tenant, with the name the tenant sees it by
curl -k -X POST "$API_URL/cloud-storages/STORAGE_UUID/assign" \
-H "X-Admin-Token: YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"tenant_id": "TENANT_ID", "name": "reports-archive"}'
# Or to a tenant of another cluster on the same Cloudflare account
curl -k -X POST "$API_URL/cloud-storages/STORAGE_UUID/assign" \
-H "X-Admin-Token: YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"cluster_id": "CLUSTER_ID", "tenant_id": "TENANT_ID", "name": "reports-archive"}'
# Delete one no tenant holds from the account, with every file in it: its name, as shown
curl -k -X DELETE "$API_URL/cloud-storages/STORAGE_UUID?confirm=Storage%201a2b3c4d" \
-H "X-Admin-Token: YOUR_ADMIN_TOKEN"
# Take one over: its name, as shown
curl -k -X POST "$API_URL/cloud-storages/STORAGE_UUID/take-over" \
-H "X-Admin-Token: YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"confirm": "reports-archive"}'
See Cloud Storages in the API reference.
Only a storage no pod uses can be deleted, and one this cluster created goes with every file in it. An assigned storage keeps its UUID and its files, appears on that tenant's Storage tab and in their pod form, marked Assigned, and their pods use it in any of their VPCs. This cluster never deletes an assigned storage's files: deleting it removes it from this cluster, keeps its files, and from then on none of that tenant's pods can reach it. One that holds files of a current tenant of this cluster (its record on this cluster lost) can only go back to that tenant, and is then its own again. An external one is assigned like any other, is never deleted by the platform, never taken over, and given to no pod. Deleting a storage no tenant holds deletes it from the Cloudflare account, with every file in it: this cluster cannot tell whether another cluster on the same account still uses those files, so delete only what you know is left over.
Given to a tenant of another cluster on the same Cloudflare account, the storage is handed to that cluster, which assigns it to its tenant within a minute, with its own rules. Until then its row says Requested and that tenant; once assigned, the Tenant column shows that tenant and its cluster, and the storage is neither assigned nor deleted on this cluster again — it is that cluster's. A cluster that refuses it says why, on the row's Refused, and the storage is offered again. The other clusters are your clusters connected to the same Cloudflare account and approved on your Circloud profile; one is offered in the pop-up once it has listed its tenants on the account, a few minutes after it is connected.
A storage marked Temporary carries a pod's persistent storage to another cluster while its VPC migrates to it: it is never assigned, taken over or given to a pod, no delete takes it while the migration runs, and the migration deletes it when it ends.
Take over is for a storage whose cluster is gone (an earlier install, a deleted tenant): this cluster then manages it as its own, and deleting it, or its last pod, deletes its files. A storage that went to another cluster with a migrated VPC cannot be taken over: that cluster uses its files. Files another cluster wrote before cloud storages had a UUID show as Unidentified and a short id; they cannot be assigned. The cluster that wrote them may still use them: delete them only when you know it does not. Take over and every delete from the account ask you to type the name shown, and all of them are recorded in the Ops Log.
Deleting a tenant takes its cloud storages with it, emptied before the delete answers — unless the
delete dialog's Also delete its cloud storages is switched off (over the API,
delete_cloud_storage=false), which keeps the storages' files on the account for the next
Refresh to list and assign to another tenant. One delete removes up to 10,000 files: a
tenant with more is refused with a sentence that says how many went, and each repeat of the delete
removes the next batch.
A VPC's cloud storage answers through two proxies on different nodes, sharing its address,
https://cloud-storage: when the node of one fails, the other answers within seconds. When one
has not answered for ten minutes — its node is down, it waits for a node, or it keeps restarting
— the alert Cloud storage on one proxy names the VPC: if the remaining one or its
node fails too, the VPC's storage calls fail until a proxy answers again. The alert is yours alone: the
tenant's storage still answers, and nothing in it is the tenant's to do. A VPC with a single node runs on
one proxy by design, and raises no alert.