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.
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.
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.
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.
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.
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 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.
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.