Why I Started Tracking Power Consumption in Containers
I run several ARM single-board computers in my homelab—mainly Raspberry Pi 4s and an Orange Pi 5. These boards handle various services through Docker containers managed by Portainer. The problem I kept hitting wasn't disk space or network bandwidth. It was thermal throttling.
When multiple containers spiked CPU usage simultaneously, the boards would heat up past 80°C, trigger thermal protection, and suddenly everything would slow down. Services would timeout, monitoring would miss intervals, and I'd get alerts about unresponsive endpoints. The frustrating part was that I couldn't predict when this would happen just by looking at CPU percentages.
I needed to understand actual power draw at the container level, not just abstract CPU metrics. That's when I started looking into RAPL (Running Average Power Limit) metrics and how to use them with Portainer to set smarter resource limits.
What RAPL Actually Measures on ARM
RAPL was originally an Intel thing for x86 processors. On ARM boards, the equivalent comes from the kernel's power monitoring interfaces, specifically through hwmon and the INA219/INA3221 power sensors that many SBCs include on their boards.
On my Raspberry Pi 4, I can read power consumption from /sys/class/hwmon/hwmon*/power1_input. This gives me real-time power draw in microwatts. The Orange Pi 5 exposes similar data through its RK3588 SoC monitoring.
The key insight: a container using 50% CPU might draw 2 watts or 4 watts depending on what it's actually doing. Transcoding video hits power differently than parsing JSON. CPU percentage alone doesn't tell you when you're about to hit thermal limits.
My Setup for Container Power Monitoring
I'm running Portainer 2.19 on these boards to manage containers. Portainer doesn't natively expose power metrics, so I had to build a monitoring layer that feeds data back into my decision-making process for resource limits.
The Monitoring Stack
I set up a lightweight container running a Python script that:
- Reads from
/sys/class/hwmonevery 5 seconds - Correlates power spikes with active containers using
docker statsoutput - Writes metrics to a local InfluxDB instance
- Triggers webhooks to n8n when power draw exceeds thresholds
The script runs privileged because it needs host-level access to hwmon interfaces. I mount /sys/class/hwmon as read-only into the container.
docker run -d \ --name power-monitor \ --privileged \ -v /sys/class/hwmon:/sys/class/hwmon:ro \ -v /var/run/docker.sock:/var/run/docker.sock:ro \ -e INFLUX_URL=http://influxdb:8086 \ -e WEBHOOK_URL=http://n8n:5678/webhook/power-alert \ power-monitor:latest
The Python code itself is straightforward. It reads the power sensor, parses container stats, and does basic correlation by timestamp. I'm not doing anything fancy with machine learning or predictions—just tracking which containers are active when power spikes happen.
What I Learned About Power Patterns
After running this for about two weeks, clear patterns emerged:
- My Jellyfin container doing software transcoding would spike to 8-9 watts on the Pi 4
- n8n workflows with heavy JSON processing hit 3-4 watts
- Syncthing during sync operations would pull 2-3 watts consistently
- Idle containers with just logging and health checks stayed under 0.5 watts
The total board draw when everything was calm sat around 4-5 watts. But when Jellyfin, n8n, and Syncthing all fired up together, I'd see 14-15 watts, and that's when the SoC temperature would climb past 75°C within minutes.
Setting Resource Limits Based on Power Budget
Once I had real power data, I could set container resource limits in Portainer that actually prevented thermal issues instead of just capping CPU percentages arbitrarily.
My Approach
I decided on a power budget system:
- Total sustainable power: 10 watts (keeps temperature under 70°C with passive cooling)
- Reserve 2 watts for system overhead and idle containers
- Allocate remaining 8 watts across active workload containers
In Portainer, I can't set power limits directly, but I can use CPU quotas and memory limits to constrain containers in ways that indirectly control power draw.
For each container, I calculated what CPU limit would keep it under its power allocation based on my observed data:
- Jellyfin: 0.5 CPU cores max (limits transcoding, keeps it around 4 watts)
- n8n: 0.3 CPU cores max (prevents runaway workflows, stays under 2 watts)
- Syncthing: 0.2 CPU cores max (slows sync but prevents spikes, around 1.5 watts)
I set these in Portainer's container settings under "Runtime & Resources" using the CPU limit field. The values are in CPU shares, where 1.0 equals one full core.
Dynamic Adjustment via n8n
Static limits worked, but they were inefficient. If only one container was active, it couldn't use available thermal headroom. So I built an n8n workflow that:
- Receives power alerts from the monitoring container
- Checks current container power consumption from InfluxDB
- Adjusts CPU limits via Portainer API based on available power budget
The workflow uses Portainer's API endpoint PUT /api/endpoints/{id}/docker/containers/{containerId}/update to modify CPU quotas on the fly.
When total power is under 8 watts, it relaxes limits. When approaching 10 watts, it tightens them proportionally across containers based on priority tags I set in Portainer labels.
What Didn't Work
Trying to Use cgroups Directly
I initially tried to set power limits using cgroups v2 directly instead of going through Portainer. The idea was to use the CPU controller's bandwidth settings to enforce power budgets at the kernel level.
This failed because:
- ARM's hwmon interfaces don't integrate with cgroups power tracking
- I couldn't reliably map power consumption to cgroup CPU bandwidth values
- Managing cgroups outside of Docker's control plane caused conflicts with Portainer's state tracking
I wasted about a week on this before accepting that working through Docker's resource limits was the practical path.
Over-Aggressive Throttling
My first attempt at dynamic adjustment was too reactive. When power spiked, the n8n workflow would immediately cut CPU limits in half. This caused containers to stall mid-operation, which actually increased overall power consumption because tasks took longer to complete.
I had to add hysteresis—the workflow now waits for 30 seconds of sustained high power before adjusting, and it ramps limits gradually rather than making sudden cuts.
False Correlation with Network I/O
Early on, I thought network-heavy containers like Syncthing were power-hungry because they correlated with spikes. Turns out the Pi's network controller shares thermal budget with the CPU, so network saturation can trigger throttling even without high CPU use.
I ended up having to track network I/O separately and include it in my power budget calculations, even though it's not directly measured by hwmon.
Current State and Practical Results
After three months of running this setup, thermal throttling events dropped from 5-6 times per week to maybe once every two weeks, and those are usually because I manually triggered something intensive without adjusting limits first.
The boards now run cooler—average SoC temperature is 62-65°C under normal load instead of bouncing between 70-80°C. This probably extends hardware lifespan, though I won't know for sure for years.
The monitoring overhead is minimal. The power-monitor container uses about 1% CPU and 40MB RAM. InfluxDB adds another 100MB, but I was already running it for other metrics.
Limitations I'm Living With
- Power measurement accuracy is ±10% according to the sensor datasheets, so my budgets have built-in margin
- The system doesn't account for ambient temperature changes—summer heat requires tighter limits
- I still have to manually tune priority labels when I add new containers
- This whole approach assumes passive cooling; active cooling would change the math entirely
Key Takeaways
Power consumption is a better predictor of thermal throttling than CPU percentage on ARM SBCs. The relationship isn't linear, and different workloads have different power profiles even at the same CPU utilization.
Portainer doesn't expose power metrics natively, but its API makes it possible to build external control loops that adjust resource limits based on real power data.
Dynamic adjustment works better than static limits, but it needs damping to avoid oscillation and stalls.
If you're running containers on hardware with thermal constraints, monitoring power at the container level gives you actionable data that CPU and memory metrics don't provide.
The setup I described isn't turnkey. It requires access to hardware power sensors, a way to correlate that data with container activity, and automation to act on it. But for anyone already running Portainer and n8n in a homelab, the pieces are there to build something similar.