App commands
shc install
Section titled “shc install”Install an app as a new stack. Apps can be installed from three sources: the app catalog (repository), a local filesystem path, or a git URL.
Basic Installation (from Repository)
Section titled “Basic Installation (from Repository)”shc install nextcloudshc install nextcloud --stack cloud --environment prodshc install wordpress --stack blog --var title="My Blog"shc install keycloak/realm --stack keycloak/test-realmshc install nextcloud --host cloud.example.com --tlsshc install nextcloud --host cloud.example.com --tls dns --dns-provider cloudflareInstall from Local Path
Section titled “Install from Local Path”Install apps directly from a local filesystem path:
# From relative pathshc install ./apps/myapp --stack myapp
# From absolute pathshc install /home/user/apps/myapp --stack myapp
# With configurationshc install ./apps/myapp --stack myapp --var admin_email=admin@example.comInstall from Git URL
Section titled “Install from Git URL”Install apps directly from a git repository:
# From GitHub (HTTPS)shc install https://github.com/user/myapp --stack myapp
# From GitLabshc install https://gitlab.com/user/myapp --stack myapp
# From Bitbucketshc install https://bitbucket.org/user/myapp --stack myapp
# From SSH URLshc install git@github.com:user/myapp.git --stack myappInstall with Git Reference
Section titled “Install with Git Reference”Specify a branch, tag, or commit hash:
# From a specific tagshc install https://github.com/user/myapp --ref v1.0.0 --stack myapp
# From a branchshc install https://github.com/user/myapp --ref main --stack myappshc install https://github.com/user/myapp --ref feature/new-ui --stack myapp
# From a commit hashshc install https://github.com/user/myapp --ref abc123def456 --stack myappInstall from Monorepo Subdirectory
Section titled “Install from Monorepo Subdirectory”For repositories containing multiple apps, specify the subdirectory:
# Install app from subdirectoryshc install https://github.com/user/monorepo --path apps/myapp --stack myapp
# Combined with refshc install https://github.com/user/monorepo --path apps/myapp --ref v2.0.0 --stack myappInstall from Private Repository
Section titled “Install from Private Repository”Provide authentication for private repositories:
# With GitHub personal access tokenshc install https://github.com/user/private-app --token ghp_xxxxxxxxxxxx --stack myapp
# With GitLab access tokenshc install https://gitlab.com/user/private-app --token glpat-xxxxxxxxxxxx --stack myappNote: Tokens are used only for the clone operation and are not stored.
Install Flags
Section titled “Install Flags”| Flag | Short | Description |
|---|---|---|
--stack | -s | Stack name (defaults to app name) |
--environment | -e | Environment name |
--node | -n | Target node for deployment |
--repo | -r | Repository name (if app exists in multiple repos) |
--var | Set configuration variable (key=value) | |
--preset | Apply a preset bundle of variables, format NAME[@VERSION] (see Preset commands) | |
--host | Host route, [service[:port]@]hostname[/path]; selectors must resolve to one Compose service and container port | |
--tls | Enable HTTPS (http-01 or dns challenge) | |
--dns-provider | DNS provider for dns-01 challenge | |
--ref | Git reference (branch, tag, or commit) | |
--path | Subdirectory path within repository | |
--token | Authentication token for private repos | |
--auth | Basic auth credentials (user:password) for private archives | |
--port | -p | Expose a port: [conn@]external[:service][:container][/udp]. Plain form publishes the host port on the node (8080, 8080:web, 8080:web:80); conn@ exposes it through the named proxy-capable connection’s load balancer with an auto-assigned node port (mylb@443:web:8443, lb@53:dns:53/udp). Persisted: a bare update replays the set, --port adds/replaces, and on shc update a trailing ! removes (see Removing a persisted override) |
--host-override | Override HTTP hostname (e.g., ‘example.com:web:8080’) | |
--runtime | Target runtime (‘compose’, ‘swarm’, ‘podman’) | |
--vault-provider | Override vault provider for this deployment | |
--storage | Storage provider (e.g., ‘local’, ‘nfs’, ‘cephfs’) | |
--cpu | CPU quota (e.g., ‘500m’, ‘1’) | |
--memory | Memory quota (e.g., ‘512m’, ‘1g’) | |
--disk | Disk quota (e.g., ‘10g’, ‘500m’) | |
--integrate | -i | Integrate with existing stack (repeatable) |
--from-backup | Restore data from backup file after install | |
--quiet | -q | Suppress progress output |
shc install refuses a trailing ! on --var, --host, --port and
-i/--integrate (X400002VALINP): the marker denotes a removal, and a fresh
install has no persisted override to remove. It is an shc update grammar —
see Removing a persisted override.
The telemetry integration: -i otel=OTEL
Section titled “The telemetry integration: -i otel=OTEL”OTEL is a token, not a stack — nothing needs to be installed first. It
wires the app’s traces and metrics (never logs — container stdout is
collected platform-side already) into SHC’s telemetry fabric: the render
injects the standard OTEL_EXPORTER_OTLP_* environment pointing at the
node-local collector, and the platform stamps tenant/stack/environment
onto everything the app emits — the app itself is never told its tenant.
Works with any app whose runtime honours the OTel SDK environment
variables; apps that don’t emit OTel data simply ignore the env. The edge
persists like any other integration (shc update <stack> -i otel!
disconnects; it cannot be qualified with @env).
Host Route Grammar
Section titled “Host Route Grammar”The canonical --host grammar is [service[:port]@]hostname[/path]:
--host huly.example.comselects the deployment’s only ingress candidate.--host nginx@huly.example.comselects servicenginx.--host :80@huly.example.comselects container port80.--host nginx:80@huly.example.comselects servicenginx, container port80.--host api:8080@example.com/v1also routes only the/v1path prefix.
SHC enumerates TCP container-port candidates from Compose expose and ports
targets, applies the supplied selectors, and requires exactly one candidate.
It does not choose a first service or port and does not default to port 80.
The old service:host form is a deprecated legacy read kept only so
existing deployment state keeps resolving; canonical writes always use the
@ selector grammar. The old host=target DNS-override suffix is
refused on new input (X400995HSTTGT) — drop the suffix and the DNS
record follows the system ingress endpoint. Deployments installed under
the old grammar keep their persisted target (read-only replay).
DNS and TLS Behavior
Section titled “DNS and TLS Behavior”--hostcreates DNS records using the selected provider for the deployment scope.- Record target ladder (top wins; an L7 host’s DNS follows the SYSTEM traefik’s public L4 endpoint — the workload’s own
conn@mappings never affect it): a persisted pre-migrationhost=targetoverride (read-only replay; the grammar is refused on new input) > an operator-fixed proxy endpoint threaded by a recover replay > for non-internet exposures only, the scopeddns.public_addressconfig key (the operator entrypoint override, ip or hostname; forexposure=internetthe same key is only the deprecated last-rung fallback) > the system traefik’s web endpoint, inferred: its persisted 80/443conn@mappings naming ONE connection use that load balancer’s endpoints (a still-provisioning VIP defers the write and the reconciler retries; mixed connections are ambiguous and defer), else with no web connection the placed node’s public address (internet exposure; a private/loopback IP is treated as absent) > the exposure-split last rung:exposure=internetwith nothing public falls back to the deprecateddns.public_addressread, else DEFERS — internet DNS fails closed and never publishes a LAN/advertise address — while other exposures keep the historical LAN fallback (the traefik placement node’s internal address, else all swarm nodes / the advertise address). Record types are auto-classified: IPv4 → A, IPv6 → AAAA, hostname → CNAME. --tlsenables HTTPS with Traefik ACME. HTTP-01 is the default challenge.--tls dnsselects DNS-01 when a DNS provider is configured.
shc uninstall
Section titled “shc uninstall”Remove a stack. Uninstall is always a full teardown — containers,
volumes, and persistent data are all removed (there is no data-preserving
mode). A safety backup is taken first; pass --no-backup to skip it.
shc uninstall nextcloudshc uninstall nextcloud --environment prodshc uninstall nextcloud --yes --no-backupshc update
Section titled “shc update”Update a deployment in place: bump the app version, change variables, add or
remove hosts and ports, adjust quotas, and rewire integrations. The version is
a positional argument (there is no --version flag).
Basic Update
Section titled “Basic Update”# Re-render/re-deploy with current settingsshc update mystack
# Update to a specific version (repository apps)shc update nextcloud 29.0.0
# Update configurationshc update nextcloud --var new_setting=value
# Adjust per-service quotas and replica countsshc update webapp --cpu web=1.0 --memory web=512Mi --scale web=3Update Flags
Section titled “Update Flags”| Flag | Short | Description |
|---|---|---|
--var | Set a variable override (key=value); key! removes it | |
--cpu | Per-service CPU quota (service=value) | |
--memory | Per-service memory quota (service=value) | |
--disk | Disk quota | |
--scale | Per-service replica count (service=count) | |
--host | Add or replace an HTTP route; hostname[/path]! removes one | |
--port | -p | Add or replace a port mapping; [conn@]external[/udp]! removes one |
--integrate | -i | Wire an integration ([plug=]provider[@env], plug=@connection); a bare <ref>! disconnects — one token may drop several edges, see Removing a persisted override |
--ha / --no-ha | Toggle high-availability mode | |
--gated / --open | Toggle the Keycloak SSO gate | |
--preset | Re-resolve preset to NAME[@VERSION] and merge resolved vars (see Preset commands) |
Source repoint (
--from/--ref/--path/--token) is accepted on the command line but not yet wired — those flags are currently a no-op.
Removing a persisted override
Section titled “Removing a persisted override”--var, --host, --port and -i/--integrate all persist as deployment
state: a bare shc update <stack> replays whatever the deployment was
installed (or last updated) with. Removal is a trailing ! on the same
flag that sets the value — there are no separate removal flags:
shc update myblog --var theme! # back to the app defaultshc update myblog --host blog.example.com! # drop a persisted routeshc update myblog --host blog.example.com/promo! # drop one path routeshc update traefik --port 'hetzner@80!' # drop an LB port mappingshc update nextcloud -i keycloak! # disconnect an integrationA -i disconnect token resolves to exactly one of three handles, tried in
this precedence order — they are not alternatives tried together:
- the consumer plug —
-i sso!; - the connection —
-i aws-primary!(the@sigil form,-i '@aws-primary!', works too); - the provider stack —
-i keycloak!, the all-caps magic spelling-i KEYCLOAK!, or narrowed to one environment as-i keycloak@prod!.
The first handle that matches any live edge wins, and the token then drops every edge matching under that handle alone; the remaining handles are never consulted. A bare provider token is therefore a stack-wide disconnect, not a single-edge one:
- on a deployment wired
sso=keycloakandauth=keycloak,-i keycloak!drops both edges; - on
m1=mail@prodandm2=mail@dev,-i mail!drops both.
Narrow it to one edge by naming the plug (-i m1!), or across environments
with provider@env (-i mail@prod!). An env-qualified token matches only an
edge that persisted with that environment — -i mail@prod! against a plain
mail edge matches nothing and fails.
Because the plug is tried first, a plug whose name happens to equal a stack
name shadows the stack handle. On kc2=keycloak plus keycloak=keycloak,
-i keycloak! resolves to the plug and drops only keycloak=keycloak; reach
the other edge by its own plug (-i kc2!).
A token that matches no live edge fails the update with
X400971INTNOF (the message lists the wired edges). It is not a silent no-op,
so re-running a disconnect that already ran fails the second time — scripts
that must be idempotent should tolerate that code. The marker rides the bare
form only: -i plug=provider! is refused with X400002VALINP rather than
guessed at.
To remove several at once, either repeat the flag or comma-join the tokens —
--host, --port and -i comma-split, so both spellings arrive as separate
removals:
shc update myblog --host a.example.com! --host b.example.com!shc update myblog --host 'a.example.com!,b.example.com!'Notes:
- A
!inside a--varvalue is never a marker.--var key=valueis structurally a set because of the=, so--var password=hunter2!sets the password tohunter2!. - Quoting is only a shell concern, and only at an interactive bash or zsh
prompt, where
!followed by a comma is a history reference. Use single quotes there — double quotes do not help, because zsh expands history inside them. Scripts, CI and the Terraform provider run non-interactive shells with history expansion off entirely, so no quoting is needed. - The marker is stripped by the CLI’s parse layer before any spec parser runs,
so a
!is never legal inside a hostname, route path, port or provider name. shc installrefuses the marker outright (X400002VALINP) — there is no persisted state to remove on a fresh install.- A
-idisconnect drops the edge from the re-rendered compose, from the deployment’s persisted state and from theintegrationtable, so it stops being replayed. It removes no provider-side resource: thejobs.unintegration:phase authored in*.socket.yaml/*.plug.yamlhas no runner anywhere in the tree — the job scanner lists it (shc job ls) but nothing executes it, on disconnect or on uninstall — so a keycloak edge’s OIDC client is never deleted by either verb and must be cleaned up by hand. A disconnect also does not revoke the sub-credential minted behind a connection-backed edge (-i s3=@aws-primary); that revoke does exist, but it is wired to uninstall only (and is skipped there for a same-key move, which hands the live mint to the moved deployment), so a disconnect leaves the scoped IAM user behind.
shc ls
Section titled “shc ls”List deployed stacks:
shc lsshc ls --tenant acme --environment prodshc ls --status runningshc ls --output jsonThe output includes the source column showing where each stack was installed from:
repo:appname- From repository cataloggit:url- From git URLpath:/path- From local path
shc status
Section titled “shc status”View deployment + service health (replaces the removed shc ps):
shc status nextcloudshc -o json status nextcloudshc move
Section titled “shc move”Rename or relocate stack:
shc move nextcloud nextcloud-v2 # rename (same node)shc move nextcloud nextcloud --node node-abc123 # move to another nodeshc move nextcloud nextcloud --all-connected # move with integrationsshc clone
Section titled “shc clone”Duplicate a stack:
shc clone myapp myapp-cloneshc clone myapp myapp-prod --node node-abc123shc clone myapp myapp-test --environment stagingshc scale
Section titled “shc scale”Scale service replicas. The replica count is mandatory via -r/--replicas,
the service is positional:
shc -s nextcloud scale -r 3 webshc -s wordpress scale -r 0 workershc -s nextcloud -e prod scale -r 5 webshc validate
Section titled “shc validate”Validate app definitions:
shc validate myappshc validate /path/to/app.yamlshc validate ./apps/myappshc validate https://github.com/user/myapp