Skip to content
SHC Docs

App commands

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.

Terminal window
shc install nextcloud
shc install nextcloud --stack cloud --environment prod
shc install wordpress --stack blog --var title="My Blog"
shc install keycloak/realm --stack keycloak/test-realm
shc install nextcloud --host cloud.example.com --tls
shc install nextcloud --host cloud.example.com --tls dns --dns-provider cloudflare

Install apps directly from a local filesystem path:

Terminal window
# From relative path
shc install ./apps/myapp --stack myapp
# From absolute path
shc install /home/user/apps/myapp --stack myapp
# With configuration
shc install ./apps/myapp --stack myapp --var admin_email=admin@example.com

Install apps directly from a git repository:

Terminal window
# From GitHub (HTTPS)
shc install https://github.com/user/myapp --stack myapp
# From GitLab
shc install https://gitlab.com/user/myapp --stack myapp
# From Bitbucket
shc install https://bitbucket.org/user/myapp --stack myapp
# From SSH URL
shc install git@github.com:user/myapp.git --stack myapp

Specify a branch, tag, or commit hash:

Terminal window
# From a specific tag
shc install https://github.com/user/myapp --ref v1.0.0 --stack myapp
# From a branch
shc install https://github.com/user/myapp --ref main --stack myapp
shc install https://github.com/user/myapp --ref feature/new-ui --stack myapp
# From a commit hash
shc install https://github.com/user/myapp --ref abc123def456 --stack myapp

For repositories containing multiple apps, specify the subdirectory:

Terminal window
# Install app from subdirectory
shc install https://github.com/user/monorepo --path apps/myapp --stack myapp
# Combined with ref
shc install https://github.com/user/monorepo --path apps/myapp --ref v2.0.0 --stack myapp

Provide authentication for private repositories:

Terminal window
# With GitHub personal access token
shc install https://github.com/user/private-app --token ghp_xxxxxxxxxxxx --stack myapp
# With GitLab access token
shc install https://gitlab.com/user/private-app --token glpat-xxxxxxxxxxxx --stack myapp

Note: Tokens are used only for the clone operation and are not stored.

FlagShortDescription
--stack-sStack name (defaults to app name)
--environment-eEnvironment name
--node-nTarget node for deployment
--repo-rRepository name (if app exists in multiple repos)
--varSet configuration variable (key=value)
--presetApply a preset bundle of variables, format NAME[@VERSION] (see Preset commands)
--hostHost route, [service[:port]@]hostname[/path]; selectors must resolve to one Compose service and container port
--tlsEnable HTTPS (http-01 or dns challenge)
--dns-providerDNS provider for dns-01 challenge
--refGit reference (branch, tag, or commit)
--pathSubdirectory path within repository
--tokenAuthentication token for private repos
--authBasic auth credentials (user:password) for private archives
--port-pExpose 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-overrideOverride HTTP hostname (e.g., ‘example.com:web:8080’)
--runtimeTarget runtime (‘compose’, ‘swarm’, ‘podman’)
--vault-providerOverride vault provider for this deployment
--storageStorage provider (e.g., ‘local’, ‘nfs’, ‘cephfs’)
--cpuCPU quota (e.g., ‘500m’, ‘1’)
--memoryMemory quota (e.g., ‘512m’, ‘1g’)
--diskDisk quota (e.g., ‘10g’, ‘500m’)
--integrate-iIntegrate with existing stack (repeatable)
--from-backupRestore data from backup file after install
--quiet-qSuppress 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.

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).

