Actions
35 actions, callable from the panel, the command palette and the API as
POST /api/v1/a/<id>. Internal actions used between modules are not listed.
| Action | What it does | Risk | Preview |
|---|---|---|---|
apps.preset.list | List framework presets (read) | low | No dry run |
apps.runtime.install | Install language runtimes | medium | No dry run |
apps.runtime.list | List installed runtimes (read) | low | No dry run |
apps.app.detect | Detect a repository's framework (read) | low | No dry run |
apps.app.create | Create an app | medium | |
apps.app.update | Change an app | medium | |
apps.app.get | Show an app (read) | low | No dry run |
apps.app.list | List apps (read) | low | No dry run |
apps.app.delete | Delete an app | high | No dry run |
apps.env.list | List environment variables (read) | low | No dry run |
apps.env.set | Edit environment variables | medium | No dry run |
apps.deploy.run | Deploy an app | medium | |
apps.deploy.list | List deploys (read) | low | No dry run |
apps.deploy.get | Show a deploy with its log (read) | low | No dry run |
apps.deploy.rollback | Roll back to a previous release | medium | No dry run |
apps.release.list | List releases on the server (read) | low | No dry run |
apps.unit.control | Start, stop or restart a process | low | No dry run |
apps.status | Process status (read) | low | No dry run |
apps.logs.get | Process logs (read) | low | No dry run |
apps.workers.set | Set queue workers | medium | No dry run |
apps.cron.set | Set scheduled jobs | medium | No dry run |
apps.site.link | Serve an app on a domain | medium | No dry run |
apps.webhook.info | Show the webhook secret | high | No dry run |
apps.webhook.rotate | Rotate the webhook secret | high | No dry run |
apps.webhook.receive | Receive a Git webhook | medium | No dry run |
apps.reconcile | Re-apply every app's units | low | No dry run |
apps.template.list | List app templates (read) | low | No dry run |
apps.template.get | Show an app template (read) | low | No dry run |
apps.template.save | Save an app as a template | low | |
apps.template.delete | Delete an app template | medium | |
apps.template.export | Export a template as YAML (read) | low | No dry run |
apps.template.import | Import a template from YAML | low | |
apps.override.preview | Preview the repository's respirecloud.yaml (read) | low | No dry run |
apps.nginx.get | Show the app's web server snippets (read) | low | No dry run |
apps.nginx.set | Set the app's web server snippets | medium |
Permissions and limits
Permissions
apps.viewView apps, deploys and logsapps.operateStart, stop and restart app processesapps.deployDeploy and roll backapps.manageCreate, change and delete appsapps.runtime.manageInstall language runtimesapps.webhookTrigger a deploy through a webhookapps.adminAdminister apps on every server
Plan limits
apps.appsAppsapps.container_cpuCPU of one app containerapps.container_memory_mbMemory of one app container
Engineering notes
Generated from modules/apps/docs.md at build d90e9e2. These are the notes the engineers keep
next to the code: precise, technical, and honest about what is not done yet.
Declared in module.yaml; agent driver internal/agent/apps*.go; op types sdk/agentop/apps.go; detection table
modules/apps/preset (pure Go, unit-tested; heuristics follow railpack's providers, MIT).
Model
- Runtimes:
apps.runtime.installinstalls Node, Python, Ruby, Go, Java, .NET, Deno, Bun, Elixir, PHP or Rust per account with mise (pinned release, sha256 checked,/usr/local/bin/mise; data in~/.local/share/mise). PHP apps default to the distro PHP + composer (runtime: system) so they match the web module's FPM. - Apps (
appstable): repo URL, branch, preset, build/migrate/start as argv arrays (no shell strings), health path, shared paths, workers, cron, keep-N, port (21000-21999, per node), link to the web site. A generated ed25519 deploy key (private half in the vault, public half returned) and a webhook secret (vault). - Environment:
apps.env.set/list; secret values go to the vault (apps/<id>/<KEY>), never to the table, never returned. On the node they land in/etc/respirecloud/apps/<user>/<app>.env(root:user 0640) read by the units (EnvironmentFile=), by build steps (-E) and, for dotenv readers like Laravel, through a.envsymlink. - Processes:
rc-app-<user>.<app>[.<worker>].service, cron asrc-cron-<user>.<app>.<job>.{service,timer}(cron expressions are converted to OnCalendar), allUser=,Slice=rc-acct-<user>.slice,NoNewPrivileges,PrivateTmp,ProtectSystem=full, restart on failure.${PORT}in argv is expanded by systemd. The dot separator keeps accountsa-b/appcanda/appb-cfrom colliding. - Deploy (
apps.deploy.run, one at a time per app): fetch the bare mirror (~/apps/<app>/repo) with the deploy key,git clonea release dirreleases/<id>, drop.git, link shared paths (first deploy seedsshared/from the repo, e.g. Laravelstorage), run build steps then migrations in the release dir, as the account user, in its slice (systemd-run --uid --slice; root never follows a path in the home), swapcurrentwithln -sfn+mv -T(atomic rename), rewrite units and restart, reload the account'src-fpm-<ver>-<user>for PHP, health-check (HTTP path or "unit stays active"); any failure after the swap flipscurrentback and restarts the previous release. Old releases beyondkeepare deleted.apps.deploy.rollbackswaps to the previous (or a named) release. Log (256 KiB head+tail) is stored indeploysand the final status goes out asapps.deploy.finished. - Presets (
apps.preset.list): Laravel, Symfony, WordPress, Drupal, generic PHP, Next.js, Nuxt, SvelteKit, Remix / React Router, Astro (static or node adapter), NestJS, Express/Fastify/Koa, Node, Vite, Eleventy, Hugo, Jekyll, MkDocs, Django (module derived frommanage.py), Flask, FastAPI, Python, Rails, Spring Boot, ASP.NET Core, Phoenix, Go, Rust, Deno, Bun, static.preset: autoruns detection on the first deploy and stores the answer; every field is editable. - Sites:
apps.site.link(ordomain_idon create) callsweb.sites.create: PHP ->phpsite with docrootapps/<app>/current/<public>, static ->static, process ->proxytohttp://127.0.0.1:<port>(needs the adminproxy.allow_internallayer setting, see modules/web). The web module pre-creates the docroot path as real directories; the first swap replaces an emptycurrent/directory with the symlink. - Webhook (
apps.webhook.receive): verifies HMAC-SHA256 (sha256=<hex>over the raw body, GitHub/Gitea) or the GitLab-style token in constant time, ignores pushes to other branches andping, then deploys in the background.
Not done / needs core
- A public webhook URL. There is no module-owned unauthenticated HTTP route;
PublicRoutesis core. The action takes the raw body (base64) and signature and needs an API token withapps.webhook(admin default). Core should addPOST /api/v1/hooks/apps/{app_id}that forwards body +X-Hub-Signature-256/X-Gitlab-Token/event header toapps.webhook.receiveas the system actor (rate-limited), see the handover. - UI bundle (card w6-05), per-site "deploy status" in the web module.
- Runtimes are per account, not shared (disk); mise compiles Ruby/PHP/Python from source for some versions (needs the allow-listed build packages, installed on demand).
Customisable apps (w9-06)
Presets are starting points: every field of an app is editable (apps.app.update, with dry_run = field diff).
- Fields: runtime + version (any
misespec shaped like a version/alias; the agent refuses what mise cannot resolve), commands, process types (workers, cron, release = migrate), ports, working directory, docroot, shared paths, release retention, health path + timeout, restart policy, per-process resource limits (sub-limits inside the account slice, so never above the plan), deploy mode. Stored inappscolumns plusapps.config(AppConfig). - Env scope: each variable is
both(default),build(build/migrate/hooks only) orruntime(running processes only). A secret showsset: true, never its value; saving it with an empty value keeps the stored one. - Deploy hooks:
pre_build, post_build, pre_release, post_release, post_deploy, argv lists run in the release dir. A failing hook before the switch fails the deploy and removes the release;post_deployfailures are warnings. - Custom preset:
preset: customstarts empty (no framework detection). - Templates (
apps.template.*): save any app (secrets keep their name only), versioned per (scope, owner, name), scopes global (admin) / reseller / account. Export/import is YAML (apiVersion: respirecloud/app-template/v1, keys are the JSON field names, strict: unknown keys refused).apps.app.createtakestemplate: {id|name,scope,version}; fields in the request win over the template. Store packages: the Store verifies the signature and calls the internalapps.template.install_signed {yaml, store_ref}(becomes a global template,source: store). - respirecloud.yaml / Procfile in the repository (read after checkout, strict schema, argv only, nothing executes that
is not declared):
runtime: node@22,build,release,start,health,health_timeout,restart,workdir,docroot,shared,env,build_env,hooks.*,workers: {name: cmd},cron: {name: {schedule, command}}. Procfile:web= start,release= migrate, other types = workers. It overrides the app for that deploy (the deploy row lists the fields:override_file,overrides); panel variables win over the file'senv(a repo cannot clobber a secret); limits and secrets are not overridable.apps.override.previewshows the changes before deploying; the app can opt out (config.no_repo_file). Known limit: a later panel-side unit rewrite (env change without redeploy) uses the panel's values until the next deploy. - Per-app nginx snippets:
apps.nginx.get/setgo throughweb.layers.get/seton the app site's layer (allow-listed, validated with line numbers, drift rules and dry-run nginx diff apply). - Deploy rows carry
auto_reverted(failed after the switch and went back by itself).
Container apps: Dockerfile and Compose modes (w9-15)
mode: dockerfile | compose (config dockerfile / compose_file paths, container_port, default 8080). Needs Docker on
the node (an administrator installs it with containers.runtime.install). Code: internal/agent/apps_container.go.
- Release = a checkout of the repository (release list, marker,
currentandkeepwork as before) + one local image per built service,rc-app-<account>-<app>-<service>:<release>, labelledrc.account/rc.app/rc.release. - Dockerfile mode: the agent generates a one-service compose file (
build:the repository, publishes the container port on127.0.0.1:<app port>, runtime variables as${KEY}references,PORT=<container port>, the app's limits). Compose mode: the repository's compose file (compose.yaml, ...). Both go through the containers module's strict validator and renderer (modules/containers/compose), extended only withbuild:for the app's own repository: context/dockerfile must be repository-relative (no absolute path,.., URL), args literal,targetallowed;ssh,secrets,additional_contexts,network, cache and platforms are refused. Everything else stays refused exactly as in the containers module: privileged, host network/pid/ipc, devices,cap_addoutside the allow-list, host-path mounts, non-loopback ports. An app publishes one port (the site proxies to it); other ports stay internal. The rendered compose adds the account slice as cgroup parent, caps, no-new-privileges,pull_policy: neverfor built images. - Build:
docker buildwith BuildKit on the node's Docker daemon, streamed to the job log. Secret build variables (env scopebuild/both+ secret) are BuildKit secrets (RUN --mount=type=secret,id=KEY): written to a root-only temp dir, removed after the build, never--build-arg, sodocker history/ image config do not show them; their values are redacted from the job log and the stored log. Non-secret build variables are--build-arg. - Run: the stack
app-<name>(so app names are at most 27 characters in these modes) is applied through the containers driver; the plan's per-service ceilings come from the limitsapps.container_cpu/apps.container_memory_mb. Healthy = the containers' healthchecks plus, when the app has a health path, an HTTP check on its port. - Failure and rollback: a failure after the new containers started re-applies the previous release's compose (the
previous images are still there) and deletes the new release and image.
apps.deploy.rollbackre-applies an earlier release's stored definition (rel-<id>.inin the root-only stack directory) with its image; if that image was pruned the rollback says so.keepprunes old release directories, their images and stored definitions. - Operate: status lists the compose services (the app service = unit "", others
worker:<service>), logs and start/stop/restart go throughdocker compose; an env change (apps.units.apply) re-applies the current release. Removing the app removes the stack, its images and (with purge) its volumes. - Limits of this version: BuildKit runs build steps in the Docker daemon's cgroup, so a build is bounded by a timeout
(25 min) and the node, not by the account's slice (a per-build slice would need a daemon change; see the handover).
Workers/cron of buildpack mode do not apply (use compose services). The container stack name
app-<name>is also reachable for a user throughcontainers.stack.*(same account); do not reuse it there.
Store app templates
The Store publishes app-template items (signed channel metadata with k-of-n threshold, expiry and a monotonic version;
the package's release-role attestation covers template.yaml only; per-template rollback floor). store.template.list
shows them with version and publisher; store.template.install verifies and calls the internal
apps.template.install_signed {yaml, store_ref} as the system actor (becomes a global template, source: store).