Introduction
Moving a Pyvorin Edge device from the lab bench to the field requires more than copying a config file. This checklist covers every step an operator must complete before a device is considered production-ready: hardware validation, OS hardening, service configuration, data protection, and observability. Follow it in order. Skip nothing.
Pre-Flight Hardware Checks
Before you flash an SD card or power on for the first time, verify the physical layer. Field failures are expensive — a £45 site visit to replace a £12 SD card is not uncommon.
SD Card Health
Consumer SD cards fail silently. Check for bad blocks and wear indicators before imaging:
# Install f3 if not present
sudo apt update && sudo apt install -y f3
# Test the card (replace /dev/sdX with your device)
sudo f3write /media/pi/TEST && sudo f3read /media/pi/TEST
# Check SMART-like wear data (if available)
sudo smartctl -a /dev/sdX 2>/dev/null || echo "SMART not available for this reader"
Power Supply Validation
Raspberry Pi 4 boards require a stable 5.1V/3A supply. Undervoltage causes random corruption and silent reboots.
# Monitor under-voltage events
vcgencmd get_throttled
# If bit 0 is set, the board has experienced under-voltage since boot.
# Continuous voltage monitoring (run for 60 seconds)
for i in {1..60}; do
vcgencmd measure_volts
done
Network Connectivity
Verify that the EdgeAgent can reach its cloud endpoint and MQTT broker before sealing the enclosure:
# Basic reachability
ping -c 4 api.pyvorin.com
# Verify HTTPS egress (cloud sync)
curl -I https://api.pyvorin.com/v1/health
# Verify MQTT egress (if using MQTT ingest)
nc -vz mqtt.broker.local 8883
# DNS resolution speed
dig api.pyvorin.com +stats | grep "Query time"
Security Hardening
The default Raspberry Pi OS image is designed for convenience, not security. Every unused service is an attack surface.
Disable Unused Services
# Stop and mask services that have no role on an edge device
sudo systemctl stop avahi-daemon bluetooth
sudo systemctl disable avahi-daemon bluetooth
sudo systemctl mask avahi-daemon bluetooth
# Disable wireless if using Ethernet only
sudo rfkill block wifi
sudo rfkill block bluetooth
# Verify what is still running
sudo systemctl list-units --type=service --state=running
Configure UFW Firewall
Only open ports that the EdgeAgent actually needs. The health endpoint defaults to 8080; restrict it to localhost unless you have an explicit remote monitoring requirement.
sudo apt install -y ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
# SSH (restrict to your management subnet if possible)
sudo ufw allow from 10.0.0.0/8 to any port 22 proto tcp
# MQTT broker (if broker runs on this device)
sudo ufw allow 8883/tcp
# Health endpoint — localhost only
sudo ufw allow from 127.0.0.1 to any port 8080 proto tcp
sudo ufw enable
sudo ufw status verbose
SSH Key-Only Authentication
# On your workstation, copy your public key
ssh-copy-id pi@edge-device.local
# On the edge device, harden sshd
sudo sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin no/' /etc/ssh/sshd_config
sudo systemctl restart ssh
Systemd Service Setup for EdgeAgent
The EdgeAgent must survive reboots, crashes, and power glitches. systemd is the correct tool for this job on Linux.
sudo tee /etc/systemd/system/pyvorin-edge.service > /dev/null <<'EOF'
[Unit]
Description=Pyvorin Edge Agent
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=pi
Group=pi
WorkingDirectory=/home/pi/pyvorin-edge
ExecStart=/home/pi/.local/bin/pyv-edge-agent --config /home/pi/pyvorin-edge/config.toml
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable pyvorin-edge
sudo systemctl start pyvorin-edge
After starting, confirm the service is healthy:
sudo systemctl status pyvorin-edge
journalctl -u pyvorin-edge --since "5 minutes ago" --no-pager
Log Rotation with logrotate
Edge devices write to SD cards with limited write endurance. Unbounded logs will fill the filesystem and accelerate wear. The EdgeAgent outputs structured JSON logs to stdout, which journald captures. We configure journald and logrotate together.
Journald Disk Limits
sudo tee /etc/systemd/journald.conf.d/00-edge-limits.conf > /dev/null <<'EOF'
[Journal]
SystemMaxUse=256M
SystemMaxFileSize=32M
MaxFileSec=1week
EOF
sudo systemctl restart systemd-journald
Logrotate for Custom Log Files
If you redirect EdgeAgent output to a file (not recommended; use journald), add a logrotate rule:
sudo tee /etc/logrotate.d/pyvorin-edge > /dev/null <<'EOF'
/home/pi/pyvorin-edge/logs/*.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
create 0640 pi pi
}
EOF
Backup Strategy for the SQLite Database
The EdgeAgent stores sensor readings, events, and queue state in SQLite. The default path is edge_store.db, and the cloud sync queue defaults to sync_queue.db. Both live in the working directory unless configured otherwise in config.toml.
Automated Nightly Backup
sudo tee /usr/local/bin/edge-db-backup.sh > /dev/null <<'EOF'
#!/bin/bash
set -euo pipefail
BACKUP_DIR="/home/pi/pyvorin-edge/backups"
DB_DIR="/home/pi/pyvorin-edge"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"
# SQLite online backup using the built-in backup API
sqlite3 "$DB_DIR/edge_store.db" ".backup '$BACKUP_DIR/edge_store_$DATE.db'"
sqlite3 "$DB_DIR/sync_queue.db" ".backup '$BACKUP_DIR/sync_queue_$DATE.db'"
# Keep only the last 14 backups
ls -t "$BACKUP_DIR"/edge_store_*.db | tail -n +15 | xargs -r rm -f
ls -t "$BACKUP_DIR"/sync_queue_*.db | tail -n +15 | xargs -r rm -f
# Verify the latest backup integrity
sqlite3 "$BACKUP_DIR/edge_store_$DATE.db" "PRAGMA integrity_check;"
EOF
sudo chmod +x /usr/local/bin/edge-db-backup.sh
# Schedule via cron at 02:17 every night
(crontab -l 2>/dev/null; echo "17 2 * * * /usr/local/bin/edge-db-backup.sh >> /var/log/edge-backup.log 2>&1") | crontab -
Monitoring Alerts
The EdgeAgent exposes system metrics via GET /health and GET /metrics on port 8080. The underlying collector is SystemMetrics in /var/www/pyvorin/edge_runtime/pyv_edge_agent/health_monitor/metrics.py, which reads /proc/stat, /proc/meminfo, /sys/class/thermal/thermal_zone0/temp, and shutil.disk_usage.
Simple Shell Health Monitor
For small fleets, a cron-based monitor is sufficient before investing in Prometheus or Datadog:
sudo tee /usr/local/bin/edge-health-check.sh > /dev/null <<'EOF'
#!/bin/bash
# Post-deployment verification and alerting script
HEALTH_URL="http://127.0.0.1:8080/health"
ALERT_WEBHOOK="${ALERT_WEBHOOK:-}"
LOG_FILE="/var/log/edge-health.log"
log() {
echo "$(date -Iseconds) $1" | tee -a "$LOG_FILE"
}
# Fetch health JSON
HEALTH_JSON=$(curl -s -m 5 "$HEALTH_URL" 2>/dev/null) || {
log "CRITICAL: Health endpoint unreachable"
[ -n "$ALERT_WEBHOOK" ] && curl -s -X POST -H "Content-Type: application/json" \
-d '{"text":"EdgeAgent health endpoint unreachable"}' "$ALERT_WEBHOOK" > /dev/null
exit 1
}
# Extract metrics via jq (install with: sudo apt install jq)
CPU=$(echo "$HEALTH_JSON" | jq -r '.metrics.cpu_percent // 0')
DISK=$(echo "$HEALTH_JSON" | jq -r '.metrics.disk_percent // 0')
THERMAL=$(echo "$HEALTH_JSON" | jq -r '.metrics.thermal_celsius // 0')
QUEUE=$(echo "$HEALTH_JSON" | jq -r '.cloud.queue_depth // 0')
# Thresholds
CPU_LIMIT=85
DISK_LIMIT=90
THERMAL_LIMIT=75
QUEUE_LIMIT=5000
CRIT=0
if (( $(echo "$CPU > $CPU_LIMIT" | bc -l) )); then
log "WARNING: CPU at ${CPU}%"
CRIT=1
fi
if (( $(echo "$DISK > $DISK_LIMIT" | bc -l) )); then
log "CRITICAL: Disk at ${DISK}%"
CRIT=1
fi
if (( $(echo "$THERMAL > $THERMAL_LIMIT" | bc -l) )); then
log "WARNING: Thermal at ${THERMAL}°C"
CRIT=1
fi
if (( QUEUE > QUEUE_LIMIT )); then
log "WARNING: Cloud queue depth ${QUEUE}"
CRIT=1
fi
if [ "$CRIT" -eq 0 ]; then
log "OK: CPU=${CPU}% DISK=${DISK}% THERMAL=${THERMAL}°C QUEUE=${QUEUE}"
fi
exit 0
EOF
sudo chmod +x /usr/local/bin/edge-health-check.sh
sudo apt install -y jq bc
# Run every 5 minutes
(crontab -l 2>/dev/null; echo "*/5 * * * * /usr/local/bin/edge-health-check.sh") | crontab -
Post-Deployment Verification Script
The following script is a single, copy-pasteable bash checklist. Run it immediately after every deployment. It returns exit code 0 only if all checks pass.
#!/bin/bash
# Pyvorin Edge — Production Deployment Verification
# Run as: sudo bash verify-deployment.sh
set -uo pipefail
ERRORS=0
pass() { echo " [PASS] $1"; }
fail() { echo " [FAIL] $1"; ((ERRORS++)); }
echo "=== Pyvorin Edge Deployment Verification ==="
# 1. Service status
if systemctl is-active --quiet pyvorin-edge; then
pass "pyvorin-edge service is running"
else
fail "pyvorin-edge service is not running"
fi
# 2. Health endpoint
HEALTH=$(curl -s -m 5 http://127.0.0.1:8080/health 2>/dev/null)
if [ -n "$HEALTH" ]; then
pass "Health endpoint responds"
STATUS=$(echo "$HEALTH" | python3 -c "import sys,json; print(json.load(sys.stdin)['status'])")
if [ "$STATUS" = "healthy" ]; then
pass "Agent status is healthy"
else
fail "Agent status is $STATUS"
fi
else
fail "Health endpoint unreachable"
fi
# 3. Config file exists and is readable
CONFIG="/home/pi/pyvorin-edge/config.toml"
if [ -r "$CONFIG" ]; then
pass "Config file readable: $CONFIG"
else
fail "Config file missing or unreadable: $CONFIG"
fi
# 4. SQLite databases are writable
DB_DIR="/home/pi/pyvorin-edge"
for db in edge_store.db sync_queue.db; do
if [ -w "$DB_DIR/$db" ]; then
pass "Database writable: $db"
else
fail "Database not writable: $db"
fi
done
# 5. Backup directory exists
if [ -d "$DB_DIR/backups" ]; then
pass "Backup directory exists"
else
fail "Backup directory missing"
fi
# 6. Log rotation configured
if [ -f /etc/logrotate.d/pyvorin-edge ] || [ -d /etc/systemd/journald.conf.d ]; then
pass "Log rotation configured"
else
fail "Log rotation not configured"
fi
# 7. Firewall active
if sudo ufw status | grep -q "Status: active"; then
pass "UFW firewall is active"
else
fail "UFW firewall is not active"
fi
# 8. SSH password auth disabled
if grep -q "^PasswordAuthentication no" /etc/ssh/sshd_config; then
pass "SSH password authentication disabled"
else
fail "SSH password authentication still enabled"
fi
# 9. Thermal within range
THERMAL=$(vcgencmd measure_temp 2>/dev/null | sed "s/temp=//;s/'C//") || THERMAL="0"
if (( $(echo "$THERMAL < 80" | bc -l) )); then
pass "SoC thermal: ${THERMAL}°C"
else
fail "SoC thermal too high: ${THERMAL}°C"
fi
# 10. Disk usage
DISK_USAGE=$(df / | tail -1 | awk '{print $5}' | sed 's/%//')
if [ "$DISK_USAGE" -lt 90 ]; then
pass "Root disk usage: ${DISK_USAGE}%"
else
fail "Root disk usage critical: ${DISK_USAGE}%"
fi
# Summary
echo ""
if [ "$ERRORS" -eq 0 ]; then
echo "=== ALL CHECKS PASSED ==="
exit 0
else
echo "=== $ERRORS CHECK(S) FAILED ==="
exit 1
fi
Summary
A production Pyvorin Edge deployment is not complete until hardware is validated, the OS is hardened, the agent runs under systemd, logs rotate automatically, SQLite backups run nightly, and health monitoring is in place. Use the verification script after every install. Automate what you can. Document what you cannot.