Flow: app install
Step-by-step trace of what happens when an operator runs
shc install <app> --stack <name>. Source of truth:
modules/app/install_async.go’s
RunInstallWithObserver (orchestrating modules/app/install_runner.go);
the numbered steps below correspond to the code there.
This page covers the success path and the rollback path.
For the lifecycle-hook contract, see
docs/app-development/lifecycle-hooks.md.
For upgrade / uninstall flows, see
upgrade.md and uninstall.md.
Actors
Section titled “Actors”| Component | Role |
|---|---|
CLI (shc install) | Accepts args, forwards to HTTP API |
API (POST /api/method/shc.app.install) | Thin router wrapper |
StackService.Install (app/service.go) | Entry; delegates to install_flow.InstallStack |
InstallStack (app/install_flow.go) | The pipeline below |
app_def = AppDefinition | Parsed app.yaml |
| Docker / Swarm | Actually runs containers |
Success path
Section titled “Success path”1. Name validation (lines 102-109)
Section titled “1. Name validation (lines 102-109)”If a stack_name is supplied, validate it is lowercase-alphanumeric
with hyphens. Invalid names raise X400069INVNME immediately — nothing
else happens.
2. App lookup (lines 111-144)
Section titled “2. App lookup (lines 111-144)”Resolve app_name to a row in AppModel:
- Split
parent/childsyntax if present (used by VApps). - Scope the search to repos visible to the current tenant + environment:
- the caller’s own
tenant_nameor thesystemtenant - the caller’s
environmentor thesystemenvironment
- the caller’s own
- Optionally filter by
repo_name(--repoflag) andapp_version(--versionflag). - Order by repo priority ascending, version descending.
- First row wins; no match ⇒
X404002APPNOF.
This is how SHC decides which app named foo to install when
multiple repos contain one — tenant-scoped repos override the system
repos by repo priority.
3. VApp fast-path (lines 146-154)
Section titled “3. VApp fast-path (lines 146-154)”If the resolved app has is_vapp=True, delegate to
service._install_vapp(...). VApps don’t create their own stack — they
extend another running stack (e.g. the acme VApp adds TLS to a
running Traefik). See
docs/app-development/vapps.md and
the app module for the VApp model.
For the rest of this flow, assume a regular app.
4. Uniqueness check (lines 158-160)
Section titled “4. Uniqueness check (lines 158-160)”service.get_stack(name, environment) — if a stack with this name
already exists in this environment, raise X409003STKEXS. SHC does not
auto-increment or replace.
5. Directory scaffolding (lines 162-170)
Section titled “5. Directory scaffolding (lines 162-170)”Two directories are created via fsops.Mkdir(...):
paths.deployment_dir(tenant, name, environment)— operational state for this deployment (rendered compose,vars.yaml, logs).paths.app_dir(tenant, name, environment)— the app’s source files, copied from the app repo.
App sources are resolved via GetAppSourcePath(db, app) (handles
git / path / archive repos — see repo module)
and copied in with CopyAppFiles.
6. Parse AppDefinition (line 172)
Section titled “6. Parse AppDefinition (line 172)”LoadAppDefinition(appDir) parses the app’s app.yaml into a
typed AppDefinition — the schema, hooks, platform constraints,
integration declarations, and vars.
7. Platform constraint check (lines 174-182)
Section titled “7. Platform constraint check (lines 174-182)”If the caller pinned a node (--node), verify it satisfies the app’s
platform constraints (OS, arch, required features). Raises if not.
8. Variable merging (lines 184-186)
Section titled “8. Variable merging (lines 184-186)”MergeVariables(appDef, userVars, integrations) combines:
- Default
varvalues fromapp.yaml - User-supplied
--var key=valueoverrides - Values derived from declared integrations (plugs ⇄ sockets)
This is a declarative merge — the result is a flat
map[string]string; no rendering yet.
9. Pre-render for resource extraction (lines 188-196)
Section titled “9. Pre-render for resource extraction (lines 188-196)”PreRenderCompose(...) renders the compose file with the merged
variables just enough to extract resource hints. This is needed so
the quota / capacity checks below see the real numbers (CPU, memory)
the app will ask Docker for.
10. Quota enforcement (lines 198-212)
Section titled “10. Quota enforcement (lines 198-212)”ExtractComposeResources reads cpu / memory reservations out of
the pre-rendered compose, plus disk from the --disk flag, and
EnforceStackCreationQuota either allows the install to proceed or
raises a quota error. See the quota module.
11. Swarm placement preflight (lines 214-254)
Section titled “11. Swarm placement preflight (lines 214-254)”If runtime=swarm, do three additional checks before any DB write:
- Pool capacity: sum of
cpu/memoryacross all replicas fits the total free capacity of the swarm. - Largest replica fits some node: not just “fits in total” but “can actually be placed somewhere.”
- Live bin-pack:
CheckSwarmCanPlaceReplicastalks to the swarm and runs a real placement preflight.
Any failure raises X403014SWMFRG with a free-capacity report so the
operator sees why.
12. Per-node capacity (lines 256-262)
Section titled “12. Per-node capacity (lines 256-262)”If the install is pinned to a specific node (--node), run a
compose-runtime capacity check (EnforceNodeCapacity). Swarm uses
its own scheduler for this; this is only for compose.
13. Storage provider resolution (lines 273-279)
Section titled “13. Storage provider resolution (lines 273-279)”service.resolveStorage(...) picks the storage provider
(local, NFS, a cluster-aware provider) based on: --storage-provider
flag, app.yaml hints, pinning, and runtime. See the
storage module.
14. Create the DeploymentModel row (lines 284-304)
Section titled “14. Create the DeploymentModel row (lines 284-304)”A DeploymentModel struct is constructed with status=CREATED and
every resolved field (app name/version, runtime, storage provider, HA
flag, scale overrides, deployment fingerprint). Not yet committed.
15. Rollback flag setup (lines 305-313)
Section titled “15. Rollback flag setup (lines 305-313)”Six boolean flags are declared — stackRowCommitted,
ingressAllocated, hostModePortsAllocated, composeRendered,
composeStarted, finalizeCompleted. Each flag corresponds to a
piece of externally-visible state that could leak on a mid-flight
failure. The error-handling block in §18 uses them to know what needs undoing.
16. Forward pipeline (lines 315-424)
Section titled “16. Forward pipeline (lines 315-424)”From here everything runs with deferred rollback that triggers on any error.
16.1 DB commit (lines 316-319)
Section titled “16.1 DB commit (lines 316-319)”db.Create(&stack)stackRowCommitted = true. Now the cluster can see “an install is in
flight” even if the process crashes.
16.2 Persist vars.yaml (lines 321-332)
Section titled “16.2 Persist vars.yaml (lines 321-332)”The merged variables are written to
deployment_dir/vars.yaml. This is what subsequent upgrades and
re-renders read.
16.3 Ingress allocation (lines 334-336)
Section titled “16.3 Ingress allocation (lines 334-336)”If the user passed --host foo.example.com / --port 443/tcp,
service.allocateIngress reserves the host/port in the ingress
bookkeeping table and configures DNS records. Sets
ingressAllocated = true.
16.4 Host-mode port allocation (lines 338-340)
Section titled “16.4 Host-mode port allocation (lines 338-340)”For services using network_mode: host, reserve ports against the
owning node so no two stacks collide. Sets
hostModePortsAllocated = true.
16.5 Integrations (lines 342-350)
Section titled “16.5 Integrations (lines 342-350)”If --integrate <other-stack> was passed, CreateIntegrations
wires the plug/socket connections into persistent state so that later
renders can resolve integration+…:// references.
16.6 Required-plug validation (line 352)
Section titled “16.6 Required-plug validation (line 352)”ValidateRequiredPlugs(appDef, integrations, mergedVariables) —
if app.yaml declares a plug as required: true and no integration
or inline var satisfied it, raise. This is the “you can’t install
Keycloak without a Postgres somewhere” check.
16.7 Real render (lines 354-370)
Section titled “16.7 Real render (lines 354-370)”Two passes with the real variables now:
renderComposeFile— writes the finalcompose.yaml(or swarm stack file) that Docker will consume.renderYAMLFiles— writes any other templated YAML files the app declares (e.g.*.job.yaml,*.exporter.yaml).
Sets composeRendered = true.
16.8 Remote sync (line 372)
Section titled “16.8 Remote sync (line 372)”If the stack is owned by another node, syncDeploymentToRemote
ships the rendered files over the internal API to the owning node
before the next step tries to run docker there.
16.9 pre_install hook (line 374)
Section titled “16.9 pre_install hook (line 374)”User-defined pre_install hook (shell job inside a throwaway
container, or host command — see
lifecycle hooks). A
non-zero exit rolls the whole install back.
16.10 Docker network creation (lines 376-382)
Section titled “16.10 Docker network creation (lines 376-382)”EnsureNetworksExist creates the per-tenant overlay network
and per-stack private network if they don’t already exist (idempotent
— see network module).
16.11 Compose up (lines 384-419)
Section titled “16.11 Compose up (lines 384-419)”Three cases:
- If
LoadComposeDatareturned a parsed struct, useComposeUp(...)which goes through the structured compose pipeline. - If the app dir has a compose file but no parsed data (edge case),
fall back to
composeCommand(name, env, "up", "-d", …). - If the app has no compose file at all (job-only apps), treat it as success with no-op.
On failure ⇒ X500907STKFAL with compose exit code + stderr + stdout
embedded.
On success ⇒ composeStarted = true.
16.12 Swarm post-deploy poll (lines 405-406)
Section titled “16.12 Swarm post-deploy poll (lines 405-406)”If runtime=swarm, poll until replicas converge (or timeout). This
catches swarm-mode deploys where ComposeUp returns success but
actual containers never reach running.
16.13 Finalize (lines 407-410)
Section titled “16.13 Finalize (lines 407-410)”finalizeSuccessfulInstall:
- Runs the
post_installhook (unlessskipPostInstall=true). - Registers the stack’s jobs with the scheduler.
- Installs any cron schedules declared in
app.yaml. - Updates tenant usage counters.
- Publishes the
A200100STKINSaudit event.
Sets finalizeCompleted = true.
16.14 Commit + return (lines 421-424)
Section titled “16.14 Commit + return (lines 421-424)”Final db.Save() persists any state changes from finalize, then
return the DeploymentModel.
Rollback path
Section titled “Rollback path”The deferred rollback handler catches any error — including programming errors. The rollback runs in reverse order of forward progress so we don’t, say, delete the stack row before the ingress release has a chance to look up the allocations.
Key property: every cleanup step is individually guarded. A
failure in one rollback sub-step is logged (X200003OPFAIL with an
operation tag) and does not prevent later rollback steps from
running. The original install error is returned at the end.
Order of rollback
Section titled “Order of rollback”- Dump container logs (if
composeStarted) — so the operator can see why the container failed, even after the stack is gone. - Usage counter decrement (if
finalizeCompleted). - Unregister jobs (if
finalizeCompleted). - Delete schedules (if
finalizeCompleted). - Release ingress (if
ingressAllocatedorhostModePortsAllocated). Queried from the DB rather than from the flags, becauseallocateIngressmay have persisted rows before returning an error. Must run before the stack row delete — CASCADE FKs would otherwise wipe the allocation rows. rollbackFailedInstall(the shared helper): compose down, network removal, integration cleanup, stack row delete.- Deployment / app dir cleanup (if
composeRenderedorstackRowCommitted): remove any rendered compose / copied sources the shared helper missed.
If noRollback=true was passed (--no-rollback), skip steps 2-7 —
the operator has chosen to leave the mess for manual inspection. This
is rarely used outside of debugging.
What rollback does NOT undo
Section titled “What rollback does NOT undo”pre_installhook side-effects: if the hook already ran a migration against an external database, SHC does not know about it.- External DNS propagation: the ingress release asks the DNS provider to remove the record, but TTLs are what they are.
- Vault secrets: secrets written during
pre_installstay. Callers that need rollback-safe secret allocation should write under the stack’s vault namespace (which is deleted by the shared helper).
Events emitted
Section titled “Events emitted”| Step | Event | Meaning |
|---|---|---|
| 16.13 (success) | A200100STKINS | Stack installed successfully |
| 16.11 (compose failure) | V500907STKFAL | Install failed at compose-up |
| Rollback sub-step failure | X200003OPFAIL with operation tag | Cleanup step failed (non-fatal) |
Full reference: errors.md.
See also
Section titled “See also”- Architecture: app module — owns this pipeline
- Architecture: quota module — step 10
- Architecture: storage module — step 13
- Architecture: network module — step 16.10
- Lifecycle hooks — steps 16.9 / 16.13
- Plugs & sockets — steps 8, 16.5, 16.6