Running EdgeAgent Under Systemd

June 2, 2026 | 18 min read

Introduction

Running the EdgeAgent from an interactive terminal is fine for development, but production requires a process supervisor. systemd is the standard on every modern Linux distribution, from Raspberry Pi OS to Ubuntu Server to Yocto-based industrial gateways. This article provides a production-grade unit file, explains every directive, and covers resource limits, graceful shutdown, and optional socket activation.

The Complete Unit File

Save this file as /etc/systemd/system/pyvorin-edge.service. It references the exact CLI entry point and default config paths used by EdgeAgent in /var/www/pyvorin/edge_runtime/pyv_edge_agent/main.py.

[Unit]
Description=Pyvorin Edge Agent
Documentation=https://docs.pyvorin.com/edge
After=network-online.target time-sync.target
Wants=network-online.target time-sync.target

[Service]
Type=simple
User=pi
Group=pi
WorkingDirectory=/home/pi/pyvorin-edge
Environment="PYTHONUNBUFFERED=1"
Environment="PATH=/home/pi/.local/bin:/usr/local/bin:/usr/bin:/bin"
ExecStart=/home/pi/.local/bin/pyv-edge-agent --config /home/pi/pyvorin-edge/config.toml
ExecReload=/bin/kill -HUP $MAINPID

# Restart policy
Restart=always
RestartSec=5
StartLimitInterval=60s
StartLimitBurst=3

# Graceful shutdown
TimeoutStopSec=30
KillSignal=SIGTERM
SendSIGKILL=yes

# Resource limits
MemoryMax=512M
CPUQuota=80%
TasksMax=64

# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/home/pi/pyvorin-edge
PrivateTmp=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true

# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=pyvorin-edge

[Install]
WantedBy=multi-user.target
  

Understanding Each Section

[Unit] — Dependencies and Ordering

Directive Purpose
After=network-online.target Ensures network interfaces are configured before the agent starts. EdgeAgent initialises cloud sync and MQTT adapters during start(), so early launch causes unnecessary retries.
Wants=time-sync.target SQLite timestamps and cloud queue ordering depend on correct wall-clock time. This target waits for NTP or systemd-timesyncd.

[Service] — Runtime Behaviour

Directive Purpose
Type=simple The main process (pyv-edge-agent) runs in the foreground. systemd considers the service active as soon as the exec succeeds.
ExecStart=... Matches the CLI signature defined in main.py: pyv-edge-agent --config <path>. The --once flag is omitted so the agent enters run_loop().
Restart=always If the process exits for any reason — clean exit, uncaught exception, or signal — systemd restarts it automatically.
RestartSec=5 Waits 5 seconds before restarting. Prevents tight restart loops that could overwhelm the CPU or log volume.
StartLimitBurst=3 If the service fails 3 times within 60 seconds, systemd stops trying and marks it as failed. This protects against permanently broken configurations.

Graceful Shutdown

The EdgeAgent.stop() method in main.py handles SIGTERM gracefully: it sets _running = False, signals _shutdown_event, stops the health HTTP server, joins threads with timeouts, and closes the SQLite store. systemd sends SIGTERM by default, so no extra configuration is required — but the timeouts must align.

# From /var/www/pyvorin/edge_runtime/pyv_edge_agent/main.py

def stop(self) -> None:
    if not self._running:
        return
    logger.info("EdgeAgent stopping...")
    self._running = False
    self._shutdown_event.set()
    self._stop_health_server()
    if self._thread:
        self._thread.join(timeout=10.0)
        self._thread = None
    if self._store:
        self._store.close()
        self._store = None
    logger.info("EdgeAgent stopped")
  

The unit file sets TimeoutStopSec=30, which is generous: the agent needs at most 10 seconds for the main thread plus a few seconds for journal flush. If the process does not exit within 30 seconds, systemd sends SIGKILL.

Resource Limits

Edge devices run on constrained hardware. The directives below prevent a runaway Python process from starving the OS or causing thermal throttling.

Memory Limits

MemoryMax=512M
  

If the EdgeAgent and its buffers exceed 512 MiB, the kernel's OOM killer terminates the process. systemd then restarts it according to Restart=always. For a Pi 4 with 2 GB RAM, 512 MiB leaves headroom for the OS, MQTT broker, and monitoring agents. On a Pi Zero 2 W with 512 MB RAM, reduce this to 256M.

CPU Quota

CPUQuota=80%
  

Caps the service to 80% of one CPU core. This prevents a tight loop in a custom adapter from starving systemd, SSH, or the health endpoint thread. The EdgeAgent's main loop in run_loop() already uses _shutdown_event.wait(timeout=interval) to avoid busy-waiting, but defensive limits are essential in production.

Task Limits

TasksMax=64
  

Prevents fork bombs or runaway thread creation. The EdgeAgent normally uses four threads: main loop, health HTTP server, SQLite connection pool worker, and cloud uploader.

Journald Logging

With StandardOutput=journal and StandardError=journal, all logs are captured by systemd-journald. Query them with:

# Follow live logs
journalctl -u pyvorin-edge -f

# Logs from the last hour, no pager
journalctl -u pyvorin-edge --since "1 hour ago" --no-pager

# Export last 24 hours to a file for support
journalctl -u pyvorin-edge --since "24 hours ago" > /tmp/edge-logs.txt

# Filter by syslog identifier (useful if multiple edge instances run)
journalctl -t pyvorin-edge --since today
  

Commands Reference

After installing or modifying the unit file, run these commands in order:

# Reload systemd to pick up the new unit
sudo systemctl daemon-reload

# Enable the service to start on boot
sudo systemctl enable pyvorin-edge

# Start immediately
sudo systemctl start pyvorin-edge

# Check status
sudo systemctl status pyvorin-edge

# Restart after config changes
sudo systemctl restart pyvorin-edge

# Reload configuration without restarting (if supported)
sudo systemctl kill -s HUP pyvorin-edge

# Stop and disable
sudo systemctl stop pyvorin-edge
sudo systemctl disable pyvorin-edge
  

Optional: Socket Activation

Socket activation allows systemd to listen on the health endpoint port (8080) before the EdgeAgent starts. The agent receives the bound socket as file descriptor 3. This is useful for two reasons:

  1. The health endpoint is available the instant the agent starts, eliminating a brief window where monitoring tools see connection refused.
  2. If the agent crashes, the socket remains bound, so health checks still connect (and receive a connection reset, which is a valid failure signal).

Socket Unit

sudo tee /etc/systemd/system/pyvorin-edge.socket > /dev/null <<'EOF'
[Unit]
Description=Pyvorin Edge Agent Health Socket

[Socket]
ListenStream=8080
BindIPv6Only=both
NoDelay=true

[Install]
WantedBy=sockets.target
EOF
  

Modified Service Unit

Add the socket dependency to the service unit:

[Unit]
Description=Pyvorin Edge Agent
Requires=pyvorin-edge.socket
After=pyvorin-edge.socket network-online.target

[Service]
# ... existing directives ...
# The agent would read the socket from LISTEN_FDS env var
# (Implementation in main.py would need a small patch to accept fd 3)
  

Summary

A well-configured systemd unit turns the EdgeAgent from a script into a resilient service. The combination of Restart=always, MemoryMax, CPUQuota, and TimeoutStopSec protects both the application and the host. Use journald for log aggregation, align shutdown timeouts with the agent's internal thread joins, and consider socket activation for zero-downtime health checks.