Skip to content
SHC Docs

Data model

Deployments are identified by a composite key:

tenant + stack + environment = unique deployment
  • Tenant: Isolation boundary (e.g., default, acme-corp)
  • Stack: The installed app instance (e.g., nextcloud, my-blog)
  • Environment: Deployment context (e.g., default, prod, staging)

The deployment_id (used for Docker Compose project naming) is derived:

deployment_id = "{tenant}_{environment}_{stack}"
# Example: "default_prod_nextcloud"

See Deployment Identity for more details.

Environments provide deployment isolation within a stack. Common environment names:

EnvironmentPurpose
defaultDefault when not specified
prodProduction workloads
stagingPre-production testing
devDevelopment and experimentation
Terminal window
# Override config for specific environment
shc config set backup.retention_days 14 --scope deployment
shc config set resources.memory_limit 4G --scope deployment
AspectShared Between Environments?
App definitionYes (same app.yaml, compose.yaml)
Data volumesNo (each env has own mounts/ directory)
ConfigInherited but can be overridden per-env
Vault secretsCan be scoped to environment or shared
Docker networkNo (each env has own network)
Container namesNo (prefixed with deployment_id)

SHC uses a minimal set of directories, following XDG conventions on Linux:

DirectoryLinuxmacOS (Darwin)BSD
state_home/var/lib/shc/var/lib/shc/var/lib/shc
cache_home/var/cache/shc/var/cache/shc/var/cache/shc
runtime_dir/run/shc/var/run/shc/var/run/shc
logs_dir/var/log/shc/var/log/shc/var/log/shc
DirectoryAll Platforms (XDG)
config_home~/.config/shc
state_home~/.local/share/shc
cache_home~/.cache/shc
runtime_dir$XDG_RUNTIME_DIR/shc or /tmp/shc-{uid}
logs_dir~/.local/share/shc/logs

When running from source (go.mod detected), all paths consolidate under the project:

{project_root}/.shc/
├── state/
├── cache/
├── run/
└── logs/
  • Config paths - Configuration is stored in the database, not files
  • Backup paths - Backups go to Restic or user-specified paths
  • Deployment state paths - Handled by StorageService (provider-dependent)

# State (database only - deployment state handled by StorageService)
{state_home}/
├── shc.db # SQLite database (single-node)
├── engine.json # engine marker (present in cluster mode)
├── node.id # this node's cluster member id
└── rqlite/ # rqlited data (cluster mode: raft log + snapshots)
# Logs (scoped hierarchy, each node has its own logs)
{logs_dir}/
├── shc.log # Global log
└── tenants/{tenant}/
└── stacks/{stack}/
└── environments/{environment}/
└── shc.log # Deployment log
# Runtime (ephemeral, cleared on reboot)
{runtime_dir}/
├── shc.pid # Server PID file
└── shc.sock # Unix socket
# Cache (can be deleted anytime)
{cache_home}/
└── git/ # Cloned git repositories
└── {url_hash}/ # Hashed repo URL

Deployment state is managed by storage providers, not paths.go. The path depends on the configured storage provider:

# Storage provider base path varies:
# - Local: /var/lib/shc/deployments (or configured path)
# - NFS: /mnt/shc/nfs (managed NFS mount)
# - CephFS: /mnt/shc/cephfs (managed CephFS mount)
{provider_base_path}/{tenant}/{stack}/{environment}/
├── compose/ # Generated compose files
│ ├── compose.yaml
│ └── vars.yaml
├── mounts/ # DATA mounts (BACKED UP)
│ ├── pgdata/
│ └── uploads/
├── files/ # FILE mounts (regenerated on install)
└── app/ # App definition (copied from repo)
├── app.yaml
└── compose.yaml
ModeDatabasePath
Single-nodeSQLite (pure-Go, file + WAL){state_home}/shc.db
Clusterrqlite (external rqlited process){state_home}/rqlite/

Cluster mode is detected by the presence of the {state_home}/engine.json marker; the daemon then supervises rqlited and swaps its pool to the raft-replicated engine at boot.

All configuration is stored in the database, not in files:

  • Single-node: Config table in SQLite
  • Cluster: Config table in rqlite (raft-replicated across nodes)

Config supports scoped cascade lookup: global → tenant → stack (most specific wins).

Backups are not stored in a fixed location. They either:

  • Go to Restic (configured via backup.repo)
  • Go to an explicit path specified by the user

FilePathPurpose
SQLite DB{state_home}/shc.dbDatabase (single-node)
rqlite dir{state_home}/rqlite/Database (cluster)
Engine marker{state_home}/engine.jsonCluster-engine flag
PID file{runtime_dir}/shc.pidServer process ID
Unix socket{runtime_dir}/shc.sockCLI-server communication
Global log{logs_dir}/shc.logSystem-wide log
Deployment log{logs_dir}/tenants/.../shc.logPer-deployment log
Git cache{cache_home}/git/Cloned git repositories

TypeExampleLocationBacked Up
DATApgdata:/var/lib/postgresqlmounts/pgdata/✅ Yes
FILE./config.yaml:/app/config.yamlfiles/config.yaml❌ No (part of app)
RUNTIMEtmp:/cache/tmp/...❌ No (ephemeral)
SYSTEM/var/run/docker.sockunchanged❌ No (host path)

The mounts/ directory contains persistent application data (DATA mounts). This is what gets backed up.

The files/ directory contains config files copied from the app definition (FILE mounts). These are regenerated on install/upgrade, not backed up.

Both mounts/ and files/ are within the deployment state directory, managed by the storage provider.