Skip to content
SHC Docs

Host lifecycle: prepare, install, init, run

This document maps every entry point that touches a host to the responsibilities it owns. The goal is one concern, one owner — no duplication, no hidden state changes.

There are three distinct concerns when bringing SHC up on a host:

  1. “Make this host capable of running SHC” — system packages, Docker installed, runtime libs.
  2. “Make SHC available as a binary on this system” — file placement, user/group, service unit.
  3. “Make this host into an SHC node” — Docker daemon checks, Swarm init, certs, cluster bootstrap, system apps.

These are owned by different entry points depending on whether SHC is installed via package manager (deb/rpm/apk/brew) or run from source (make dev, make install).

ConcernOwned by (deb/rpm/apk)Owned by (brew)Owned by (source)
Make host capableDepends: / Requires: / depends=depends_onmake prepare
Make SHC availablePackage payloadBrew Cellarmake install
Make host an SHC nodeshc init + daemon ensure_node_ready(same)(same)

The cluster bootstrap (shc init, daemon startup post-init) is always SHC’s own code, never the package manager’s. Packages stay minimal: file placement, user creation, service unit registration. They never mutate Docker config or initialize a Swarm.

make install supports DESTDIR for callers that want to stage files into a directory before packaging (e.g. downstream distro maintainers building from source). The ifdef DESTDIR gate splits the two paths:

  • DESTDIR set → staging mode: place files into $DESTDIR/..., don’t install runtime deps, don’t enable services.
  • DESTDIR unset → direct install: also pull prepare deps and start the systemd unit.

Goreleaser packaging (.deb/.rpm/.apk) does not call make install. It builds binaries directly and stages files into the package via nfpms.contents; scriptlets under scripts/ run on the end-user’s machine after install.

make prepare installs runtime system packages (Docker, system libs, the asdf toolchain) — what’s needed to build and run SHC. Currently Debian/Ubuntu only (apt install docker-ce ...). Idempotent.

It does not:

  • Initialize Swarm (that’s shc initmake dev auto-runs it on first invocation)
  • Mutate /etc/docker/daemon.json (read-only validation happens in ensure_node_ready)
  • Create cluster certs, bootstrap files, or system apps

shc init is the only thing that initializes the cluster (Swarm, certs, node_bootstrap.json). make dev auto-runs shc init --yes on first invocation when no bootstrap.json exists, so contributors run make prepare && make dev and never need a separate “dev-cluster bootstrap” step.

PhaseWhenAuthority to mutate host?Calls ensure_node_ready?
Debian/RHEL/Alpine package installOperator runs apt install shc (or dnf / apk add)No — depends pull runtime packages, payload places files, postinst creates user + reloads systemdNo
make prepareContributor / source operator preparing runtimeYes — installs system packages (Docker, libs, goreleaser)No
make install (direct)Source-built prod installYes (pulls prepare deps)No
make install DESTDIR=...Manual staging into a treeNo (DESTDIR set)No
make packageBuild .deb/.rpm/.apk via goreleaserNo (snapshot mode, build only)No
shc initFirst-time cluster bootstrap on a hostYes — runs ensure_node_ready, then generates certs, bootstrap.json, Swarm, system appsYes
daemon startup (post-init)systemd / make dev after shc initYes — re-verifies state idempotentlyYes
daemon startup (pre-init)First-ever daemon boot, no bootstrap.jsonNo — limited mode, only serves /api/init/*No
make devContributor running daemon from sourceYes — auto-runs shc init --yes if no bootstrap, then starts daemonYes (via shc init and daemon startup)
Test environmentbats / Go test fixture spins ephemeral SHCYes (sandboxed under SHC_BASE_DIR)Yes

Defined in modules/cluster/ (ce module; the join/enroll overlays live in this repo’s modules/cluster/). Idempotent. Called from shc init, shc join, daemon post-init startup.

StepLinux
Verify Docker daemon is reachable (docker info)
Validate /etc/docker/daemon.json (no iptables: false)
Apply sysctl tuning (/etc/sysctl.d/99-shc.conf)✓ (root only, skipped in dev)

It does not initialize Swarm, generate certs, or write node_bootstrap.json — those are caller responsibilities (shc init does them after ensure_node_ready returns).

SHC’s HTTP listener and the callback hostname containers use to reach it can differ. See modules/cluster/network.go (ce module).

get_bind_address(): persisted bind_ip (post-init) → docker bridge gateway → 0.0.0.0. get_callback_host(): persisted bind_ip → docker bridge gateway → first private IP. On Linux these collapse to one value — containers on local bridges reach the host through inter-bridge routing via docker0’s gateway.

See core/paths/paths.go (ce module). Three modes: dev (project-relative), root (system FHS), non-root (XDG home).

ConcernLinux rootDev mode
Config/etc/shc<project>/.shc/config
State (database, vault, deployments)/var/lib/shc<project>/.shc/state
Cache/var/cache/shc<project>/.shc/cache
Logs/var/log/shc<project>/.shc/logs
Runtime / socket / pid/run/shc<project>/.shc/run
Service unitsystemd /usr/lib/systemd/system/shc.service(foreground process)

SHC never edits /etc/hosts. Dev/test domains use nip.io (or sslip.io) wildcard DNS:

keycloak.10.240.0.4.nip.io → 10.240.0.4
app.127.0.0.1.nip.io → 127.0.0.1

This works without sudo, without DNS-cache flushes, and on every platform. Operators with real DNS supply --host example.com to override.

make package runs goreleaser release --snapshot --clean, which builds the pure-Go binaries (CGO_ENABLED=0) and bundles the external rqlited raft-engine binary, then produces .deb, .rpm, .apk, and source tarballs under dist/. Tag pushes trigger the same flow in CI without --snapshot for real releases.

PlatformPackageInit systemStatus
Debian/Ubuntu.debsystemdFirst-class
RHEL family (Fedora, Rocky, RHEL, CentOS Stream).rpmsystemdFirst-class
Alpine.apkOpenRCFirst-class
macOS(source via make install)Best-effort
Windows(none)(none)Out of scope — WSL2 if requested