Core concepts
Read this once. Everything else — the CLI, app definitions, flow docs — assumes this vocabulary.
The composite identity: tenant × stack × environment
Section titled “The composite identity: tenant × stack × environment”The most important thing to understand about SHC is that a running app instance is identified by a tuple, not a name:
(tenant, stack, environment) → one deploymentThis tuple is SHC’s universal primary key. File paths, vault scopes, DNS names, audit events, backup archives — all of them derive from it. See architecture/core/deployment-identity for the full rationale; here’s what you need to know day-to-day:
- Tenant — the isolation boundary. Different tenants never see
each other’s data, secrets, stacks, or events. Default:
default. - Stack — a named instance of an app. You can install the same app multiple times with different stack names.
- Environment — a variant of one stack (e.g.
prod,staging,dev). Fully isolated data, config, secrets. Default:default.
shc install nextcloud # (default, nextcloud, default)shc install nextcloud --stack cloud # (default, cloud, default)shc install nextcloud -s cloud -e prod # (default, cloud, prod)shc install nextcloud -s cloud -e staging # (default, cloud, staging)Those four installs are four completely separate deployments, with separate databases, separate vault entries, separate DNS names.
Cluster, nodes, runtimes
Section titled “Cluster, nodes, runtimes”Cluster
Section titled “Cluster”A single-node SHC keeps its control-plane database as a plain
SQLite file. The moment a second node joins, the database is
promoted into the raft engine — an external rqlited process
the daemon supervises — and replicated across the cluster. The
promotion is automatic; there is no manual migration step.
- Single-node: plain SQLite, all services run on one host
- Multi-node: 3 or 5 raft voters over mTLS + Nebula, Raft consensus, horizontal scale-out of stacks
A node is one host running the SHC daemon.
shc node ls # list nodesshc node ls -v # verbose, full IDsshc install postgres --node <node-id> # pin a stack to a specific nodeNodes have attributes that affect scheduling: their runtime (compose / swarm / podman), their architecture (amd64/arm64), their linked storage providers, and whether they’re currently draining. See operations/cluster-setup.
Container runtimes
Section titled “Container runtimes”SHC can drive three container runtimes
(modules/node/types.go::Runtime):
| Runtime | Cross-node networking | Typical use |
|---|---|---|
compose | Single-node only (bridge network) | Single-host deployments, dev clusters, or per-node local stacks |
swarm | Yes — Docker overlay networks | Multi-node production where stacks may span hosts |
podman | Single-node only | Rootless deployments |
One cluster may have a mix — some nodes running compose, others
running swarm. A stack’s runtime is picked by matching the app’s
requires.runtimes against what the target node supports. See
app-definition — requirements.
Strict per-node runtime
Section titled “Strict per-node runtime”Each node has exactly one declared runtime, set at shc init --runtime …
(for the cluster-init node) or shc node join --runtime … (for
workers). At install time SHC enforces that the stack’s runtime matches
the target node’s runtime:
shc install <app> --node X --runtime swarmrequiresX.runtime == "swarm". Otherwise the install fails withX400955RTMPIN.shc install <app> --runtime swarm(no--node) places the stack via the docker swarm scheduler against the pool of nodes whereruntime == "swarm". An empty pool fails withX400956NOSWPL.shc install <app> --node X --runtime composerequiresX.runtime == "compose".
This is a hard constraint, not a hint: a compose-declared node cannot host swarm services even if its docker daemon happens to be swarm-attached, and vice versa. The intent is that a node’s declared runtime represents operator intent, not the docker daemon’s incidental state.
To move a stack across runtimes, shc clone and shc move accept
--runtime <new>; the operation re-renders the source onto the new
runtime and lands the result on a node that supports it. This is the
supported runtime-conversion path — there is no in-place flip.
Apps, stacks, deployments
Section titled “Apps, stacks, deployments”Three similar-sounding words, three different things. Keep them straight:
-
App — a template. Lives on disk under
apps/<name>/. Never runs anything. Just definition files (app.yaml,compose.yaml, plug/socket files). Shipped with SHC or pulled from a repo. -
Stack — the installation record of an app. Created by
shc install <app> --stack <name>. One stack = one row in the control-plane database’sstackstable. The stack carries the install-time variables, which node it’s assigned to, which runtime, … -
Deployment — the running state of a stack in a specific environment. One stack + one environment = one deployment. This is the
(tenant, stack, environment)tuple above. Backups, snapshots, compose state all key off the deployment.
A stack with environments default, staging, and prod is one
stack row but three deployments.
Tenants
Section titled “Tenants”Tenants are hard isolation boundaries:
- Separate database records scoped to the tenant
- Separate vault scopes (so
ref+vault://db_password?scope=tenanton tenant A resolves differently from the same reference on tenant B) - Separate networks — each tenant gets its own Docker bridge/overlay
- Separate audit trails
- API permissions scoped by tenant (see security.md)
The default tenant is created at shc init and is the implicit
target when you don’t pass --tenant. For true multi-tenant use:
shc tenant create acmeshc install nextcloud --tenant acme --stack cloudThe cross-tenant --all-tenants flag on a few admin commands is
guarded by an explicit admin.tenants permission — see
security.md.
Integrations: plugs & sockets
Section titled “Integrations: plugs & sockets”Apps connect to each other via sockets (providers) and plugs
(consumers). Install a consumer with -i provider and SHC runs a
multi-phase job pipeline to provision the connection (create a
database, grant a user, issue an API key, …).
shc install wordpress --stack blog -i postgres# ^^^^^^^^ provider stackThis is SHC’s killer feature — the orchestration isn’t limited to “start these containers in the right order,” it can actually provision the things apps need from each other.
Full model: plugs-and-sockets.
Related concept — vApps — let one stack host many sub-resources (many databases in one Postgres, many realms in one Keycloak). See vapps.
Jobs, hooks, crons
Section titled “Jobs, hooks, crons”When SHC needs to run something — a DB migration, a backup converter, a cache flush, an integration provisioning step — it uses a job.
A job is defined in <name>.job.yaml inside an app directory. Its
context selects where it runs: in an already-running service
(service), in a new one-shot container (container), as an HTTP
call (http), as a GraphQL mutation (graphql), as a pipeline, or
as a Playwright browser script.
Three things trigger jobs:
| Trigger | Fires when | Docs |
|---|---|---|
| Hooks | Lifecycle events (pre_install, post_install, pre_backup, …) | lifecycle-hooks |
| Crons | Cron schedule defined in app.yaml | architecture/modules/schedule |
| Integration phases | discovery, integration, post_deployment on connect. unintegration and cleanup are declared but have no runner — disconnect and uninstall execute neither | plugs-and-sockets — integration phases |
Run history is persisted as rows in the control-plane database —
see shc job runs <stack>.
A repo is a source of apps. SHC ships with a builtin repo of
~60 apps installed with the package — the daemon catalogs them
automatically. You can register additional repos pointing at:
- A git repository (
https://github.com/you/shc-apps) - A local bare git remote (
git+file:///srv/mirrors/apps.git) - An OCI image (not yet)
Repos are remote-only: file-path repos were removed. To install an app
from a local directory, install it directly
(shc install ./path/to/app) instead of registering the directory.
shc repo listshc repo add my-apps https://github.com/acme/shc-appsshc install acme-internal-tool --repo my-appsSee app source resolution for the full search order.
Storage & vault
Section titled “Storage & vault”Two orthogonal systems you’ll touch as an operator:
-
Storage providers — where stack volumes and backup data live. Types:
local,nfs,cephfs. See operations/storage-providers. -
Vault — where secrets live. Referenced from YAML via
ref+vault://key?scope=.... Backend providers include a built-in store encrypted in the control-plane database plus 10 external options. See operations/vault.
Events & audit
Section titled “Events & audit”Two streams of observability data, both sharing the same event infrastructure:
-
Events — lifecycle signals (
A200100STKINSwhen a stack installs,V500320STKERRwhen it errors). Subscribe via Server-Sent Events throughshc eventsor the HTTP API. See flow: event lifecycle. -
Audit trail — who did what, when. Every vault read, every permission-checked API call, every tenant admin action.
shc events --audit. See architecture/modules/audit.
Both streams can be pushed to an external collector (default: OpenObserve) — see operations/observability.
Quick reference: what owns what
Section titled “Quick reference: what owns what”tenant├── stacks (one app install → one stack)│ ├── deployments (one stack × environment → one deployment)│ │ ├── compose services (containers running the app)│ │ ├── volumes (data on storage provider)│ │ ├── vault entries (secrets at deployment scope)│ │ ├── integrations (edges to other stacks' sockets)│ │ └── vApp instances (sub-resources: realms, DBs, buckets, …)│ └── stack-scoped state (hooks, crons, variables, …)├── tenant-scoped vault entries (?scope=tenant)├── audit trail└── events
cluster├── nodes (one host = one node)├── control-plane database (SQLite; raft-replicated via rqlited in cluster mode)├── nebula mesh (optional encrypted overlay)├── storage providers (local / nfs / cephfs)└── vault (providers: local + 10 external)Where to go next
Section titled “Where to go next”- I want to run my first stack: Quick start
- I want to understand the install pipeline in depth: Architecture: install flow
- I want to stand up a multi-node cluster: Multi-node cluster setup
- I want to write my own app: App definition
- I want to know exactly how tenant isolation works: Security