Skip to content
SHC Docs

Tech stack

SHC is a Go CLI + daemon that manages Docker Compose deployments across single or multiple nodes. The CLI (shc) and the daemon (shcd) are two binaries built from cmd/shc/ and cmd/shcd/; the rest of the code lives under internal/. This document defines the technical choices for all aspects of the codebase.


ComponentChoiceNotes
LanguageGo 1.25+Compiled binaries, pure Go (CGO_ENABLED=0)
Build toolchainasdf + gomake build produces bin/shc + bin/shcd
Bundled binariesrqlitedFetched to vendor/rqlite/ by make vendor/rqlite
ComponentChoiceNotes
CLI Frameworkspf13/cobraStandard Go command tree
Terminal Outputcharmbracelet/lipgloss + bubbleteaStyled text + Bubbletea TUI for shc hud
Shell Completioncobra-generatedBash/Zsh/Fish completion support
ComponentChoiceNotes
HTTP Routergo-chiLightweight chi router + middleware
HTTP Servernet/http with chiUDS + TCP listeners served from one daemon
Validationgo-playground/validator/v10validate: struct tags + custom validators
ComponentChoiceNotes
Lifecycle Managersubsystems/lifecycleCentral orchestrator for all server subsystems
Subsystem Protocollifecycle.SubsystemUniform interface: Name(), Start(), Stop(), Health()
Fail-FastsystemdService crashes bring down server; systemd restarts
Leader Electionraft (leader_hub)The raft leader id (rqlited) drives the leader signal

See daemon-manager for details.

ComponentChoiceNotes
DatabaseSQLite / rqlitePlain file + WAL at {state}/shc.db single-node; raft-replicated rqlite in clusters
Drivermodernc.org/sqlite (pure Go)Registered as the sqlite sql driver; the pool swaps to shc-rqlite at boot when {state}/engine.json exists
Distributedrqlite (external rqlited process)Supervised by the daemon (core/rqlited); SQLite with Raft consensus
Query layerent ORM + per-module reposent-generated client (ent/) over the shared db pool; one *StackRepo / *NodeRepo per module wraps it
MigrationsAtlas (ariga/atlas)Versioned SQL under migrations/, integrity hash in atlas.sum

Database modes:

  • Single-node: plain SQLite (no raft, zero coordination)
  • Cluster: external rqlited raft engine (automatic promotion when the first shc node join enrolls)
ComponentChoiceNotes
Container RuntimeDockerIndustry standard
OrchestrationDocker ComposeSimple multi-container apps
Docker SDKgithub.com/docker/docker go SDKTalks to the host docker daemon
Swarm SupportOptionalFor multi-node clustering

shc/
├── cmd/
│ ├── shc/main.go # CLI binary (cobra root)
│ └── shcd/main.go # daemon binary
├── internal/
│ ├── core/ # cross-module primitives
│ │ ├── coded/ # X######CCCC error machinery
│ │ ├── api/ # chi helpers, manifest, SSE
│ │ ├── db/ # *db.Pool, atlas migrations, scan helpers
│ │ ├── log/ # slog wrapper + audit
│ │ ├── paths/ # XDG-aware filesystem layout
│ │ └── registry/ # central router + command + error registries
│ ├── daemon/ # daemon boot + wiring
│ ├── modules/ # feature modules (one per domain)
│ │ ├── app/
│ │ ├── backup/
│ │ ├── cluster/
│ │ ├── vault/
│ │ └── ...
│ └── subsystems/ # lifecycle-managed background workers
│ ├── lifecycle/ # supervisor + resolver
│ └── impl/ # rqlite_engine, collectors, queue, http, ...
├── apps/ # App catalog (sample apps)
│ ├── apps.json
│ ├── keycloak/
│ │ ├── app.yaml
│ │ ├── compose.yaml
│ │ ├── vars.yaml
│ │ └── realm.vapp.yaml
│ └── ...
├── migrations/ # Atlas SQL migrations + atlas.sum
├── tests/ # unit/, integration/, parity/, e2e/, proxmox/
└── docs/ # Documentation

Each module under modules/{module}/ (ce module; ee-only modules under modules/) follows a consistent structure:

