Skip to content
SHC Docs

Virtual apps (vApps)

A vApp is a sub-resource that lives inside another stack rather than as a top-level stack of its own. vApps let a parent app expose “per-tenant instances” of something it provides — a Keycloak realm, a Postgres database, a MinIO bucket, a MariaDB database — without each instance being a full stack with its own compose file, networks, and volumes.

Source of truth (ce module): modules/app/vapp_runner.go (the synchronous vapp install path and the *.vapp.yaml / *.vsocket.yaml / *.vplug.yaml / *.vvars.yaml file contract), with the cleanup guard in modules/app/vapp_cleanup_lint.go and modules/app/vapp_cleanup_report.go.


Use a vApp when:

  • The resource is naturally owned by a parent stack (a realm belongs to a Keycloak, a bucket belongs to a MinIO).
  • You want many instances of it on one parent — a dozen realms on one Keycloak stack, a dozen databases on one Postgres.
  • It has no containers of its own — it’s configuration applied to containers the parent already runs.

Use a stack when:

  • It has its own compose file and runs independently.
  • It could plausibly live on a different node from the thing it integrates with.

vApps do not start any containers. They exist as rows in the database plus jobs that configure state inside parent-stack services.


vApps and their instances use slash notation throughout:

ConceptFormatExample
App name in the catalog<parent-app>/<vapp>keycloak/realm, postgres/database
Stack name (parent + instance)<parent-stack>/<instance>my-keycloak/test-realm, dbs/blog

The stack name’s left-hand side identifies the host stack (must already be installed); the right-hand side names this specific instance.


Files shipped with the postgres app (apps/postgres/):

apps/postgres/
├── app.yaml
├── compose.yaml # the actual postgres service
├── postgres.socket.yaml # admin-level socket
├── database.vapp.yaml # ← THE vAPP: a sub-resource "database"
├── database.vsocket.yaml # ← per-instance socket consumers plug into
├── database.vplug.yaml # ← vPlug definition (how the vApp talks to postgres)
└── database.vvars.yaml # ← variables the vApp auto-generates

database.vapp.yaml — the vApp definition

Section titled “database.vapp.yaml — the vApp definition”
version: "1.0"
name: database
app: postgres
type: vapp
description: "PostgreSQL database with dedicated user"
vars:
database: Database name
username: Database username
password@secret: Database password

This declares that the postgres app offers a database sub-resource with those three variables. app: postgres identifies the parent. type: vapp is the discriminator.

database.vsocket.yaml — the per-instance socket

Section titled “database.vsocket.yaml — the per-instance socket”
schema:
plug:
database@string!: Database name to create
username@string!: Database username
password@secret!: Database password
extensions@json: Optional list of PostgreSQL extensions
socket:
hostname@string!: PostgreSQL hostname
port@port!: PostgreSQL port
admin_user@string!: Admin username
admin_password@secret!: Admin password
data:
hostname: "{{ environment }}.postgres.{{ stack }}"
port: 5432
admin_user: "{{ vars.username }}"
admin_password: "{{ vars.password }}"
jobs:
integration:
- name: vapp-create-database
context: service
service: postgres
command: |
# create DB + user inside the parent postgres container
...

A vSocket is almost identical to a regular socket, but it’s scoped per vApp instance — when wordpress plugs into a specific instance of postgres/database, it gets that instance’s database name and password, not the parent postgres admin credentials.

database.vvars.yaml — auto-generated variables

Section titled “database.vvars.yaml — auto-generated variables”
username: "!ref+autogen?length=16&charset=alphanum"
password: "!ref+autogen?length=32"
database: "{{ vapp_instance }}"

Auto-generated values ensure every instance gets unique credentials without the operator having to supply them.


Terminal window
# Parent stack already installed:
shc install postgres --stack dbs
# Install vApp instances into it — slash on both sides:
shc install postgres/database --stack dbs/blog
shc install postgres/database --stack dbs/analytics
shc install postgres/database --stack dbs/wiki

postgres/database names a vApp in the postgres app. --stack dbs/blog says “host stack dbs, instance name blog”. The full stack identifier becomes dbs/blog.

Install flow (InstallVApp in vapp_flow.go):

  1. Validates the parent stack exists (X400005PARNOF if not).
  2. Validates the instance name is unique on that parent (X409003STKEXS on collision).
  3. Loads the vApp definition and vars; merges auto-generated and user-supplied values.
  4. Persists a vApp-instance row keyed by (tenant_name, parent_deployment, vapp_name, instance_name) with parent_deployment_id set on the new DeploymentModel row.
  5. Runs the vSocket’s integration phase jobs inside parent-stack services to provision the resource (create the database, create the user, grant privileges).

