Application Templates

Available from replication-manager 3.1.43.

An application is deployed next to the database cluster from a template: a TOML file of the template repository (prov-app-template-repo, the public cloud18-templates) or of the cluster's local templates. Add it from the GUI, from the API (POST /api/clusters/{cluster}/actions/addserver/{name}/{port}/app/{template}) or from the MCP tool app-add, then provision it.

10.6.1 Substitution keys

A template is substituted with the cluster's data before it is loaded. The raw file must be valid TOML before the substitution, so selectors take bare values.

key value
{{app.name}}, {{app.port}}, {{app.host}} the app being added; host is its service name in the cluster namespace (<app>.<cluster>.svc.<orchestrator cluster>)
{{name}} the cluster name
{{config.<field>}} a cluster setting, e.g. {{config.provAppAgents}}, {{config.cloud18Domain}}
{{proxies.0.name}}, {{proxies.0.writePort}}, {{proxies.#.name}} the proxies: the first one, or all names comma-joined
{{apps.#(name==valkey1).host}} another app of the cluster, by name: host, port, config.deployment.variables.#(name==X).value, db.*

The namespace glues the apps together: every app answers on its service name, so a dependent app references another one through its environment variables, e.g. REDIS_CACHE = "redis://{{apps.#(name==erpnext-cache).host}}:6379". The referenced app must exist in the cluster when the dependent one is added (not necessarily provisioned).

A template carries plain default values, never free placeholders: an unresolved key refuses the add with the list of missing keys.

10.6.2 A database for the app (app.db)

The cluster is the database service, so an app asks it for its own schema:

app-db-auto-create = true

[[deployment.variables]]
  name = "DB_HOST"
  type = "env"
  value = "{{proxies.0.name}}"
[[deployment.variables]]
  name = "DB_NAME"
  type = "env"
  value = "{{app.db.schema}}"
[[deployment.variables]]
  name = "DB_USER"
  type = "env"
  value = "{{app.db.user}}"
[[deployment.variables]]
  name = "DB_PASSWORD"
  type = "secret"
  value = "{{app.db.password}}"

Referencing {{app.db.*}} is enough to request the database. At add time the app settings app-db-schema and app-db-user default to the app name (lower-case, a-z0-9_), and app-db-pass to a generated password stored encrypted; all three can be set by hand in the app Overview or through the app settings route. At provision, replication-manager creates the schema, the user for % (the configurator disables name resolution, so a hostname-bound account could never log in; the database is reachable from the cluster network only) and grants it all privileges on that schema only. Every statement is in the SQL log.

An existing schema or user is never touched. The app records what it created (app-db-owned); a provision that finds the schema or the user already present without that mark is refused, the app shows the state APPERR008 and nothing is altered. Free the names with app-db-schema / app-db-user, or set app-db-owned when the objects really belong to the app. On an owned account the provision re-applies the stored password, and setting app-db-pass rotates it at once. Dropping the app never drops the schema or the user.

Dependent processes share the owner's database: {{apps.#(name==erpnext-backend).db.user}}, .db.schema, .db.password (as a secret variable). A prov-app-docker-cmd reads the password from its environment variable, never from the key.

Settings: app-db-auto-create (bool), app-db-schema, app-db-user, app-db-pass, app-db-owned (bool), on the route /api/clusters/{cluster}/apps/{app}/settings/actions/set/{setting}/{value} (or switch for the booleans). The MCP tools app-add and list-cluster-apps answer the db object.

10.6.3 Files on the S3 provider (s3-mounts)

An app that must share files between its processes mounts a bucket of the cluster's S3 provider app (template rustfs, app-s3-provider = true, its volume is metered as archive, BAU) instead of a shared block volume:

[[deployment.paths]]
  dockerpath = "/home/frappe/frappe-bench/sites/erpnext/public/files"
  name = "path-s3-erp-public"
  srcname = "erp-public"
  srcpath = "mnt/erp-public/"
  srctype = "s3"
  volumename = "erp-mounts"

[deployment.storages]
  [[deployment.storages.s3-mounts]]
    name = "erp-public"
    bucket = "erp-public"
    endpoint = "{{apps.#(name==rustfs1).host}}:9000"
    accesskey = "{{apps.#(name==rustfs1).config.deployment.variables.#(name==RUSTFS_ACCESS_KEY).value}}"
    secretkey = "{{apps.#(name==rustfs1).config.deployment.variables.#(name==RUSTFS_SECRET_KEY).value}}"
    volumename = "erp-mounts"
    volumedir = "mnt/erp-public"
    uid = "1000"
    gid = "1000"

A mount sidecar presents the bucket inside the container, as the owner uid/gid (default 33, www-data). The provider's credentials are read from its variables, MinIO (MINIO_ROOT_USER/MINIO_ROOT_PASSWORD), RustFS (RUSTFS_ACCESS_KEY/RUSTFS_SECRET_KEY) or AWS names.

10.6.4 Start timeout

prov-app-start-timeout (default 2m) is the container start and image pull timeout written in the service definition, per app or for the cluster.

10.6.5 How an app is monitored

An app with a route is probed on each route: over TCP for a tcp route, with an HTTP GET for an http or https route. The HTTP check reads / and expects 200 unless the route carries a monitor block in the template:

[[deployment.routes]]
  cname = "{{app.name}}.{{name}}.{{config.cloud18SubDomain}}-{{config.cloud18SubDomainZone}}.{{config.cloud18Domain}}.cloud18.io"
  port = "8080"
  primary = true
  protocol = "https"
  [deployment.routes.monitor]
    path = "/api/method/ping"
    expect-status = "200"

Use it when the root of the application is slow (ERPNext's / renders a full page through its backend) or answers something else than 200 to an anonymous request (the S3 API of RustFS answers 403 at its root, its health endpoint /health answers 200). The route is named by its public URL and the destination port behind the gateway (https://name -> :8080): the gateway terminates TLS on 443, the destination port is reached on the cluster network only.

An app without a route lives on the cluster network only. By default it is up when app-port answers a TCP connect. A background process that listens on nothing (ERPNext's worker and scheduler) declares app-monitor-mode = "ping": the monitor sends one ICMP echo to the app host instead (IPv4 when the name has an A record, else IPv6 over ICMPv6), and app-port only identifies the app. The setting is on the app page (Overview, "Monitor mode") and on the API (POST /api/clusters/{cluster}/apps/{id}/settings/actions/set/app-monitor-mode/ping). A failed echo opens the APPERR009 state; a monitor that cannot open an ICMP socket says so instead of reporting the app up. The echo proves that the instance's network is up: on OpenSVC the address belongs to the pause container of the instance, so a background process that exited while its instance stays up is not detected by the ping.

A web service declares app-monitor-mode = "https" (or "http"): the monitor sends a GET to https://<app-host>:<app-port><app-monitor-path> and expects app-monitor-expect-status (default path /, default status 200, a comma list such as 200,204). Redirects are followed and the final status is compared: a page that answers 302 to its login page is checked with the login page's status (usually 200). This is the check that sees a service that accepts connections and never answers, which port and ping report up. No answer within the monitor timeout opens APPERR001, a wrong status APPERR002. A full app-host name (status.example.com) is called as is; a short one on the app's cluster address. app-monitor-mode = "https-cert" adds a verified look at the certificate: an invalid certificate (chain, name), or one that expires within app-monitor-cert-expire-days (default 14), opens APPERR010 and shows the app in warning: an invalid certificate never makes the app fail, the service still answers. Use it with the public name the certificate carries as the app host. The app page shows the path, expected status and certificate fields for these modes.

How often an app is checked

Each check runs on its own interval. A check that passes runs again once its interval has elapsed, and its last result stands in between. A check that fails runs at every monitoring tick (monitoring-ticker, 2 s by default) until it passes again, so a failure is confirmed in a few seconds (app-error-debounce-threshold checks in a row) and a recovery is seen at the next tick.

Setting Where Applies to Default
interval in the route's monitor block template [deployment.routes.monitor], app page Routes (Edit, "Check interval"), API apps/{id}/deployment/routes/index/{i}/monitor.interval/modify the route's local and external check, HTTP and TCP the app's app-monitor-interval
app-monitor-interval app page Overview ("Check interval"), API apps/{id}/settings/actions/set/app-monitor-interval/{seconds} the check of an app without a route, and the default of its routes 60 s

Values are in seconds; 0 or empty means the default. Checks run on monitoring ticks, so an interval is rounded up to the next tick (with a 30 s tick, a 45 s interval runs every 60 s). A check runs within a second of its interval, so an interval at or below the monitoring tick checks at every tick, as before this setting existed. An HTTP route's external check goes through the public gateway, so a short interval adds one request to the gateway's access log at each run.

  [deployment.routes.monitor]
    path = "/health"
    interval = 30

Monitor-only apps

To watch a web service that runs elsewhere (another gateway, another team's server), add an app with Monitor only ticked in the add dialog (Cluster, Add monitor, type app): host and port, no image, no template. Then set its monitor mode (usually https), path and expected status on the app page. A monitor-only app is never provisioned (provision, unprovision, start, stop and restart refuse it), reserves no plan unit, and publishes nothing on the cluster's gateway. Its states and alerts are those of any app.

Restarting an app with several instances

On OpenSVC v3, Restart on the app page (or POST /api/clusters/{cluster}/apps/{app}/actions/restart) restarts every instance of the app; the orchestrator does it one instance at a time, each after the previous one is back up, so a flex app keeps serving. .../actions/restart/{node} restarts the instance on that node only.

10.6.6 Database engines from templates (PostgreSQL)

A template with prov-app-configurator = "postgres" deploys a database server of the cluster, not an app next to it: the server is monitored, counted in the database plan, and driven by the cluster's actions (rolling restart, upgrade, switchover, failover). The template gives the deployment definition only; the shape follows the cluster topology:

  • replication-active-passive: one instance that moves with its data, the template's shape, a failover service on every agent over a DRBD volume.
  • replication-master-slave-pg-stream and -pg-logical: each member has its own data and never moves. The cluster places it on one agent (round-robin over prov-db-agents) and renders its volume on the cluster's data pool (prov-db-volume-data), whatever pool the template names, like a MariaDB server.

Existing services keep their shape until reprovisioned.

10.6.7 Example: ERPNext as linked apps

The erpnext templates deploy ERPNext as one container per process, no shared volume: two valkey apps (erpnext-cache, erpnext-queue), erpnext-backend (owns the database through app.db and the site, creates it on the empty schema at first start), erpnext-worker, erpnext-scheduler, erpnext-websocket, erpnext-frontend (the https route). The site files live on the S3 provider (rustfs1), the database is the cluster's MariaDB through the proxy. Add them in that order with those names.