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:

ProcessVerdict
npm startNormal
next-serverNormal
/tmp/nodes --config /tmp/1.jsonWait β€” 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(). No child_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 COPY layer 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

StepAction
1Downloaded a fresh XMRig binary from a remote server
2Downloaded a pool configuration file
3Attempted cat .env .env.local .env.production
4Started 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 to moneroocean.stream, supportxmr.com, pool.minexmr.com, or nanopool.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