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.
| Field | Type | Default | Description |
|---|---|---|---|
BinaryPath | string | /usr/local/bin/plexd (Linux) | Path to install the plexd binary (per platform, below) |
ConfigDir | string | /etc/plexd (Linux) | Configuration directory (per platform) |
DataDir | string | /var/lib/plexd (Linux) | Data directory (per platform) |
RunDir | string | /var/run/plexd (Linux) | Runtime directory (per platform) |
LogDir | string | (empty on Linux) | Directory the service manager writes plexd's output to (per platform, below) |
UnitFilePath | string | /etc/systemd/system/plexd.service (Linux) | Path of the service definition file (per platform, below) |
ServiceName | string | plexd | Name the host's service manager knows plexd by |
APIBaseURL | string | (empty) | Control plane API URL (optional) |
TokenValue | string | (empty) | Bootstrap token value (optional) |
TokenFile | string | (empty) | Path to token file to copy from (optional) |
Three of those defaults are resolved per platform:
| Field | Linux | macOS | Windows |
|---|---|---|---|
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.UnitFilePathandLogDirare not required, because they are legitimately empty on some platforms; the managers that need one check it themselves.
GenerateUnitFile
func GenerateUnitFile(cfg InstallConfig) stringProduces a complete systemd unit file. Calls cfg.ApplyDefaults() before generating output.
Unit file directives
| Section | Directive | Value | Purpose |
|---|---|---|---|
[Unit] | Description | plexd node agent | Service description |
After | network-online.target | Start after network is available | |
Wants | network-online.target | Declare network dependency | |
Wants | plexd-session-helper.socket | Start the session helper socket with plexd | |
After | plexd-session-helper.socket | Listen on the helper socket before plexd dials it | |
StartLimitBurst | 5 | Max restart attempts in interval | |
StartLimitIntervalSec | 60 | Crash loop protection window (seconds) | |
[Service] | Type | simple | Process type |
ExecStart | {BinaryPath} up --config {ConfigDir}/config.yaml | Start command | |
Restart | always | Restart unconditionally | |
RestartSec | 5s | Delay between restarts | |
LimitNOFILE | 65536 | File descriptor limit for WireGuard tunnels | |
EnvironmentFile | -{ConfigDir}/environment | Optional environment file (dash = optional) | |
AmbientCapabilities | CAP_NET_ADMIN CAP_NET_RAW | Network capabilities for WireGuard and ICMP | |
CapabilityBoundingSet | CAP_NET_ADMIN CAP_NET_RAW | Limit capabilities to required set | |
ProtectSystem | full | Make /usr, /boot, /efi and /etc read-only | |
ProtectHome | true | Make /home, /root, /run/user inaccessible | |
ReadWritePaths | -{DataDir} -{RunDir} -{ConfigDir}/bootstrap-token | Allow writes to the data and runtime dirs and the bootstrap token file | |
[Install] | WantedBy | multi-user.target | Enable at boot in multi-user mode |
Also | plexd-session-helper.socket | Enabling 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
func GenerateSessionHelperSocketUnit() string
func GenerateSessionHelperServiceUnit(cfg InstallConfig) stringThe 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:
[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.targetsystemd'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}:
[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-groupIt 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:
| Mode | When | Signing key |
|---|---|---|
| Socket | LISTEN_PID is its pid and LISTEN_FDS is 1 (systemd Accept=yes), without --child | tunnel.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 exist | Every --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
func GenerateLaunchdPlist(cfg InstallConfig) stringProduces 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
| Key | Value | Purpose |
|---|---|---|
Label | com.plexsphere.plexd | The reverse-DNS label launchd keys the daemon by |
ProgramArguments | {BinaryPath}, up, --config, {ConfigDir}/config.yaml | Start command, one array entry per argument |
RunAtLoad | true | Start when launchd loads the daemon |
KeepAlive | true | Restart unconditionally (the unit file's Restart=always) |
ThrottleInterval | 5 | Seconds between restarts (RestartSec=5s) |
StandardOutPath | {LogDir}/plexd.log | Where the daemon's stdout goes |
StandardErrorPath | {LogDir}/plexd.log | Where the daemon's stderr goes |
SoftResourceLimits.NumberOfFiles | 65536 | File descriptor limit (LimitNOFILE) |
HardResourceLimits.NumberOfFiles | 65536 | File 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
func GenerateNewsyslogConf(cfg InstallConfig) stringProduces 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 * JThe 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:
| Setting | Value |
|---|---|
| Service name | plexd |
DisplayName | plexd node agent |
Description | Plexsphere node agent. Registers the node, builds WireGuard mesh tunnels and enforces network policy. |
StartType | mgr.StartAutomatic — starts at boot |
ErrorControl | mgr.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
func GenerateDefaultConfig(apiBaseURL string) stringProduces 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
| Field | Value | Description |
|---|---|---|
api.base_url | Provided URL or # api: base_url: … | Control plane API URL |
data_dir | /var/lib/plexd (Linux) | Data directory |
log_level | info | Log verbosity |
registration.token_file | /etc/plexd/bootstrap-token (Linux) | Bootstrap token file path |
Installer
func NewInstaller(cfg InstallConfig, mgr ServiceManager, root RootChecker, logger *slog.Logger) *InstallerThe 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:
- Verify privileges (
RootChecker.IsRoot()): root on Unix, an elevated token on Windows - Verify the host's service manager is available (
ServiceManager.Available()) - Create directories:
ConfigDir(0755),ConfigDir/hooks(0755),DataDir(0700),RunDir(0755), andLogDir(0755) where it is set. An existing directory and its contents are left as they are - Copy the running binary to
BinaryPath(0755) - Write default
config.yamlif absent (preserves existing) - Write bootstrap token if
TokenValueorTokenFileis set (0600) - 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:
- Verify privileges
- If the service is not registered (
ServiceManager.Registered()), return nil (idempotent) - Stop the service and remove its definition (
ServiceManager.Unregister()) - Remove binary
- If
purgeis true, removeDataDirandConfigDirrecursively
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
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:
| Method | systemd | launchd | Service Control Manager |
|---|---|---|---|
Name | systemd | launchd | service control manager |
Available | systemctl on PATH | launchctl on PATH | the SCM accepts a connection |
Registered | the unit file exists | the plist exists | OpenService finds the service |
Register | write plexd.service and the two session helper units beside it, systemctl daemon-reload | write the plist and the newsyslog rule | CreateService or UpdateConfig, recovery actions, Event Log source |
Unregister | systemctl stop and disable for plexd and plexd-session-helper.socket, remove the three unit files, daemon-reload | launchctl bootout, remove the plist and the newsyslog rule | stop, Delete, remove the Event Log source |
Start | systemctl start | launchctl bootstrap system <plist> | Service.Start |
Stop | systemctl stop | launchctl bootout when loaded | Service.Control(svc.Stop), then poll for Stopped |
Restart | systemctl restart | launchctl kickstart -k | a detached Restart-Service |
Status | systemctl is-active | launchctl print reports state = running | Service.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
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
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:
| Path | Permission | Created by | Description |
|---|---|---|---|
/usr/local/bin/plexd | 0755 | Install | plexd binary |
/etc/plexd/ | 0755 | Install | Configuration directory |
/etc/plexd/hooks/ | 0755 | Install | Hook scripts; read-only to plexd under its unit |
/etc/plexd/config.yaml | 0644 | Install | Service configuration |
/etc/plexd/bootstrap-token | 0600 | Install | Bootstrap token; emptied rather than removed after registration under the unit |
/etc/plexd/environment | (user) | Operator | Optional env vars |
/var/lib/plexd/ | 0700 | Install | Data directory |
/var/run/plexd/ | 0755 | Install | Runtime directory |
/etc/systemd/system/plexd.service | 0644 | Install | Systemd unit file |
/etc/systemd/system/plexd-session-helper.socket | 0644 | Install | Session helper socket unit |
/etc/systemd/system/plexd-session-helper@.service | 0644 | Install | Session helper service template |
/run/plexd-session-helper.sock | 0600 | systemd | Session helper socket, root only |
/etc/plexd/session-signing-key | 0644 | Session helper | The 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:
| Path | Permission | Created by | Description |
|---|---|---|---|
/usr/local/bin/plexd | 0755 | Install | plexd binary |
/Library/Application Support/plexd/ | 0755 | Install | Configuration directory |
/Library/Application Support/plexd/hooks/ | 0755 | Install | Hook scripts |
/Library/Application Support/plexd/data/ | 0700 | Install | Data directory |
/var/run/plexd/ | 0755 | Install | Runtime directory |
/Library/LaunchDaemons/com.plexsphere.plexd.plist | 0644 | Install | LaunchDaemon definition, root:wheel |
/Library/Logs/plexd/ | 0755 | Install | Log directory |
/Library/Logs/plexd/plexd.log | (launchd) | launchd | Daemon stdout and stderr |
/etc/newsyslog.d/com.plexsphere.plexd.conf | 0644 | Install | Log 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:
| Path | Created by | Description |
|---|---|---|
%ProgramFiles%\plexd\plexd.exe | Install | plexd binary |
%ProgramFiles%\plexd\plexd.exe.old | Upgrade | The previous binary, removed by the next upgrade or at the next reboot after plexd uninstall |
%ProgramData%\plexd\ | Install | Configuration directory |
%ProgramData%\plexd\hooks\ | Install | Hook scripts |
%ProgramData%\plexd\data\ | Install | Data directory |
%ProgramData%\plexd\run\ | Install | Runtime directory |
SCM service plexd | Install | Service definition, in the SCM's own database |
Event Log source plexd | Install | The 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-tokenwith 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
curl -fsSL https://get.plexsphere.com/install.sh | sh -s -- [OPTIONS]Flags
| Flag | Description | Default |
|---|---|---|
--token VALUE | Bootstrap token for enrollment | (none) |
--api-url URL | Control plane API URL | (none) |
--version VERSION | Version to install | latest |
--no-start | Don't start the service after install | (start) |
Behavior
- Detects the OS: Linux or macOS (
Darwin); anything else is fatal - Detects architecture (
x86_64→amd64,aarch64andarm64→arm64; Linux reportsaarch64where macOS reportsarm64for the same silicon) - Downloads
plexd-{os}-{arch}from the artifact URL - Downloads and verifies SHA-256 checksum
- Creates the
plexdandplexd-secretsgroups wheregroupaddexists - Runs
plexd installwith passthrough flags - Starts the service unless
--no-start:systemctl enable --now plexdon Linux,launchctl bootstrap system /Library/LaunchDaemons/com.plexsphere.plexd.pliston macOS - 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
| Variable | Description | Default |
|---|---|---|
PLEXD_ARTIFACT_URL | Base URL for binary artifacts | https://artifacts.plexsphere.com/plexd |