servicectl is a systemd-unit-compatible service control stack built around
Dinit and s6-rc. It reads familiar .service and .socket definitions,
generates Dinit services for workload execution, and adds a local control plane
for orchestration, state observation, cgroup membership, D-Bus activation,
logging, and system/user isolation.
It is not a replacement PID 1 and it is not a complete reimplementation of systemd. The project focuses on running and operating service units on a Dinit/s6 runtime while preserving useful systemd-facing interfaces where that can be done accurately.
| Area | Current behavior |
|---|---|
| Unit execution | Translates supported systemd service/socket definitions into Dinit services. Dinit remains the final workload supervisor. |
| Orchestration | Uses s6-rc for the system control plane, per-unit orchestrators, groups, targets, and startup dependencies. |
| Status | Builds a topology-first view from Dinit, s6, cgroup, D-Bus, observation, and control-plane evidence. |
| State APIs | Exposes local Unix-socket query, event, property, group, and snapshot APIs through servicectl-api, sysvisiond, and sys-propertyd. |
| Activation | Supports notify, socket, and strict D-Bus activation paths through dedicated helper daemons. |
| Cgroups | Tracks ready service process trees in cgroup v2 leaves without taking ownership of resource-controller policy. |
| Logging | Runs per-unit sys-logd workers through a root system broker and emits standard journald object-unit fields, with syslog fallback. |
| Modes | Keeps system and user service planes, runtime sockets, Dinit instances, and service identities separate. |
| Compatibility | Accepts virtual .target inputs and can optionally expose a minimal org.freedesktop.systemd1 API through the separate systemd1-broker project. |
The source installation expects a Linux host with:
- Go 1.22 or newer
- Dinit and
dinitctl - s6 and s6-rc
- systemd-style unit files in the standard system or user unit paths
- a writable runtime directory for the selected mode
journald is used for structured unit-attributed logs when its native socket is available. Per-unit log workers fall back to the normal syslog path when the system log broker cannot be reached. cgroup v2 and D-Bus integration are optional at runtime; their status becomes degraded or unavailable rather than being silently fabricated.
Install the CLI and helper daemons under ~/.local:
bash scripts/install.shInstall directly under /usr/local:
sudo bash scripts/install.sh --systemDeployment shells that deliberately want both the current account's local
prefix and a system copy can use --also-system. Under sudo -H, the local
copy is installed under root's home:
sudo -H bash scripts/install.sh --also-systemThe system installation path also reconciles the system s6 source graph by
running servicectl ensure-s6. Running the command again is safe when the core
service definitions need to be rebuilt:
sudo servicectl ensure-s6Installation does not replace PID 1, reconfigure the host bootloader, or turn the host into a Dinit-based boot automatically. Boot integration and ownership of the live s6 supervision tree remain deployment decisions.
Fedora 44+ SRPM and RPM instructions are maintained in
packaging/README.md. The package filesystem and source
strategy are documented in
packaging/SRPM-DESIGN.md.
List known application units. Add --all to include generated and internal
services:
servicectl list --plain
servicectl list --all --jsonOperate a system unit:
servicectl status demo.service
servicectl start demo.service
servicectl restart demo.service
servicectl stop demo.service
servicectl logs -n 100 demo.service
servicectl logs -f demo.serviceUse the same commands against the user plane:
servicectl --user list --all --plain
servicectl --user status pipewire.service
servicectl --user restart pipewire.service
servicectl --user logs -f pipewire.serviceUseful state checks for scripts:
servicectl is-active demo.service
servicectl is-failed demo.service
servicectl is-enabled demo.serviceUse servicectl dinit ... only when raw Dinit output or an operation outside
the normal servicectl surface is required.
Persistent enablement is stored by the property/group control plane and is realized by the s6 backend:
servicectl enable demo.service
servicectl disable demo.service
servicectl is-enabled demo.service
servicectl enable group:audio
servicectl --group audio status
servicectl --group audio disable.target names are accepted as a compatibility input. Internally they resolve
to groups and properties managed by sys-propertyd:
servicectl enable pipewire.target
servicectl --group pipewire statussys-propertyd imports Wants= and Requires= from target files and from
.target.wants/ or .target.requires/ directories. Explicit definitions take
priority over imported target membership:
- system groups:
/etc/servicectl/groups.d/*.conf - system target aliases:
/etc/servicectl/targets.d/*.conf - user groups:
~/.config/servicectl/groups.d/*.conf - user target aliases:
~/.config/servicectl/targets.d/*.conf
Example group definition:
[Group]
Name=audio
Units=pipewire.service pipewire-pulse.service wireplumber.service
Targets=pipewire.targetExample target alias:
[Target]
Name=pipewire.target
Group=audioFor enable, disable, is-enabled, and status, a service that belongs to
exactly one group can be resolved automatically. An ambiguous service requires
an explicit --group selection.
servicectl status UNIT separates workload lifecycle from the health of the
components that control or observe it:
servicectl status demo.service
servicectl status demo.service --plain
servicectl status demo.service --verbose
servicectl status demo.service --json- Terminal output shows a primary control path and side branches. Narrow terminals switch to an indented tree without dropping topology data.
--plainemits stable ASCII without ANSI styling or width truncation.--verboseexpands endpoints, evidence, timestamps, process metadata, and the bounded log sample.--jsonemits status schema version 2 with typed nodes, edges, diagnostics, and logs.list --jsonremains schema version 1.active (degraded)means the workload is running while a declared participant is conclusively unhealthy or missing.active (unknown)means the workload is running but required evidence could not be collected reliably.
Status exits with 0 for a resolved healthy model, 3 for failed, degraded,
or unknown aggregate health, 4 for a missing unit, and 1 for usage,
collection, rendering, or encoding failures. The workload lifecycle remains
available separately as summary.runtime_state in JSON.
NO_COLOR, TERM=dumb, and non-TTY output disable color without removing
state labels or topology structure.
New generated logger definitions run a per-unit sys-logd --worker process
with the target unit identity. Workers send fixed-schema records to the system
sys-logd broker at /run/servicectl/logd; the broker is a root s6 service and
authenticates each route with Unix peer credentials, process metadata, and the
Dinit logger PID.
The broker writes native journald records with:
OBJECT_SYSTEMD_UNIT=<unit>.servicefor system unitsOBJECT_SYSTEMD_USER_UNIT=<unit>.servicefor user unitsSYSLOG_IDENTIFIER=servicectl[<unit>]
This makes standard journal filtering work:
journalctl -u demo.service
journalctl --user-unit pipewire.serviceWhen the broker cannot accept a record, the worker uses the existing syslog path and periodically retries the broker. It spills to a private per-route directory only if both delivery paths fail. Legacy generated logger definitions are rewritten without restarting business services and take effect at the next runtime or unit restart.
sys-cgroupd tracks ready servicectl-managed services in dedicated cgroup v2
leaves. It moves the MainPID first and then descendants visible at migration
time. It does not guess detached processes and it does not configure CPU,
memory, IO, pids, cpuset, freeze, or kill policy.
servicectl cgroup status demo.service
servicectl cgroup list
servicectl cgroup inspect demo.service
servicectl cgroup pids demo.service
servicectl cgroup attach demo.service 1234
servicectl --user cgroup list
servicectl --user cgroup attach demo.service 5678User requests are authenticated with the Unix socket peer UID and cannot
inspect or modify another user's plane. There is no detach operation. The
default managed root is /sys/fs/cgroup/servicectl.slice; use
sys-cgroupd --cgroup-root when an outer manager provides a different writable
delegated subtree. --no-auto-mount disables the daemon's conservative empty
directory cgroup2 mount behavior.
sys-dbusd provides the strict activation frontend for system units with
BusName=. It resolves systemd D-Bus service entries to managed servicectl
routes and activates only registered unit control sockets.
Inspect and configure the D-Bus daemon helper as root:
servicectl dbus-activation check --backend=daemon
servicectl dbus-activation status --backend=daemon
sudo servicectl dbus-activation enable --backend=daemonEnabling or disabling the helper writes a managed manifest so the previous bus
configuration can be restored safely. Reload or restart the system bus only in
a controlled maintenance window, as printed by the command. Use
servicectl dbus-activation disable --backend=daemon to restore the previous
configuration.
The high-level data flow is:
systemd unit files
|
v
servicectl ------> Dinit ------> workload process
| |
| +-------> per-unit sys-logd worker
v
s6-rc control plane -------> sys-orchestrd / core daemons
| |
+------> sysvisiond <-----------+
|
+------> status / APIs / cgroup reconciliation
| Component | Responsibility |
|---|---|
servicectl |
CLI, unit parsing, Dinit generation, local API, and mode selection. |
| Dinit | Final supervisor for workload processes and generated per-unit logger services. |
| s6-rc | Supervises the system control plane and UID-qualified user infrastructure; owns orchestrator dependency graphs. |
servicectl-api |
Local unit catalog, typed snapshots, status manifests, refresh, and event stream. |
sysvisiond |
Normalizes lifecycle events and serves watch/query APIs for one system or user plane. |
sys-orchestrd |
Per-unit or per-group state machine that reconciles desired state through servicectl. |
sys-propertyd |
Persistent property, group, target, enabled-list, and runner-list truth source. |
sys-cgroupd |
Authenticated cgroup membership tracking and reconciliation. |
sys-notifyd |
Type=notify readiness and managed process state bridge. |
sys-socketd |
Managed socket activation for supported .socket units. |
sys-dbusd |
Managed BusName= activation and D-Bus control frontend. |
sys-logd |
Root s6 journal broker plus least-privilege per-unit Dinit workers and fallback delivery. |
System and user modes have distinct runtime sockets and Dinit instances. The s6 source graph is shared, but user infrastructure service names are UID-qualified.
The core project interoperates with systemd-facing tools in several bounded ways:
- consumes supported systemd unit-file directives
- accepts compatible
.targetnames and target dependency inputs - emits standard journald unit attribution for
journalctl -uandjournalctl --user-unit - provides D-Bus daemon activation for supported
BusName=services - exposes typed local snapshots that compatibility frontends can consume
The separate systemd1-broker worktree in the upstream systemd source tree can
act as an optional compatibility frontend. It is not built or packaged by the
current servicectl RPM.
Its servicectl backend connects to:
/run/servicectl/servicectl.sock/run/servicectl/sysvision/sysvisiond.sock/run/servicectl/managed
The broker dynamically publishes a minimal org.freedesktop.systemd1 Manager,
Unit, Service, and Job surface. The current implementation supports unit
catalog/list/get/load operations, lifecycle start/stop/restart/reload jobs, and
typed properties populated from servicectl snapshots. This is sufficient for
a useful subset of systemctl and sd-bus clients; it is not the complete
systemd manager API. Continue to use servicectl enable and disable for
servicectl enablement semantics.
Build the optional targets from a configured systemd build tree:
meson compile -C build systemd1-broker systemd1-broker-backend-servicectlExample standalone invocation:
SYSTEMD1_BUILD=/path/to/systemd/build
sudo "$SYSTEMD1_BUILD/systemd1-broker" \
--socket=/run/systemd/private \
--bus-address=unix:path=/run/dbus/system_bus_socket \
--backend="$SYSTEMD1_BUILD/src/systemd1-broker/libsystemd1-broker-backend-servicectl.so"--socket is required. The D-Bus address and backend are optional to the
generic broker executable but both are needed for normal servicectl-backed
systemctl interoperability. The target bus name and /run/systemd/private
must not already be owned by a real systemd manager.
With the broker active, supported lifecycle and status calls can use familiar clients:
systemctl list-units --type=service
systemctl status demo.service
systemctl start demo.service
systemctl restart demo.service
systemctl stop demo.serviceUnit-file enable/disable APIs are intentionally outside the current broker
surface; use servicectl enable and servicectl disable for those operations.
Backend path overrides are available for test or non-default deployments:
| Variable | Default |
|---|---|
SYSTEMD1_BROKER_SERVICECTL_SOCKET |
/run/servicectl/servicectl.sock |
SYSTEMD1_BROKER_SYSVISION_SOCKET |
/run/servicectl/sysvision/sysvisiond.sock |
SYSTEMD1_BROKER_SERVICECTL_RUNTIME |
/run/servicectl/managed |
SYSTEMD1_BROKER_SYSTEMD_UNIT_PATH |
Standard system unit search path |
Before starting the broker, ensure the servicectl s6 control plane and the two query sockets are ready. Run the compatibility broker under a supervisor in a real deployment rather than as an interactive shell process.
The control-plane daemons use HTTP-like protocols over local Unix sockets. They are intended for local tools and orchestrators, not as network services.
| Purpose | System path | User path |
|---|---|---|
| servicectl query API | /run/servicectl/servicectl.sock |
/run/user/<uid>/servicectl/servicectl.sock |
| servicectl event stream | /run/servicectl/servicectl-events.sock |
/run/user/<uid>/servicectl/servicectl-events.sock |
| sysvision query API | /run/servicectl/sysvision/sysvisiond.sock |
/run/user/<uid>/servicectl/sysvision/sysvisiond.sock |
| sysvision ingress | /run/servicectl/sysvision/events.sock |
/run/user/<uid>/servicectl/sysvision/events.sock |
| managed service state | /run/servicectl/managed |
$XDG_RUNTIME_DIR/servicectl/managed |
| log broker | /run/servicectl/logd |
Shared system broker |
| generated Dinit files | /run/dinit.d/generated |
$XDG_RUNTIME_DIR/dinit.d/generated |
| s6 source graph | /s6/rc |
Shared graph with UID-qualified services |
The system API normally runs as the s6 servicectl-api service. It can also be
started directly for development with servicectl serve-api. Example queries:
curl --unix-socket /run/servicectl/servicectl.sock \
'http://unix/v1/units?all=1'
curl --unix-socket /run/servicectl/servicectl.sock \
'http://unix/v1/units/demo.service'
curl --unix-socket /run/servicectl/servicectl.sock \
'http://unix/v1/status-manifest/demo.service'Fast repository checks:
go test ./... -count=1
go vet ./...
go build ./...
bash scripts/test-status-topology.sh
bash scripts/test-cgroupd-integration.sh --self-testThe full integration runner is:
bash scripts/test-all.shIt runs host integration suites after a five-second warning. Those suites may
start or stop real services and write systemd, Dinit, s6, and servicectl
runtime state. Set SERVICECTL_NO_HOST_WARNING=1 only to suppress the warning;
it does not make the suite non-mutating.
Useful focused integration scripts include:
bash scripts/test-sys-notifyd.sh
bash scripts/test-system-user-env.sh
bash scripts/test-logs-follow.sh
bash scripts/test-servicectl-dinit.sh
bash scripts/test-property-targets.sh
bash scripts/test-group-orchestration.sh
bash scripts/test-sysvisiond-bus.sh
bash scripts/test-sys-orchestrd.sh
bash scripts/test-s6-orchestrd.shReal cgroup migration tests require an explicitly delegated, writable cgroup v2 subtree:
sudo bash scripts/test-cgroupd-integration.sh \
--cgroup-root /sys/fs/cgroup/servicectl-test- top-level
*.go: main CLI, unit parsing, generation, API, and shared control logic cmd/: standalone runtime daemons and helpersinternal/visionapi/: runtime paths, event types, snapshots, and status contractsinternal/statusview/: status topology model and health derivationinternal/cgrouptrack/: cgroup protocol, identity, membership, and recoveryinternal/dbusactivation/: D-Bus route resolution and activation frontendscripts/: installation and regression/integration testspackaging/: Fedora spec, SRPM tooling, units, tmpfiles, and package design
- Only the systemd unit directives implemented by the parser and generated backends have servicectl semantics. Unknown directives do not become full systemd behavior automatically.
- Dinit remains the workload supervisor; s6-rc orchestrates the control plane and desired state rather than replacing Dinit.
- cgroup tracking is membership/accounting infrastructure, not resource policy.
- D-Bus and
systemd1-brokercompatibility cover defined subsets and should not be presented to clients as a complete systemd manager. - Source installation does not provide a complete boot transaction or choose how Dinit and the live s6 tree are started by the host.
servicectl is licensed under the MIT License. See LICENSE.