I found a cryptominer in my self-hosted Plausible analytics container. The process continued to run after its file was deleted.
Last month, I noticed my home server was lagging a bit, not dramatically, but still. When I started looking at resource usage, I noticed my self-hosted Plausible analytics container was consuming about 800% CPU and 2.7 GiB of memory. Yikes.
Inside it, I found an unauthorized cryptocurrency miner running under the same user as my analytics application. Its executable had been deleted, its process name had been disguised, and the logs showed mining activity going back weeks.
Identifying the problem
The process consuming the CPU was a Linux executable whose original file had been deleted while it was still running.
On Linux, unlinking a running executable does not terminate the process. The filename disappears, but the underlying executable can remain accessible through /proc/[pid]/exe while the process is running, which is what allowed me to preserve it. I copied the executable through /proc without running the recovered file.
The process ran as UID 999, the same user as the Plausible application. Its command line presented it as linuxsys. A deleted executable and inconspicuous process name can make a process harder to notice during a casual inspection, but neither prevent recovery.
Identifying the malware
I identified the recovered executable as XMRig 6.26.0, an open-source cryptocurrency miner commonly used for Monero mining. XMRig is legitimate software; deploying it on someone else’s infrastructure without authorization is called cryptojacking.
The recovered file was 10,093,888 bytes, with this SHA-256 hash:
4d17d32ed6efe92ea61d16602f9c6cd5261cdd3820adec427ee68d4088b6ab5bI searched the recovered binary and logs for Monero mainnet wallet-address patterns, but could not identify a wallet. This does not mean that wallet information was not available via another configuration source.
The binary contained randomx.xmrig.com:443 which is associated with XMRig’s built-in developer-donation functionality. At the time of discovery, the process also had a socket associated with 103.189.234.47:80, in the CLOSE_WAIT state.
A mining proxy is one possible explanation for the absence of local payout details, but so is runtime configuration or deleted artifacts. I did not contact the endpoint and had no packet capture to establish a mining-pool login.
The likely entry point
The installed Plausible Community Edition image was v3.2.0, which falls within the versions affected by an unauthenticated remote-code-execution vulnerability involving its /storybook endpoint.
Plausible’s security advisory, published June 3, 2026, identifies versions from 3.0 up to, but excluding, 3.2.1 as affected. The issue relates to CVE-2026-8467 in PhoenixStorybook. Plausible’s patched image removes the vulnerable endpoint.
The application logs do not identify the HTTP request that initiated the compromise.
The timeline I constructed is as follows:
- Day 0: download attempt involving
/var/tmp/nodepm. I could not establish successful execution from that record. - Day 14: earliest confirmed miner launch in the logs.
- Day 15-20: repeated download and launch activity, including failed commands.
- Day 20: discovery of the live, CPU-heavy process.
- Day 20-1: patch
First steps - recovery & triage
I paused the compromised container and disabled its restart behavior while preserving its state.
Evidence collection included process and network details, container configuration, filesystem and application-volume archives, raw and exported logs, and the recovered executable.
I replaced the application with the patched Plausible release, pinned its image, and used a fresh application volume. I retained the database data after taking backups, but excluded the compromised application’s writable volume from the replacement. I also blocked the vulnerable endpoint in my reverse proxy.
Server security updates
The compromised application had no Windows bind mount, Docker socket, privileged mode, or shared host PID namespace.
Those boundaries reduced its direct access, but they did not make compromise harmless. The application could still access the internet and internal credentials. All impacted credentials were rotated.
To Docker, I applied resource limits, tighter capabilities, privilege restrictions, and outbound network controls, reducing the opportunities available to a future compromised container.
“Inside a container” still means code is running on your machine
The miner was a Linux process inside a Docker container, running through Docker Desktop’s Linux environment on my Windows machine, consuming my hardware and electricity.
I found no evidence establishing a container escape, Windows compromise, or file exfiltration.
Available Windows telemetry had significant gaps. Process-creation auditing and Task Scheduler operational logging were not enabled at collection time, and no Sysmon telemetry was available.
What I am taking from this
I self-host analytics partly to reduce my reliance on third-party vendors. That choice also puts responsibility for updates, access controls, monitoring, and recovery on me, something I've taken more seriously in the last few weeks.
The running miner was removed, the application was replaced with a patched image, credentials were rotated, and our services are now more tightly isolated. I have preserved evidence for further review, so reach out with any questions, comments, or concerns.