Skip to content

Bare-Metal Packaging Reference ​

Reference documentation for the internal/packaging module, which installs plexd as a host service and manages it afterwards: a systemd unit on Linux and other Unix, a launchd daemon on macOS, a Windows service under the Service Control Manager (SCM).

InstallConfig ​

Configuration struct for packaging and installing plexd.

FieldTypeDefaultDescription
BinaryPathstring/usr/local/bin/plexd (Linux)Path to install the plexd binary (per platform, below)
ConfigDirstring/etc/plexd (Linux)Configuration directory (per platform)
DataDirstring/var/lib/plexd (Linux)Data directory (per platform)
RunDirstring/var/run/plexd (Linux)Runtime directory (per platform)
LogDirstring(empty on Linux)Directory the service manager writes plexd's output to (per platform, below)
UnitFilePathstring/etc/systemd/system/plexd.service (Linux)Path of the service definition file (per platform, below)
ServiceNamestringplexdName the host's service manager knows plexd by
APIBaseURLstring(empty)Control plane API URL (optional)
TokenValuestring(empty)Bootstrap token value (optional)
TokenFilestring(empty)Path to token file to copy from (optional)

Three of those defaults are resolved per platform:

FieldLinuxmacOSWindows
BinaryPath/usr/local/bin/plexd/usr/local/bin/plexd%ProgramFiles%\plexd\plexd.exe
UnitFilePath/etc/systemd/system/plexd.service/Library/LaunchDaemons/com.plexsphere.plexd.plist(empty)
LogDir(empty)/Library/Logs/plexd(empty)

UnitFilePath is empty on Windows because the SCM keeps its service definition in its own database rather than in a file. LogDir is empty wherever the manager keeps the logs itself: journald on Linux, the Application Event Log on Windows. %ProgramFiles% is the ProgramFiles environment variable, with C:\Program Files as the fallback when it is unset or empty.

Methods ​

  • ApplyDefaults() — Sets default values for zero-valued fields.
  • Validate() error — Returns an error if any required field (BinaryPath, ConfigDir, DataDir, RunDir, ServiceName) is empty. UnitFilePath and LogDir are not required, because they are legitimately empty on some platforms; the managers that need one check it themselves.

GenerateUnitFile ​

go
func GenerateUnitFile(cfg InstallConfig) string

Produces a complete systemd unit file. Calls cfg.ApplyDefaults() before generating output.

Unit file directives ​

SectionDirectiveValuePurpose
[Unit]Descriptionplexd node agentService description
Afternetwork-online.targetStart after network is available
Wantsnetwork-online.targetDeclare network dependency
Wantsplexd-session-helper.socketStart the session helper socket with plexd
Afterplexd-session-helper.socketListen on the helper socket before plexd dials it
StartLimitBurst5Max restart attempts in interval
StartLimitIntervalSec60Crash loop protection window (seconds)
[Service]TypesimpleProcess type
ExecStart{BinaryPath} up --config {ConfigDir}/config.yamlStart command
RestartalwaysRestart unconditionally
RestartSec5sDelay between restarts
LimitNOFILE65536File descriptor limit for WireGuard tunnels
EnvironmentFile-{ConfigDir}/environmentOptional environment file (dash = optional)
AmbientCapabilitiesCAP_NET_ADMIN CAP_NET_RAWNetwork capabilities for WireGuard and ICMP
CapabilityBoundingSetCAP_NET_ADMIN CAP_NET_RAWLimit capabilities to required set
ProtectSystemfullMake /usr, /boot, /efi and /etc read-only
ProtectHometrueMake /home, /root, /run/user inaccessible
ReadWritePaths-{DataDir} -{RunDir} -{ConfigDir}/bootstrap-tokenAllow writes to the data and runtime dirs and the bootstrap token file
[Install]WantedBymulti-user.targetEnable at boot in multi-user mode
Alsoplexd-session-helper.socketEnabling or disabling plexd does the same to the socket

systemctl enable --now plexd therefore enables the helper socket and starts it with plexd. The sandbox directives stay: the session helper is what runs outside them.

