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
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 read-only
ProtectHometrueMake /home, /root, /run/user inaccessible
ReadWritePaths{DataDir} {RunDir}Allow writes to data and runtime dirs
[Install]WantedBymulti-user.targetEnable at boot in multi-user mode

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), DataDir (0700), RunDir (0755), and LogDir (0755) where it is set
  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 the unit file, systemctl daemon-reloadwrite the plist and the newsyslog ruleCreateService or UpdateConfig, recovery actions, Event Log source
Unregistersystemctl stop, disable, remove the unit file, 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/config.yaml0644InstallService configuration
/etc/plexd/bootstrap-token0600InstallBootstrap token
/etc/plexd/environment(user)OperatorOptional env vars
/var/lib/plexd/0700InstallData directory
/var/run/plexd/0755InstallRuntime directory
/etc/systemd/system/plexd.service0644InstallSystemd unit file

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/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\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_64amd64, aarch64 and arm64arm64; 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