Your Docker Build Cache Might Be Hiding a Crypto Miner
ποΈ The Symptom β A Simple 504
A site we run started timing out. No drama. No spike. No bad deploy. Just a 504 Gateway Error on every request.
SSH in. The app is running. Port 3000 is listening. But the TCP queue has 17 connections piled up β the server stopped accepting them.
Run ps aux and you see three processes:
| Process | Verdict |
|---|---|
npm start | Normal |
next-server | Normal |
/tmp/nodes --config /tmp/1.json | Wait β what? |
An 8 MB binary. Sitting in /tmp. Running for hours.
π΅οΈ What We Found
That binary was XMRig β the most deployed cryptojacking malware in the wild.
{
"pools": [
{ "url": "gulf.moneroocean.stream:10128" },
{ "url": "pool.supportxmr.com:3333" }
],
"cpu": { "enabled": true }
}
It was mining Monero (XMR) , consuming every spare CPU cycle. The 504 was just a side effect β the Node.js event loop was being starved.
β οΈ The source code was clean. We audited every API route. Every dependency. Every import. No
eval(). Nochild_process. No RCE vector. The application itself was not the problem.
π¦οΈ The Real Culprit β Docker Build Cache
Here is how Docker builds work: if a layer's inputs do not change, Docker skips the work and reuses the cached layer.
At some point in the past, a compromised layer was produced during npm run build. It entered the cache. And because source files rarely changed between deploys, Docker kept serving that tainted cache for weeks.
#16 [builder 5/5] RUN npm run build
#16 CACHED <--- This is the problem
Every deploy was a fresh container running old, compromised JavaScript.
π‘οΈ A second environment on the same server was completely clean. Why? A single source file changed there, which busted the
COPYlayer and forced a full rebuild from scratch. Same repo. Same Dockerfile. Different outcome. This is how subtle cache poisoning is.
βοΈ What the Miner Did at Startup
| Step | Action |
|---|---|
| 1 | Downloaded a fresh XMRig binary from a remote server |
| 2 | Downloaded a pool configuration file |
| 3 | Attempted cat .env .env.local .env.production |
| 4 | Started mining on all available CPU cores |
π‘οΈ 6 Ways to Protect Your Deployments
1. Prune Your Build Cache
docker builder prune -af
In Dokploy, toggle Clean Cache on your app. In CI/CD, add a periodic cache invalidation step. Do not let your cache live forever.
2. Audit Dependencies Before Every Build
npm audit
npx lockfile-lint --path package-lock.json --type npm
Pay special attention to postinstall scripts β they run arbitrary code during npm ci and are the most common supply chain vector.
3. Scan Your Images
docker scout quickview <image>
trivy image <image>
Run these in CI. Block the deploy if a critical or high CVE appears.
4. Lock Base Images by Digest
# Risky β tag can be moved
FROM node:20-alpine
# Safe β pinned to an immutable checksum
FROM node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293
5. Mount /tmp with noexec
read_only: true
tmpfs:
- /tmp:noexec
This single flag would have completely prevented the attack. The binary could download but could never execute.
6. Monitor for Unexpected Processes
If your Node.js container suddenly spawns a binary called nodes from /tmp, you need to know immediately. Set up:
- Health checks that compare running processes against an allowlist
- CPU anomaly alerts (sustained 100% usage with no traffic = red flag)
- Outbound connection monitoring for mining pool domains
ποΈ Quick Self-Audit β Run These Now
# 1. Check what is actually running in your containers
docker exec <container> ps aux
# 2. Look for unexpected files in /tmp
docker exec <container> ls -la /tmp/
# 3. Check outbound connections
docker exec <container> ss -tlnp
# 4. See how much cached build data you are sitting on
docker builder du
βοΈ Red flags: processes named
nodes,xmrig,minerd, or anything running from/tmp. Outbound connections tomoneroocean.stream,supportxmr.com,pool.minexmr.com, ornanopool.org. Containers chewing CPU at 2 AM.
ποΈ The Takeaway
The Docker build cache is a performance feature β and a persistence mechanism. A compromised layer stays cached until you explicitly clear it or the build context changes.
Treat your cache like you treat your dependencies: trust nothing, verify everything, prune often.
# Add this to your deploy script today
docker builder prune --filter until=24h -f
Stay safe. βοΈ
Comments
No comments yet β be the first to share your thoughts.
Leave a Comment