/etc stays read-only for plexd on purpose. The session helper trusts config.yaml and the signing key it pins at {ConfigDir}/session-signing-key only because plexd cannot write them. The bootstrap token file is the one path under ConfigDir the unit lets plexd write: plexd cannot unlink the spent token, because its directory stays read-only, so it empties the file instead (see Registration). The hooks directory is created by plexd install, not by plexd.

Every ReadWritePaths entry carries the - prefix, which tells systemd to skip a path that does not exist instead of failing the unit with 226/NAMESPACE. RunDir lives on tmpfs and is gone after every reboot until plexd recreates it for the node API socket, and plexd deregister --purge removes DataDir until plexd saves a new identity. /run and /var stay writable under ProtectSystem=full, so plexd can create both. A token file that does not exist at start leaves nothing to empty.

Session helper units ​

go
func GenerateSessionHelperSocketUnit() string
func GenerateSessionHelperServiceUnit(cfg InstallConfig) string

The session helper starts the processes of mediated ssh sessions outside plexd's sandbox (see SSH Sessions). The socket unit, plexd-session-helper.socket, is root-only, and Accept=yes starts one helper instance per connection:

ini
[Unit]
Description=plexd session helper socket

[Socket]
ListenStream=/run/plexd-session-helper.sock
SocketMode=0600
SocketUser=root
SocketGroup=root
Accept=yes
MaxConnections=512
TriggerLimitIntervalSec=0

[Install]
WantedBy=sockets.target

systemd's defaults for Accept=yes sockets would let one session holder break ssh sessions for the whole node. MaxConnections=64 is below the 400 processes plexd's defaults allow (10 sessions of 4 connections of 10 channels), and more than 200 connections in 2 s would put the socket in a failed state until an operator restarts it. The unit raises MaxConnections above plexd's own bound, which is what limits the helpers, and turns the trigger limit off.

The template each connection instantiates, plexd-session-helper@.service, calls cfg.ApplyDefaults() for {BinaryPath} and {ConfigDir}:

ini
[Unit]
Description=plexd session helper (one mediated ssh process)
CollectMode=inactive-or-failed

[Service]
Type=simple
ExecStart={BinaryPath} session-helper --config {ConfigDir}/config.yaml
StandardInput=null
StandardOutput=journal
StandardError=journal
KillMode=control-group

It carries no sandbox directives on purpose: it exists to run outside plexd's sandbox, where a shell can become the login user, and its guard is the helper's own check of the session token against a key plexd cannot write. KillMode=control-group takes down whatever a session left behind. deploy/systemd/ ships all three units byte-identical to the generators' output for the Linux defaults, which a unit test keeps true.

plexd session-helper ​

A hidden command that serves one request of the helper protocol on fd 3 and exits 0, or 1 on an error. It is not run by hand, and runs in one of two modes:

ModeWhenSigning key
SocketLISTEN_PID is its pid and LISTEN_FDS is 1 (systemd Accept=yes), without --childtunnel.session_signing_public_key from --config, else the key pinned at session-signing-key beside the config file, written from identity.json on the first run
Child--child, when plexd starts it because the socket does not existEvery --trusted-key (standard base64); no config is read and nothing is pinned

--trusted-key in socket mode, --child without --trusted-key, and a run that is neither socket-activated nor --child (or both) are refused. On macOS only the child mode exists: a run without --child is refused with plexd session-helper: plexd starts it as its own child (--child); macOS has no helper socket. The socket mode unsets LISTEN_PID, LISTEN_FDS and LISTEN_FDNAMES before it starts anything. Resolving the key fails on a malformed pin (session helper: pinned key <path> is malformed), on an unregistered node when no pin exists yet (session helper: node is not registered; no signing key to pin), and on any other error reading the pin (session helper: read pinned key <path>); the helper then answers nothing and the client's login gets a launch error.

GenerateLaunchdPlist ​

go
func GenerateLaunchdPlist(cfg InstallConfig) string

Produces the LaunchDaemon property list macOS loads from /Library/LaunchDaemons. Calls cfg.ApplyDefaults() before generating output, and escapes every interpolated string as XML character data.

Plist keys ​