vApps do not run compose up — all their work happens through jobs against services the parent stack already runs.

vApp instances appear as regular stacks in the listings:

Terminal window
$ shc ls
NAME APP ENV STATUS
dbs postgres default running
dbs/blog postgres/database default running
dbs/analytics postgres/database default running
my-keycloak keycloak default running
my-keycloak/qa keycloak/realm default running

A vApp instance presents itself as a regular stack. The consumer plugs into it the same way it would any other stack — by stack name:

Terminal window
# WordPress's `postgres` plug connects to the `blog` instance
# of postgres on the `dbs` stack:
shc install wordpress --stack blog \
-i postgres=dbs/blog

The <plugname>=<stack-name> form names the target stack. Because vApp instances are stacks, the same syntax works — the consumer doesn’t need to know it’s connecting to a vApp rather than a top-level stack. The consumer’s integrations.postgres.* template context is populated from the vApp instance’s vSocket data, which contains per-instance credentials, not parent-stack admin credentials.


OperationWhat happens
shc uninstall <parent>vApp instances are uninstalled first (unintegrated + cleaned up) before the parent’s compose-down
shc uninstall <parent>/<instance>Only that instance’s unintegration phase runs — parent untouched
shc backup create <parent>vApp instance data is included in the parent’s backup
shc backup restore <parent>vApp instances are restored; any vSocket post_restore integration jobs replay
shc clone <parent>vApp instances are copied with the parent stack; source instances remain
shc move <parent>vApp instances move with the parent

vApp unintegration is driven by RunVAppUnintegration in vapp_flow.go.


vApps vs plugs-and-sockets — when do I use which?

Section titled “vApps vs plugs-and-sockets — when do I use which?”
ScenarioUse
I want a blog to connect to an existing Postgres databasea plug on the blog, a socket on Postgres
I want the blog to get its own private database inside a shared Postgresa vApp instance of postgres/database, then a plug on the blog connecting to that instance’s stack
I want each tenant of my multi-tenant app to get its own Keycloak realma vApp instance of keycloak/realm per tenant against a shared Keycloak
I want one Redis shared across 20 stacksa plain socket on Redis, plugs on each consumer

Rule of thumb: if the thing being provisioned has per-consumer state (a database, a realm, a bucket), use a vApp. If it’s a shared cache / shared service with no per-consumer state to create, use a plain socket.


  • shc search — shows both regular apps and vApps (vApp names contain a slash, e.g. keycloak/realm); pass a query to filter across both.
  • shc ls — lists installed stacks; vApp instances show as <parent>/<instance> with their parent’s app name as the app.
  • shc search --vapp — filters to vApp catalog entries only.

The catalog index (apps.json) lists vApps with a parent_app and type: vapp discriminator. vApps are auto-discovered from *.vapp.yaml files within app directories during repo sync.


vApps share endpoints with regular apps and stacks:

OperationEndpointNotes
List appsGET /api/resource/AppFilter vApps with ?vapp=true or ?parent_app=X
Get appGET /api/resource/App/{name}{name} may be keycloak/realm
InstallPOST /api/method/shc.stack.installPass the parent/instance pair via stack_name (slash-separated)
List stacksGET /api/resource/Stack?environment=XIncludes vApp instances; each carries parent_stack
UninstallDELETE /api/resource/Stack/{name}On a parent, cascades to child vApp instances

Schema-wise, AppModel carries parent_app (str?) and vapp (bool); DeploymentModel carries parent_deployment_id (FK with cascade delete) and instance_name (str?).


Minimal layout:

apps/myapp/
├── app.yaml
├── compose.yaml
├── myapp.socket.yaml # optional — admin-level socket
├── resource.vapp.yaml # the vApp definition
├── resource.vsocket.yaml # per-instance socket
└── resource.vvars.yaml # auto-generated per-instance vars

resource.vapp.yaml:

name: resource
app: myapp
type: vapp
version: "1.0"
description: A per-instance resource inside myapp
vars:
instance_name@string!: Name of the instance
credential@secret: Generated credential

resource.vsocket.yaml: exactly like a regular *.socket.yaml, with jobs.integration calling context: service against services the parent app already runs. Do not declare context: container with your own image — a vApp doesn’t own any containers. Instead, call the parent’s admin CLI via docker exec into a parent service.