Nimbus

For machines

Working with Nimbus from an AI agent

There are three separate things here, and they are for three different moments. Pick the one that matches what your agent is doing.

Operating the box MCP
An assistant that should be able to act — create a site, provision a database, unlock an address, restart a container. Connect it to the panel’s MCP server and it discovers the tools it may call.
Scripting the box HTTP API
The same actions as JSON over HTTP, for a caller that is a shell script or a CI job rather than a model.
Working in a site’s repo deployment.md
A coding agent editing an application that is hosted here. It does not need the panel at all — it needs to know how deployment works. Drop the file below into the repository.

Connect

The MCP server

Each box exposes its own endpoint at /mcp on the panel domain. Tools are scoped to the token’s account, so an agent can never do more than the person who issued it.

Claude Code .mcp.json
{
  "mcpServers": {
    "nimbus": {
      "url": "https://panel.example.com/mcp",
      "headers": {
        "Authorization": "Bearer $NIMBUS_TOKEN"
      }
    }
  }
}
Anything else HTTP

The API mirrors the panel’s vocabulary one-to-one, so the guide doubles as its documentation.

$ curl -H "Authorization: Bearer $NIMBUS_TOKEN" \
    https://panel.example.com/api/sites
Scope A token belongs to a panel account and inherits its role, and there are three of them. An administrator token reaches every site on the box plus the platform-wide pages — accounts, routes, Site Images, backups, settings. A site owner token reaches that account's own sites and does everything on them: create, publish, deploy, provision a database, lock, delete. An unlock_user token can unlock an address and nothing else. Treat one like the password it replaces.
Do not guess hostnames The panel serves /system, a read-only page stating what this particular box uses: the panel's own version number under Platform, which is what to quote in any problem report; the MySQL and Redis hosts — separately for catalog sites and for stacks, because they differ — where site and stack folders live, and the certificate authority. Any signed-in account may read it, whatever its role, provided the request comes from an unlocked address. It carries no credentials; those come from the site's own Databases page.
A 404 can mean "not yours" A site a site-owner token has no claim on is answered as though it does not exist, so 404 means deleted or somebody else's and the response cannot tell you which. Check the account's role before reporting that a site is gone. A 403 is a separate matter and still means the IP allowlist, never the role — the two are not interchangeable, and a runbook that starts on an administrator-only page has to name the role it needs. Whether a site is locked is now readable from the sites list itself: its Visibility column carries a Locked chip for a whole-site lock and a folder count when only some paths are locked, alongside published or unpublished, which is a separate question — a site can be both published and locked, and for a staging site usually is.
The image list is open-ended, and images are versioned Do not treat laravel, web-base and web-spa as the set of site types. They are the built-in ones; an administrator can upload custom images — a Dockerfile and a nimbus-image.json manifest in a zip — through the Site Images page, formerly Catalog, and they appear in the same picker. Read the list off the box rather than assuming it. A custom image also carries a version, and a site pins the exact version it was created with: images are immutable, there is no edit of any field, and changing one means uploading a new version and moving sites onto it with the ordinary change-image action. An image that has not finished building, or that failed, is listed but cannot be chosen. The settings a site accepts come from the image's manifest, so they vary per image and an undeclared key is rejected rather than passed through. The manifest also decides whether a site gets an .env and a database at all: nimbus-image.json takes displayName, description, settings and envFile, and only when envFile is true does a site on that image get the Environment and Databases tabs. It is optional and defaults to false, so a manifest written without it produces an application image with no way to set credentials — and nothing reports that as an error. Built-in images are not special here; laravel declares it like any other.
The unlocked-address list is filled three ways The Unlocked IPs page holds three kinds of entry. Addresses unlocked by an account — one per account, replaced when that account unlocks from a new place, dropped when the account is deleted; addresses opened by an API token, one per token, listed with the token, its account and when it last called, and rewritten without anyone touching the panel; and well-known addresses, typed in by an administrator with a name, belonging to no account and removed only by hand. All three grant the same thing: every locked site and folder on the box. Adding or removing a well-known address is administrator-only, takes a single IPv4 or IPv6 address and not a CIDR range, and is refused if that address is already listed — including when it is spelled differently, since 2001:DB8::1 and 2001:db8::1 are the same address. Removing a token's row does not revoke the token, and the token reopens an address on its next call; only revoking it, by the account that holds it, is permanent. Describing the page as one table describes the old one.
Backups are per-site nightly, on days the site can choose A site's Backup tab carries Backup days: seven weekday buttons deciding which nights that site is taken on, settable by a site owner as well as an administrator. Every site starts on Not set — all seven selected, following the platform — and saving days replaces that with the site's own choice plus a Back to the default button; all seven saved looks the same as Not set in the buttons, so read the label. At least one day is required: an empty set is refused, and there is no control here for turning a site's backups off. A site is paused for a few seconds while its copy is taken, which is the reason the control exists; on a night it is not due there is no run and no skipped entry anywhere. Retention is a count of copies, never a number of days — do not convert one into the other, since the span depends on the days chosen. The platform's own copy runs nightly regardless and pauses nothing. Times and schedules follow a server timezone on Settings → General, administrator-only, defaulting to UTC; displayed times follow a change at once while the nightly schedule follows it only after the panel is restarted.
A backup can leave the platform, by email A site's Backup tab offers Download backup beside Restore this copy, and a site owner can use it on their own sites. It does not return a file: pressing it prepares a zip in the background and emails a link when the copy is ready, with the same link appearing on that row. Say so when you describe the flow, because a runbook that waits for a download after the click waits forever. The link expires after 72 hours and the file is then deleted from the server. It is per account, not a bearer URL: it needs the session of whoever asked, so it cannot be handed to an administrator or to the site's owner, and the random filename only stops it being guessed. It does work from any address, unlike most of the panel. One preparation per site at a time — a second request for the same site while one is running is refused. A failure arrives as an email saying the download is unavailable, without a reason; the reason goes to administrators, and is usually free space on the server. The zip carries the site's files, its .env, its database dump and its TLS private keys, so never route one into a shared location or a log.
Files are reachable over SFTP, and a lock does not close them Deployment is no longer the only way onto a site's folder. Any account, whatever its role, can add the public half of an SSH key on Your accountSSH keys and open its sites over SFTP on port 2223, with the account's email address as the username — quote it, since the @ otherwise reads as a host separator: sftp -P 2223 'you@example.com'@host. There is no password authentication and cannot be one. A session shows one directory per site the account holds, computed at connect time, so ownership changes need no re-authorisation; the panel's own site is never among them, and a mounted folder is not either, because a mount lives in the container while SFTP sees the folder on the server. Uploaded files are owned correctly with no chown. The security shape matters for any advice you give: that port is not behind the IP allowlist, so locking a site does not close its files — removing the key from the account is what revokes access, and it applies to the next connection rather than to an open session. An administrator can list and remove keys on any account but cannot add one. Describe the session confinement as the SFTP service restricting it to those directories, not as a filesystem permission boundary.
A new account may have no password yet An administrator creating a user either types a password or has the panel email an invitation, and the second one leaves an account that exists, is listed, and cannot sign in. Sign-in fails identically to a wrong password, on purpose, so never diagnose it from the sign-in response: the users list carries a Sign-in column reading Active, Invited with the date, Invitation expired (a link lasts 7 days by default) or Invitation not sent, which is the mail service refusing the message and the only mail failure the panel knows about. Links are single-use; re-sending invalidates the previous one immediately, Withdraw kills the link and leaves the account in place and unusable, and setting a password on the edit form silently withdraws any outstanding invitation. Delivery is unknowable — no bounce, no receipt, no send history — so the correct remedy for "they did not get it" is always re-send, never a lookup. Invitations need NIMBUS_NOTIFICATION_TOKEN in the server's .env; without it the option is shown disabled with the reason and typing a password still works. The invitation page is outside the IP allowlist by design, since the recipient could not be on it yet.
Locked out is recoverable without a person A 403 from the box means the calling address is not on the allowlist, and a token can fix that for itself: PUT /api/v1/allowlist/address on the panel domain, with the bearer token and a JSON body carrying a required label, unlocks the address the call came from — there is no address field, and one in the body, the query string or a forwarding header is ignored. That bounded blast radius is why every role can hold a token, unlock-only included, and why one is safe on a timer. The call is idempotent, one token holds one address at a time, and revoking a token also closes the address it opened. Read the reply, not just the status: a 200 carrying "enforced": false means saved but not yet in force, and the fix is to repeat the same call. 401 covers wrong and revoked alike; 429 is the limit of ten calls a minute per address. Tokens are issued at /account/tokens, which is outside the admin area on purpose, and shown exactly once.
A published port is outside the edge and outside the allowlist Not everything on the box is served by Caddy any more. A stack can have published ports: a host port in the 2200–2299 range forwarded straight to a service's container port, added by an administrator on the Ports page, for traffic that is not HTTP — git clone ssh://… against a Gitea stack is the case it exists for. Two consequences a runbook must not get wrong. There is no TLS, so nothing is encrypted unless the service does it itself. And the IP allowlist is an HTTP-level control that cannot cover a raw port at all, so locking a stack does not close its published ports — the service's own authentication is the only thing in the way. Do not advise publishing a port as a way of exposing a website; that is a Route. Catalog sites have none, and a site-owner token cannot see or change any of this. Also keep the two lists apart: a stack's allocated ports are the ${NIMBUS_PORT_*} numbers the panel picks out of the compose file and only the edge can reach, while published ports are picked by a person and reachable by anyone.
Restart and Recreate are different buttons A catalog site's page offers both. Restart runs the same container again — deployed code, an edited .env, a new background program. Recreate builds a new container from the image, and is the only one that applies a changed site type, a setting that is read as the container is created such as a memory limit, or a mounted folder. Nothing in the site's folder, and no domain, certificate, database or deployment link, is lost either way. A stack has neither; it is deployed.
A site can hold folders belonging to another site A catalog site has a Mounts tab: folders from other sites of the same owner, each with the site it comes from, the folder inside it, the path it appears at in this site, and whether this site may write to it or only read it. Only an administrator adds, changes or removes one; a site owner reads the tab. Four things follow that are easy to get wrong. Mounts are fixed as the container is built, so a change needs a Recreate and a restart does nothing — the tab shows each one as Running or Pending recreate. Read-only is the default and the recommendation. The path a mount lands on is outside the site's own folder, because a deploy re-publishes that tree; a symlink made by the deploy is the answer when an application needs the files inside its own directory. And a missing source degrades one folder rather than the site: the container still starts, that mount is skipped, the row reads Source missing with the path, the site is flagged on the sites list, and nothing is created in its place — a recreate before the folder is back changes nothing. Deleting a site whose folder is mounted is refused and the message names the dependants, as is moving it to another owner. Stacks have no mounts.
Advising on a slow or heavy site Do not reason from guesses about load: the panel measures memory, CPU, peak and disk per site, over 6 hours to 30 days, scoped like everything else to what the account owns. Two settings shape a PHP site's ceiling and only mean anything multiplied together — memory per request and how many requests it serves at once — and a site can additionally be given a container memory limit, which takes effect at the next re-create rather than on save. Disk is last night's figure, produced by the backup, so a site without backups reads "Not measured" rather than zero. Do not compare a site's CPU with the server's: a site's is a percentage of one core and legitimately exceeds 100%, while the server's is the share of the whole machine, capped at 100 and averaged over the last minute rather than live. The overview's server line adds swap in use against swap total beside host memory, in words when the server has no swap rather than as 0 B / 0 B; memory and swap are live while the CPU figure beside them is an average over up to a minute, and there is no per-site swap figure at all.
A failed backup is emailed, and the panel cannot tell you it arrived The platform sends one kind of email: one message per failed backup run, to every administrator account, naming the units that failed with the reason recorded for each, the counts that succeeded and were skipped, and a link to that run. Two states send it — a run where everything failed, and a run where some units failed while others succeeded — while a site deferred for being mid-deploy is not a failure and is simply taken on the next run. Recipients are worked out at send time from who is an administrator; there is no subscription list. Do not build a check on top of delivery: the panel hands the message to a mail service and keeps no receipt, no bounce and no send history, so it can report that a notification was sent and never that anyone received it. The credential lives in the server's configuration file, not the panel, so this is off until the operator turns it on — and off changes nothing about whether backups run or what they record. Backup retention and storage are now tabs on Settings, not on Backups; the old addresses redirect.
The panel itself is responsive Every panel page lays out down to 320px, with the menu behind a ☰ button on a narrow screen. If you are writing documentation, a walkthrough or a browser test, do not assume a fixed desktop viewport or state that anything must be done from a computer — signing out included.