KeyValuePurpose
Labelcom.plexsphere.plexdThe reverse-DNS label launchd keys the daemon by
ProgramArguments{BinaryPath}, up, --config, {ConfigDir}/config.yamlStart command, one array entry per argument
RunAtLoadtrueStart when launchd loads the daemon
KeepAlivetrueRestart unconditionally (the unit file's Restart=always)
ThrottleInterval5Seconds between restarts (RestartSec=5s)
StandardOutPath{LogDir}/plexd.logWhere the daemon's stdout goes
StandardErrorPath{LogDir}/plexd.logWhere the daemon's stderr goes
SoftResourceLimits.NumberOfFiles65536File descriptor limit (LimitNOFILE)
HardResourceLimits.NumberOfFiles65536File descriptor limit (LimitNOFILE)

launchd has no StartLimitBurst counterpart, so a daemon that exits on a configuration error restarts every five seconds until an operator boots it out. It has no EnvironmentFile counterpart either: PLEXD_* overrides on macOS go into config.yaml, or into an EnvironmentVariables dict an operator adds by hand.

When the daemon finds its own stderr to be the log file above, it writes its log records through a writer that reopens the path whenever the file sitting there changes, so the newsyslog rotation below does not strand its output in the renamed file. Every reopen uses O_NOFOLLOW, because /Library/Logs is writable by the admin group and the daemon runs as root. A symlink already at the path is therefore left alone entirely: the writer is not installed and the records keep going to the descriptor launchd opened, rather than to a writer whose every write the reopen would refuse. Output the log handler does not produce (a Go panic trace, and the missing-configuration warning that runs before the logger exists) still goes to that descriptor in either case.

GenerateNewsyslogConf ​

go
func GenerateNewsyslogConf(cfg InstallConfig) string

Produces the rotation rule plexd install writes to /etc/newsyslog.d/com.plexsphere.plexd.conf. launchd appends to StandardOutPath forever, so without it the log grows without bound. The rule carries no path_to_pid_file for the reason above. That field names the process newsyslog signals after a rotation; without it the signal goes to syslogd rather than to the daemon, and the daemon needs no signal to find the new file.

# plexd log rotation, written by plexd install
/Library/Logs/plexd/plexd.log	644	5	10240	*	J

The fields are mode 644, five rotated generations kept, rotation at 10240 KiB (10 MiB), no time restriction, and J for bzip2 compression.

Windows service configuration ​

The SCM has no definition file. Register creates the service with this configuration, or refreshes an existing one through UpdateConfig:

SettingValue
Service nameplexd
DisplayNameplexd node agent
DescriptionPlexsphere node agent. Registers the node, builds WireGuard mesh tunnels and enforces network policy.
StartTypemgr.StartAutomatic — starts at boot
ErrorControlmgr.ErrorNormal
BinaryPathName"{BinaryPath}" up --config {ConfigDir}\config.yaml, each argument quoted where it needs it
ServiceStartName(empty) — the service runs as LocalSystem

Recovery actions are the SCM's counterpart to Restart=always and RestartSec=5s: three ServiceRestart actions five seconds apart, a 60-second reset period, and SetRecoveryActionsOnNonCrashFailures(true). The SCM applies the last action to every later failure, so the service restarts indefinitely.

Register also installs the Application Event Log source plexd, pointed at %SystemRoot%\System32\EventCreate.exe, whose message table renders event ids 1 to 1000 as the message text. plexd ships no message DLL of its own.

GenerateDefaultConfig ​

go
func GenerateDefaultConfig(apiBaseURL string) string

Produces a minimal default config.yaml. When apiBaseURL is empty, writes a commented-out placeholder. The two paths it writes are the platform defaults (per platform); the values below are the Linux ones.

Output fields ​

FieldValueDescription
api.base_urlProvided URL or # api: base_url: …Control plane API URL
data_dir/var/lib/plexd (Linux)Data directory
log_levelinfoLog verbosity
registration.token_file/etc/plexd/bootstrap-token (Linux)Bootstrap token file path

Installer ​

go
func NewInstaller(cfg InstallConfig, mgr ServiceManager, root RootChecker, logger *slog.Logger) *Installer

The Installer owns the files that are the same on every platform — the binary, the config, the token — and leaves the service definition to the ServiceManager.

Install() error ​

Installs plexd as a host service. Steps:

  1. Verify privileges (RootChecker.IsRoot()): root on Unix, an elevated token on Windows
  2. Verify the host's service manager is available (ServiceManager.Available())
  3. Create directories: ConfigDir (0755), ConfigDir/hooks (0755), DataDir (0700), RunDir (0755), and LogDir (0755) where it is set. An existing directory and its contents are left as they are
  4. Copy the running binary to BinaryPath (0755)
  5. Write default config.yaml if absent (preserves existing)
  6. Write bootstrap token if TokenValue or TokenFile is set (0600)
  7. Register the service (ServiceManager.Register())

The service is registered, never started. --api-url is optional, so an install can legitimately precede a usable configuration; the start command per platform is in the CLI reference.

Uninstall(purge bool) error ​

Removes the plexd host service. Steps:

  1. Verify privileges
  2. If the service is not registered (ServiceManager.Registered()), return nil (idempotent)
  3. Stop the service and remove its definition (ServiceManager.Unregister())
  4. Remove binary
  5. If purge is true, remove DataDir and ConfigDir recursively

On Windows the binary is a running image whenever plexd uninstall runs from the installed path, and Windows refuses to delete one. The file is renamed to plexd.exe.old and handed to the boot-time delete queue instead, so it disappears at the next reboot.

Interfaces ​

ServiceManager ​

go
type ServiceManager interface {
    Name() string
    Available() bool
    Registered(cfg InstallConfig) (bool, error)
    Register(cfg InstallConfig) error
    Unregister(cfg InstallConfig) error
    Start(cfg InstallConfig) error
    Stop(cfg InstallConfig) error
    Restart(ctx context.Context, cfg InstallConfig) error
    Status(cfg InstallConfig) (ServiceStatus, error)
}

NewServiceManager(logger) returns the host's own. What each method does per manager:

MethodsystemdlaunchdService Control Manager
Namesystemdlaunchdservice control manager
Availablesystemctl on PATHlaunchctl on PATHthe SCM accepts a connection
Registeredthe unit file existsthe plist existsOpenService finds the service
Registerwrite plexd.service and the two session helper units beside it, systemctl daemon-reloadwrite the plist and the newsyslog ruleCreateService or UpdateConfig, recovery actions, Event Log source
Unregistersystemctl stop and disable for plexd and plexd-session-helper.socket, remove the three unit files, daemon-reloadlaunchctl bootout, remove the plist and the newsyslog rulestop, Delete, remove the Event Log source
Startsystemctl startlaunchctl bootstrap system <plist>Service.Start
Stopsystemctl stoplaunchctl bootout when loadedService.Control(svc.Stop), then poll for Stopped
Restartsystemctl restartlaunchctl kickstart -ka detached Restart-Service
Statussystemctl is-activelaunchctl print reports state = runningService.Query reports Running

Status returns ErrNotRegistered when the service definition is missing, and otherwise StatusRunning or StatusStopped.

Register never starts the service, on any platform. What "registered" means differs: a systemd unit is written but not enabled; a launchd plist in /Library/LaunchDaemons is loaded at the next boot; a Windows service with automatic start starts at the next boot.

Stop on launchd boots the daemon out rather than calling launchctl stop, because KeepAlive would restart a stopped daemon immediately. Restart on Windows goes through a detached PowerShell process: the SCM has no restart control, and the caller is usually the service being restarted, so stopping it from inside would kill whatever was meant to start it again.

SystemdController ​

go
type SystemdController interface {
    IsAvailable() bool
    DaemonReload() error
    Enable(service string) error
    Disable(service string) error
    Start(service string) error
    Stop(service string) error
    Restart(ctx context.Context, service string) error
    IsActive(service string) bool
}

Production implementation (NewSystemdController()) uses os/exec to call systemctl. The systemd ServiceManager drives systemctl only through this interface, so its unit-file flow is testable without systemd.

RootChecker ​

go
type RootChecker interface {
    IsRoot() bool
}

Production implementation (NewRootChecker()) uses os.Getuid() == 0 on Unix and the process token's elevation state on Windows, where os.Getuid returns -1 and would refuse an Administrator along with everybody else.

File paths and permissions ​

Linux:

PathPermissionCreated byDescription
/usr/local/bin/plexd0755Installplexd binary
/etc/plexd/0755InstallConfiguration directory
/etc/plexd/hooks/0755InstallHook scripts; read-only to plexd under its unit
/etc/plexd/config.yaml0644InstallService configuration
/etc/plexd/bootstrap-token0600InstallBootstrap token; emptied rather than removed after registration under the unit
/etc/plexd/environment(user)OperatorOptional env vars
/var/lib/plexd/0700InstallData directory
/var/run/plexd/0755InstallRuntime directory
/etc/systemd/system/plexd.service0644InstallSystemd unit file
/etc/systemd/system/plexd-session-helper.socket0644InstallSession helper socket unit
/etc/systemd/system/plexd-session-helper@.service0644InstallSession helper service template
/run/plexd-session-helper.sock0600systemdSession helper socket, root only
/etc/plexd/session-signing-key0644Session helperThe pinned signing key ssh session tokens verify against; written on the helper's first run when tunnel.session_signing_public_key is unset

The daemon's own output goes to journald.

macOS:

PathPermissionCreated byDescription
/usr/local/bin/plexd0755Installplexd binary
/Library/Application Support/plexd/0755InstallConfiguration directory
/Library/Application Support/plexd/hooks/0755InstallHook scripts
/Library/Application Support/plexd/data/0700InstallData directory
/var/run/plexd/0755InstallRuntime directory
/Library/LaunchDaemons/com.plexsphere.plexd.plist0644InstallLaunchDaemon definition, root:wheel
/Library/Logs/plexd/0755InstallLog directory
/Library/Logs/plexd/plexd.log(launchd)launchdDaemon stdout and stderr
/etc/newsyslog.d/com.plexsphere.plexd.conf0644InstallLog rotation rule

launchd refuses to load a daemon whose plist is not owned by root:wheel and writable only by its owner, which is what the installer's own privileges produce.

Windows:

PathCreated byDescription
%ProgramFiles%\plexd\plexd.exeInstallplexd binary
%ProgramFiles%\plexd\plexd.exe.oldUpgradeThe previous binary, removed by the next upgrade or at the next reboot after plexd uninstall
%ProgramData%\plexd\InstallConfiguration directory
%ProgramData%\plexd\hooks\InstallHook scripts
%ProgramData%\plexd\data\InstallData directory
%ProgramData%\plexd\run\InstallRuntime directory
SCM service plexdInstallService definition, in the SCM's own database
Event Log source plexdInstallThe Application log the daemon writes to

Windows has no POSIX permission bits; access is governed by the ACLs those directories inherit.

Token validation ​

Bootstrap tokens are validated with the same rules as internal/registration/token.go:

  • Maximum length: 512 bytes
  • Characters: printable ASCII only (0x20–0x7E)
  • Token priority: TokenValue > TokenFile
  • Written to {ConfigDir}/bootstrap-token with 0600 permissions

Install script ​

The install script (deploy/install.sh) is a POSIX-compatible shell script. It installs plexd on Linux and macOS. Windows has no install script, because a POSIX script cannot serve it; that install is the manual walkthrough in the Windows installation guide.

Usage ​

sh
curl -fsSL https://get.plexsphere.com/install.sh | sh -s -- [OPTIONS]

Flags ​

FlagDescriptionDefault
--token VALUEBootstrap token for enrollment(none)
--api-url URLControl plane API URL(none)
--version VERSIONVersion to installlatest
--no-startDon't start the service after install(start)

Behavior ​

  1. Detects the OS: Linux or macOS (Darwin); anything else is fatal
  2. Detects architecture (x86_64 → amd64, aarch64 and arm64 → arm64; Linux reports aarch64 where macOS reports arm64 for the same silicon)
  3. Downloads plexd-{os}-{arch} from the artifact URL
  4. Downloads and verifies SHA-256 checksum
  5. Creates the plexd and plexd-secrets groups where groupadd exists
  6. Runs plexd install with passthrough flags
  7. Starts the service unless --no-start: systemctl enable --now plexd on Linux, launchctl bootstrap system /Library/LaunchDaemons/com.plexsphere.plexd.plist on macOS
  8. Cleans up temporary files on exit

Step 5 is a no-op on macOS, which has no groupadd: the script warns and carries on, and the node API socket stays owner-only. Create the two groups with dscl to widen it, as the local node API guide shows.

Environment variables ​

VariableDescriptionDefault
PLEXD_ARTIFACT_URLBase URL for binary artifactshttps://artifacts.plexsphere.com/plexd