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.
When to use a vApp vs a stack
Section titled “When to use a vApp vs a stack”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.
Naming
Section titled “Naming”vApps and their instances use slash notation throughout:
| Concept | Format | Example |
|---|---|---|
| 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.
Example: the Postgres database vApp
Section titled “Example: the Postgres database vApp”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-generatesdatabase.vapp.yaml — the vApp definition
Section titled “database.vapp.yaml — the vApp definition”version: "1.0"name: databaseapp: postgrestype: vappdescription: "PostgreSQL database with dedicated user"
vars: database: Database name username: Database username password@secret: Database passwordThis 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.
Installing a vApp instance
Section titled “Installing a vApp instance”# Parent stack already installed:shc install postgres --stack dbs
# Install vApp instances into it — slash on both sides:shc install postgres/database --stack dbs/blogshc install postgres/database --stack dbs/analyticsshc install postgres/database --stack dbs/wikipostgres/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):
- Validates the parent stack exists (
X400005PARNOFif not). - Validates the instance name is unique on that parent
(
X409003STKEXSon collision). - Loads the vApp definition and vars; merges auto-generated and user-supplied values.
- Persists a vApp-instance row keyed by
(tenant_name, parent_deployment, vapp_name, instance_name)withparent_deployment_idset on the newDeploymentModelrow. - Runs the vSocket’s
integrationphase 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:
$ shc lsNAME APP ENV STATUSdbs postgres default runningdbs/blog postgres/database default runningdbs/analytics postgres/database default runningmy-keycloak keycloak default runningmy-keycloak/qa keycloak/realm default runningConnecting a consumer to a vApp instance
Section titled “Connecting a consumer to a vApp instance”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:
# WordPress's `postgres` plug connects to the `blog` instance# of postgres on the `dbs` stack:shc install wordpress --stack blog \ -i postgres=dbs/blogThe <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.
Lifecycle semantics
Section titled “Lifecycle semantics”| Operation | What 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?”| Scenario | Use |
|---|---|
| I want a blog to connect to an existing Postgres database | a plug on the blog, a socket on Postgres |
| I want the blog to get its own private database inside a shared Postgres | a 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 realm | a vApp instance of keycloak/realm per tenant against a shared Keycloak |
| I want one Redis shared across 20 stacks | a 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.
Catalog and listings
Section titled “Catalog and listings”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.
API surface
Section titled “API surface”vApps share endpoints with regular apps and stacks:
| Operation | Endpoint | Notes |
|---|---|---|
| List apps | GET /api/resource/App | Filter vApps with ?vapp=true or ?parent_app=X |
| Get app | GET /api/resource/App/{name} | {name} may be keycloak/realm |
| Install | POST /api/method/shc.stack.install | Pass the parent/instance pair via stack_name (slash-separated) |
| List stacks | GET /api/resource/Stack?environment=X | Includes vApp instances; each carries parent_stack |
| Uninstall | DELETE /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?).
Writing your own vApp
Section titled “Writing your own vApp”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 varsresource.vapp.yaml:
name: resourceapp: myapptype: vappversion: "1.0"description: A per-instance resource inside myapp
vars: instance_name@string!: Name of the instance credential@secret: Generated credentialresource.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.
Related
Section titled “Related”- App definition — where
VAppDefinitionfits in the broader schema - Plugs & sockets — the broader integration model vApps build on
- Lifecycle hooks — vApp jobs follow the same job-execution contract
- Architecture: app module —
VAppExecutorandVAppContextinternals