Docs navigation

Platform

Services & instances

What an instance is, how its health is observed, and how to run your own image.

An instance is one running service — your NocoBase, your n8n, your own container image. Each instance runs on Canadian infrastructure with its own storage, TLS certificate, and backup schedule. Instances are addressed by slug, unique within your organization.

Health

An agent on each VM reports container state to our control plane: is the service answering, is its database reachable, did setup finish. The dashboard and the API show the same observed state our operators act on.

Health statuses:

  • healthy — up and answering.
  • unhealthy — the container is running but its own health check is failing.
  • degraded — partly up: a container is stopped or crash-looping while another still serves, or setup is still in progress.
  • down — no container is serving.
  • unknown — not yet observed, or the last report is stale. Common for a short window right after a deploy. Treat a short unknown as inconclusive, not as an outage.

Reading and managing instances over the API

  • GET /api/v1/instances lists your instances with their health (read_instances scope).
  • GET /api/v1/instances/{slug} returns one (read_instances).
  • POST /api/v1/instances provisions one from the catalog, or from your own image (below), and returns 201 with the apply run in meta.run_id (write_instances).
  • PATCH /api/v1/instances/{slug} moves one to a new image and redeploys it; returns 202 with the run id (write_instances). Instances we manage declaratively on your behalf return 422 not_operator_managed.
  • DELETE /api/v1/instances/{slug} tears one down and returns 202 with the run id (write_instances). The application stops; your data is kept.

Full parameters and response shapes are in the API reference.

Bring your own app

Any single-container image can run as an instance. Provision with catalog_slug: "custom" and describe the app in custom; Lunary renders the compose file server-side. A compose file is never accepted.

{
  "catalog_slug": "custom",
  "slug": "docs",
  "custom": {
    "image": "ghcr.io/acme/docs:1.4.2",
    "port": 4000,
    "env": { "PORT": "4000" },
    "secrets": ["SECRET_KEY_BASE"],
    "volumes": [{ "name": "data", "mount": "/var/lib/app" }],
    "healthcheck": { "path": "/healthz" },
    "size": "small"
  },
  "secrets": { "SECRET_KEY_BASE": "…" }
}
  • image must carry a tag or digest (name:tag or name@sha256:…). Docker Hub is assumed when no registry is given.
  • port is what the container listens on; TLS, the public URL, and custom domains work as for catalog apps.
  • env is non-secret. Names listed in secrets take their values from the request's top-level secrets, are stored encrypted, and are never returned.
  • volumes are named, backed up, and restorable. Host paths are not settable.
  • healthcheck (optional) is an HTTP path on port returning 2xx when the app is ready; it drives the health statuses above. The check runs inside your container with wget or curl, so the image needs a shell and one of the two.
  • size is small (0.25 vCPU / 512 MiB / 2 GiB), medium (0.5 / 1 GiB / 5 GiB), or large (1 / 2 GiB / 10 GiB), counted against your plan.

Private registries

A private image needs a pull credential for its registry host, stored once per organization:

  • POST /api/v1/registries with host, username, and token (write_instances). One credential per host; delete it to rotate.
  • GET /api/v1/registries lists each credential's id, host, username, and created_at — the token is never returned (read_instances).
  • DELETE /api/v1/registries/{id} removes it (write_instances). Running instances keep their pulled image; the next deploy or redeploy of an image that needs the credential fails until a new one is stored. A public image on that host still pulls anonymously.

The credential is encrypted at rest and released only to your own VM, for the pull and start of a deploy, then removed. For GHCR, use a classic personal access token with the read:packages scope.

Updating the image

PATCH /api/v1/instances/{slug} takes { "image": "…" } and nothing else. The image is validated like custom.image, stored as given, and a redeploy run is created; poll it with GET /api/v1/runs/{run_id}.

  • A tag is re-resolved on every redeploy; a digest is not. Pin a digest when you want a deploy to mean exactly one build.
  • A custom instance can move to any repository. Port, env, secrets, volumes, and healthcheck are carried over.
  • A catalog instance takes only a new tag or digest of the repository Lunary pins (repository_mismatch otherwise). Its database and other secondary images stay as pinned. Where the catalog pins a tag variant — Stirling PDF runs -ultra-lite — the new tag must keep that suffix, and a digest is refused because it cannot show one (variant_mismatch).
  • The request is refused while a redeploy of the instance is still running (redeploy_in_flight), while its redeploy plug is switched off (plug_disabled), and when it would move from a registry you hold a credential for to one you do not (no_registry_credential). A move between hosts you hold no credential for is accepted; if the new image is private, the redeploy run fails at pull.

The dashboard's Settings → Image card shows the desired image, the image each container is observed running, and the digest the last successful deploy resolved to.

Reading with an agent? This page as markdown, or the whole docs as llms.txt.