Drop-in

deployment.md, for a site’s repository

When a coding agent is asked to add deployment to an app hosted here, it needs facts about this platform that are not in the repository it is looking at. deployment.md is those facts, written for a machine to act on.

Save it at the root of the site’s repository, keeping the filename.

$ curl -O https://panel.nimbus-online.net/deployment.md
Give it the three facts The file tells the agent what to write, but not which site it is writing for. Before you set it going, give it the runner label, the domain and the site type — what the panel now calls the image. The first two are on the site’s Deployments tab, the third on its General tab. laravel, web-base and web-spa are the built-in entries, not the whole list: the image list is per box and an administrator can upload to it, so read the value off the site rather than assuming one of three. Guessing the runner label produces a workflow that never runs. For a custom stack there is no runner and no domain: tell the agent it is writing a compose file for a stack, and it will follow the contract in deployment.md instead.
One thing to set up front Applications here run behind an edge proxy that terminates TLS, so an app that does not trust the forwarded headers sees one client address for every visitor and generates http:// links. Have the agent configure trusted proxies during scaffolding rather than after the first bug report: on Nimbus, trust all proxies — at: '*' in Laravel — with the four X-Forwarded-* headers. The proxy's own address is a container address and is not knowable from inside the app. Tell it not to carry that '*' to any other host, where a visitor could forge the header; the reasoning is in chapter 07.

Reading, not acting

This whole site, as text

For an agent that just needs to understand the platform, every page here is published as markdown too, and indexed the standard way.

/llms.txt
The index: one line per page, with a description. Start here — it is small on purpose.
/llms-full.txt
Every page concatenated, for a single fetch.
Any page .md
Append .md to any page URL — /help/sites.html.md is the markdown of /help/sites.html. A URL with no filename takes index.html.md, so /help/ becomes /help/index.html.md. That is the llmstxt.org convention: appended, not substituted.