The canonical --host grammar is [service[:port]@]hostname[/path]:

  • --host huly.example.com selects the deployment’s only ingress candidate.
  • --host nginx@huly.example.com selects service nginx.
  • --host :80@huly.example.com selects container port 80.
  • --host nginx:80@huly.example.com selects service nginx, container port 80.
  • --host api:8080@example.com/v1 also routes only the /v1 path 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).

  • --host creates 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-migration host=target override (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 scoped dns.public_address config key (the operator entrypoint override, ip or hostname; for exposure=internet the same key is only the deprecated last-rung fallback) > the system traefik’s web endpoint, inferred: its persisted 80/443 conn@ 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=internet with nothing public falls back to the deprecated dns.public_address read, 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.
  • --tls enables HTTPS with Traefik ACME. HTTP-01 is the default challenge.
  • --tls dns selects DNS-01 when a DNS provider is configured.

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.

Terminal window
shc uninstall nextcloud
shc uninstall nextcloud --environment prod
shc uninstall nextcloud --yes --no-backup

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).

Terminal window
# Re-render/re-deploy with current settings
shc update mystack
# Update to a specific version (repository apps)
shc update nextcloud 29.0.0
# Update configuration
shc update nextcloud --var new_setting=value
# Adjust per-service quotas and replica counts
shc update webapp --cpu web=1.0 --memory web=512Mi --scale web=3
FlagShortDescription
--varSet a variable override (key=value); key! removes it
--cpuPer-service CPU quota (service=value)
--memoryPer-service memory quota (service=value)
--diskDisk quota
--scalePer-service replica count (service=count)
--hostAdd or replace an HTTP route; hostname[/path]! removes one
--port-pAdd or replace a port mapping; [conn@]external[/udp]! removes one
--integrate-iWire an integration ([plug=]provider[@env], plug=@connection); a bare <ref>! disconnects — one token may drop several edges, see Removing a persisted override
--ha / --no-haToggle high-availability mode
--gated / --openToggle the Keycloak SSO gate
--presetRe-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.

--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:

Terminal window
shc update myblog --var theme! # back to the app default
shc update myblog --host blog.example.com! # drop a persisted route
shc update myblog --host blog.example.com/promo! # drop one path route
shc update traefik --port 'hetzner@80!' # drop an LB port mapping
shc update nextcloud -i keycloak! # disconnect an integration

A -i disconnect token resolves to exactly one of three handles, tried in this precedence order — they are not alternatives tried together:

  1. the consumer plug-i sso!;
  2. the connection-i aws-primary! (the @ sigil form, -i '@aws-primary!', works too);
  3. 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=keycloak and auth=keycloak, -i keycloak! drops both edges;
  • on m1=mail@prod and m2=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:

Terminal window
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 --var value is never a marker. --var key=value is structurally a set because of the =, so --var password=hunter2! sets the password to hunter2!.
  • 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 install refuses the marker outright (X400002VALINP) — there is no persisted state to remove on a fresh install.
  • A -i disconnect drops the edge from the re-rendered compose, from the deployment’s persisted state and from the integration table, so it stops being replayed. It removes no provider-side resource: the jobs.unintegration: phase authored in *.socket.yaml / *.plug.yaml has 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.

List deployed stacks:

Terminal window
shc ls
shc ls --tenant acme --environment prod
shc ls --status running
shc ls --output json

The output includes the source column showing where each stack was installed from:

  • repo:appname - From repository catalog
  • git:url - From git URL
  • path:/path - From local path

View deployment + service health (replaces the removed shc ps):

Terminal window
shc status nextcloud
shc -o json status nextcloud

Rename or relocate stack:

Terminal window
shc move nextcloud nextcloud-v2 # rename (same node)
shc move nextcloud nextcloud --node node-abc123 # move to another node
shc move nextcloud nextcloud --all-connected # move with integrations

Duplicate a stack:

Terminal window
shc clone myapp myapp-clone
shc clone myapp myapp-prod --node node-abc123
shc clone myapp myapp-test --environment staging

Scale service replicas. The replica count is mandatory via -r/--replicas, the service is positional:

Terminal window
shc -s nextcloud scale -r 3 web
shc -s wordpress scale -r 0 worker
shc -s nextcloud -e prod scale -r 5 web

Validate app definitions:

Terminal window
shc validate myapp
shc validate /path/to/app.yaml
shc validate ./apps/myapp
shc validate https://github.com/user/myapp