Apps

Runtimes, framework presets, Git deploy with rollback, workers and cron for apps in any language.

In preview apps version 0.1.0

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.

ActionWhat it doesRiskPreview
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.view View apps, deploys and logs
  • apps.operate Start, stop and restart app processes
  • apps.deploy Deploy and roll back
  • apps.manage Create, change and delete apps
  • apps.runtime.manage Install language runtimes
  • apps.webhook Trigger a deploy through a webhook
  • apps.admin Administer apps on every server

Plan limits

  • apps.apps Apps
  • apps.container_cpu CPU of one app container
  • apps.container_memory_mb Memory 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.install installs 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 (apps table): 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 .env symlink.
  • Processes: rc-app-<user>.<app>[.<worker>].service, cron as rc-cron-<user>.<app>.<job>.{service,timer} (cron expressions are converted to OnCalendar), all User=, Slice=rc-acct-<user>.slice, NoNewPrivileges, PrivateTmp, ProtectSystem=full, restart on failure. ${PORT} in argv is expanded by systemd. The dot separator keeps accounts a-b/app c and a/app b-c from colliding.
  • Deploy (apps.deploy.run, one at a time per app): fetch the bare mirror (~/apps/<app>/repo) with the deploy key, git clone a release dir releases/<id>, drop .git, link shared paths (first deploy seeds shared/ from the repo, e.g. Laravel storage), 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), swap current with ln -sfn + mv -T (atomic rename), rewrite units and restart, reload the account's rc-fpm-<ver>-<user> for PHP, health-check (HTTP path or "unit stays active"); any failure after the swap flips current back and restarts the previous release. Old releases beyond keep are deleted. apps.deploy.rollback swaps to the previous (or a named) release. Log (256 KiB head+tail) is stored in deploys and the final status goes out as apps.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 from manage.py), Flask, FastAPI, Python, Rails, Spring Boot, ASP.NET Core, Phoenix, Go, Rust, Deno, Bun, static. preset: auto runs detection on the first deploy and stores the answer; every field is editable.
  • Sites: apps.site.link (or domain_id on create) calls web.sites.create: PHP -> php site with docroot apps/<app>/current/<public>, static -> static, process -> proxy to http://127.0.0.1:<port> (needs the admin proxy.allow_internal layer setting, see modules/web). The web module pre-creates the docroot path as real directories; the first swap replaces an empty current/ 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 and ping, then deploys in the background.

Not done / needs core

  • A public webhook URL. There is no module-owned unauthenticated HTTP route; PublicRoutes is core. The action takes the raw body (base64) and signature and needs an API token with apps.webhook (admin default). Core should add POST /api/v1/hooks/apps/{app_id} that forwards body + X-Hub-Signature-256/X-Gitlab-Token/event header to apps.webhook.receive as 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 mise spec 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 in apps columns plus apps.config (AppConfig).
  • Env scope: each variable is both (default), build (build/migrate/hooks only) or runtime (running processes only). A secret shows set: 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_deploy failures are warnings.
  • Custom preset: preset: custom starts 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.create takes template: {id|name,scope,version}; fields in the request win over the template. Store packages: the Store verifies the signature and calls the internal apps.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's env (a repo cannot clobber a secret); limits and secrets are not overridable. apps.override.preview shows 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/set go through web.layers.get/set on 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, current and keep work as before) + one local image per built service, rc-app-<account>-<app>-<service>:<release>, labelled rc.account/rc.app/rc.release.
  • Dockerfile mode: the agent generates a one-service compose file (build: the repository, publishes the container port on 127.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 with build: for the app's own repository: context/dockerfile must be repository-relative (no absolute path, .., URL), args literal, target allowed; 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_add outside 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: never for built images.
  • Build: docker build with BuildKit on the node's Docker daemon, streamed to the job log. Secret build variables (env scope build/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, so docker 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 limits apps.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.rollback re-applies an earlier release's stored definition (rel-<id>.in in the root-only stack directory) with its image; if that image was pruned the rollback says so. keep prunes 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 through docker 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 through containers.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).