modules/{module}/
├── service.go # *Service struct + methods (business logic)
├── router.go # HTTP routes via chi
├── types.go # request/response Go structs + validate tags
├── errors.go # X######CCCCCC factories + Spec slice + init()
├── events.go # event handler registrations (where applicable)
├── register.go # init() side-effects (router builder, command builder, runtime singleton)
├── service_test.go # unit tests against fakes
├── router_test.go
└── commands/ # cobra subcommand surface (if module exposes CLI verbs)
├── main.go
└── ...

ToolPurposeConfig
gofmtCode formatting(canonical)
golangci-lintLint suite (forbidigo, gocritic, staticcheck, errcheck, …).golangci.yml
shfmtShell-script formatting (apps, tests)make format
  • forbidigo rejects errors.New and fmt.Errorf outside test files — every error path must surface a *coded.CodedError.
  • staticcheck, errcheck, unused, gocritic, goimports all on.
  • Tests are exempt from forbidigo’s stricter rules but not from gofmt/unused/staticcheck.

ToolPurpose
testing stdlibTest runner
testing.TB.ContextPer-test cancellable context
httptestIn-process HTTP server for handler tests
Hand-rolled fakesCross-module seam stand-ins
Terminal window
# Run all tests
make test/unit
# Run with coverage
go test -coverprofile=coverage.out ./internal/...
go tool cover -html=coverage.out -o coverage.html
# Run in parallel (default: GOMAXPROCS) — disable with -p=1
go test -p=1 ./...
# Race detector (integration suite turns this on by default)
go test -race ./internal/...
ToolPurpose
BatsBash Automated Testing System
ProxmoxMulti-node VM e2e

SurfaceComponent
Logstyped wrapper in core/log/ — WARN+ requires a *coded.CodedError (raw slog.Warn/slog.Error are lint-banned); structured JSON + audit class + event-bus fan-out
MetricsOpenTelemetry meters + scoped registries (modules/metrics/)
TracingOpenTelemetry traces with W3C traceparent propagation
EventsInternal event bus (modules/events/) + SSE for clients
// modules/metrics/definitions.go — sample shapes
backupTotal := svc.RecordCounter(ctx, "shc_backup_total", 1)
activeStacks := svc.RecordGauge(ctx, "shc_active_stacks", float64(n))
opDuration := svc.RecordHistogram(ctx, "shc_operation_duration_seconds", elapsed.Seconds())

Runtime dependencies (selected, see go.mod for the full list):

modernc.org/sqlite // pure-Go sqlite driver
github.com/spf13/cobra // CLI framework
github.com/go-chi/chi/v5 // HTTP router
github.com/go-playground/validator/v10 // input validation
go.opentelemetry.io/otel // metrics + tracing
github.com/charmbracelet/bubbletea // HUD TUI
github.com/charmbracelet/lipgloss // terminal styling
ariga.io/atlas // migrations
github.com/moby/docker // docker SDK
go.yaml.in/yaml/v3 // YAML parsing

Dev tooling lives in go.mod build constraints; gofmt, golangci-lint, and bats are installed via asdf at make prepare.


MethodCommandUse case
.deb/.rpm/.apkmake package && sudo {dpkg,rpm,apk} -i dist/shc_*_linux_amd64.<fmt>Production
Direct Installmake installDevelopment/testing

Package structure (built by goreleaser/nfpm)

Section titled “Package structure (built by goreleaser/nfpm)”
ComponentPath / approach
Build SystemGoReleaser + nfpm (.goreleaser.yaml)
Go binaries/usr/bin/shc, /usr/bin/shcd (static, CGO_ENABLED=0)
Bundled rqlited/usr/lib/shc/bin/rqlited (spawned by the daemon in cluster mode)
systemd unit/usr/lib/systemd/system/shc.service (static, FHS paths)
Group/Dirssystemd-sysusers (shc.sysusers.conf), systemd-tmpfiles (shc.tmpfiles.conf)
Scriptletsscripts/{postinstall,preremove,postremove}.sh

Legacy Python code has been removed from the repo — Go is the single implementation.


These technologies are explicitly not used:

TechnologyReason
React/Vue/SvelteNo web UI
CSS/TailwindNo frontend
JavaScript/TypeScriptPure Go project
PostgreSQL/MySQLSQLite/rqlite is sufficient
RedisNot needed for current scope
KubernetesDocker Compose focused
GraphQLREST API is simpler
Generated mocksHand-rolled fakes are cheaper to maintain