{"slug": "your-docker-builds-are-slow-because-you-re-doing-it-wrong-and-i-built-a-tool-to", "title": "Your Docker Builds Are Slow Because You're Doing It Wrong (And I Built a Tool to Prove It)", "summary": "The article explains that Docker builds are slow because developers often write Dockerfiles incorrectly, causing the layer cache to break unnecessarily. The key mistake is copying all source code before installing dependencies, which forces a full reinstall on every code change. The author created a tool called LayerLint to detect these caching anti-patterns in Dockerfiles.", "body_md": "## Stop waiting 10 minutes for CI to rebuild everything when you change one line of code. Here's what's actually breaking your Docker layer cache.\"\n\n*This post is about Docker layer caching and why your builds probably take way longer than they should. If you've ever sat there watching Docker reinstall npm packages for the 10th time today after changing one line, yeah this is for you.*\n\n## The Pain\n\nOkay so. You know that feeling, right? You changed literally ONE line of code. Fixed a typo in a comment or whatever. Push it up, CI kicks off, and now you're sitting there watching Docker reinstall 400 npm packages. Again. For the third time today.\n\nAnd you're like \"why is this happening to me\"\n\nHere's the thing - your build isn't slow because your code sucks or because AWS is being slow. It's slow because **you wrote your Dockerfile wrong**. We all do it.\n\nI'm willing to bet you've got something like:\n\n```\nFROM node:18\nCOPY . .\nRUN npm install\n```\n\nYeah. That's the problem right there. Every code change = full npm install. Docker's layer cache? Gone. Destroyed. RIP.\n\nAfter explaining this to coworkers (and myself) like 50 times, I finally got fed up and built [LayerLint](https://github.com/vviveksharma/layerLint). It's basically a tool that looks at your Dockerfile and tells you when you messed up.\n\n(Also it was a good excuse to write some Go code. But mainly the other thing.)\n\n## How Docker Caching Actually Works (The 5-Minute Version)\n\nSo nobody really explains this when you're learning Docker. They just tell you to write a Dockerfile and good luck. But here's what's actually happening: **every line in your Dockerfile creates a layer**. These layers get cached. Which is great! Except the cache breaks super easily.\n\nBest way I can explain it - imagine a stack of pancakes. (I'm hungry, don't judge.) Each line (`FROM`\n\n, `RUN`\n\n, `COPY`\n\n, whatever) is one pancake. Docker caches each pancake. So far so good.\n\nBut then. Here's where it gets annoying: **if you change one pancake, Docker throws away that pancake plus every single pancake above it**.\n\nSo like, if you change pancake 3, pancakes 3, 4, 5, 6, and 7 all get tossed. Only pancakes 1 and 2 stay cached.\n\nThis whole thing is called the \"invalidation chain\" and it's literally why your builds take forever.\n\n### The Cache-Killer Pattern\n\nHere's the classic mistake everyone makes (including me for like 2 years):\n\n```\nFROM node:18\nWORKDIR /app\n\nCOPY . .                    # Layer 1: Copy everything\nRUN npm install             # Layer 2: Install dependencies\nRUN npm run build           # Layer 3: Build\n\nCMD [\"node\", \"dist/server.js\"]\n```\n\nOkay so what happens when you change `src/index.js`\n\n?\n\n- Layer 1 (\n`COPY . .`\n\n) sees something changed ❌ - Cache = broken\n- Layer 2 (\n`npm install`\n\n) has to run again ❌ - Layer 3 (\n`npm run build`\n\n) runs again too ❌\n\nYou literally just reinstalled 400 packages because you changed one file. And Docker's sitting there like \"yep, seems right\".\n\n### The Fix\n\nAlright here's the actual correct way:\n\n```\nFROM node:18\nWORKDIR /app\n\nCOPY package.json package-lock.json ./   # Layer 1: Copy deps manifest\nRUN npm install                           # Layer 2: Install (cached!)\nCOPY . .                                  # Layer 3: Copy source\nRUN npm run build                         # Layer 4: Build\n\nCMD [\"node\", \"dist/server.js\"]\n```\n\nNow when you change `src/index.js`\n\n:\n\n- Layer 1 (\n`package*.json`\n\n) - didn't change ✅ (uses cache) - Layer 2 (\n`npm install`\n\n) - didn't change ✅ (uses cache) - Layer 3 (\n`COPY . .`\n\n) - okay this changed, rebuild - Layer 4 (\n`npm run build`\n\n) - gotta rebuild this too\n\nBut look - layers 1 and 2 stayed cached! You just saved like 2-8 minutes every single build.\n\nThis pattern works everywhere btw:\n\n- Node →\n`package.json`\n\n+`package-lock.json`\n\n- Go →\n`go.mod`\n\n+`go.sum`\n\n- Python →\n`requirements.txt`\n\nor`poetry.lock`\n\n- Rust →\n`Cargo.toml`\n\n+`Cargo.lock`\n\nSame idea. Copy the dependency files first, install them, THEN copy your actual code.\n\n## Meet LayerLint: The Dockerfile Linter You Didn't Know You Needed\n\nSo I've seen this same mistake approximately 47 times. Someone (usually me) puts `COPY . .`\n\nbefore `npm install`\n\nand then wonders why builds take forever. Eventually I just got annoyed enough to build something about it.\n\nThat's LayerLint.\n\n**What is it?**\n\nIt's a static analysis tool I wrote in Go. You point it at your Dockerfile, it reads through it and goes \"yo, this is gonna be slow\". Doesn't even need to build the image. Just looks at the file.\n\n**Why not use hadolint or whatever?**\n\nHadolint is great for syntax stuff and general best practices. LayerLint is specifically focused on **layer caching anti-patterns**. The stuff that makes your builds slow. Different problem.\n\nThink of it like having that one DevOps person who's always grumpy about Dockerfiles, except it runs instantly and won't send you passive-aggressive Slack messages.\n\n## The Walkthrough: Let's Break Some Stuff\n\n### Installation (Pick Your Poison)\n\nThere's like 3 different ways to install it depending on how paranoid you are:\n\n**The lazy way (this is what I use):**\n\n```\ncurl -sSL https://raw.githubusercontent.com/vviveksharma/layerLint/main/install.sh | sh\n```\n\n**The \"I don't trust random install scripts\" way (fair enough):**\n\n```\n# Grab the binary from releases\nwget https://github.com/vviveksharma/layerLint/releases/latest/download/layerLint_Linux_x86_64.tar.gz\ntar -xzf layerLint_Linux_x86_64.tar.gz\nchmod +x layerlint\n```\n\n**The \"I'm gonna build it from source\" way (respect):**\n\n```\ngit clone https://github.com/vviveksharma/layerLint\ncd layerLint\nmake generate-build\n```\n\n### The Scan\n\nLet me use that bad Dockerfile from before:\n\n```\nFROM golang:1.22\nWORKDIR /app\nCOPY . .\nRUN go mod download\nRUN go build -o server ./cmd/server\n```\n\nNow run LayerLint on it:\n\n```\n./layerlint scan --dockerfile Dockerfile\n```\n\n### The Output\n\n```\n╔══════════════════════════════════════════════════════════════════╗\n║                        LayerLint Report                          ║\n╚══════════════════════════════════════════════════════════════════╝\n\nRuleID:     dockerfile/broad-copy-before-deps\nSeverity:   high\nFile:       Dockerfile\nLine:       3\nTitle:      Dependency install runs after broad source copy\nMessage:    This dependency step runs after a broad COPY/ADD, so source \n            changes can invalidate the dependency cache.\nSuggestion: Copy dependency manifests first (go.mod, go.sum), install \n            dependencies, then copy the rest of the source.\n\nExample Fix:\n  COPY go.mod go.sum ./\n  RUN go mod download\n  COPY . .\n\n═══════════════════════════════════════════════════════════════════\n\nFound 1 violation:\n  - High:   1\n  - Medium: 0\n  - Low:    0\n```\n\nPretty nice right? It basically tells you:\n\n- What you did wrong (broad copy before deps)\n- Where exactly it is (line 3, can't miss it)\n- Why this is bad (breaks the cache)\n- How to actually fix it (copy manifests first)\n\nNo guessing. No googling \"why is my docker build slow reddit\". Just tells you straight up.\n\n### Other Rules It Catches\n\nLayerLint checks for a bunch of other stuff too that'll eventually bite you:\n\n| Rule | Severity | Why You Should Care |\n|---|---|---|\n`unpinned-base-image-tag` |\nMedium |\n`:latest` means your builds aren't reproducible (bad) |\n`copying-secrets-into-image` |\nHigh | Secrets live forever in layer history even if you delete them |\n`run-as-root` |\nHigh | Security issue, containers shouldn't run as root |\n`missing-dockerignore` |\nMedium | Slow builds + might leak secrets |\n`apt-update-without-install` |\nMedium | Package cache goes stale |\n`build-without-cache-mount` |\nLow | Missing BuildKit cache mounts (free speed) |\n\nThere's more. Check the [full rules docs](https://github.com/vviveksharma/layerLint/blob/main/docs/rules.md) if you're curious.\n\n## The Real Win: Automation with GitHub Actions\n\nOkay so finding issues is one thing. But the real win? Making it so nobody (including yourself in 3 months when you forget all this) can merge a bad Dockerfile.\n\nHere's what you do. Add this to `.github/workflows/docker-lint.yml`\n\n:\n\n```\nname: Lint Dockerfile\non: [push, pull_request]\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Install LayerLint\n        run: |\n          curl -sSL https://raw.githubusercontent.com/vviveksharma/layerLint/main/install.sh | sh\n\n      - name: Scan Dockerfile\n        run: ./layerlint scan --dockerfile ./Dockerfile --fail-on-severity high\n```\n\nAnd boom. Now every PR gets checked automatically. Someone tries to merge a slow Dockerfile? CI says no. PR blocked. They gotta fix it first.\n\n(Saved me from myself more times than I can count.)\n\nOh and LayerLint can output different formats if you need:\n\n-\n`--format json`\n\nif you're doing scripting stuff -\n`--format sarif`\n\nif you want it in GitHub's Security tab -\n`--format html`\n\nfor when you need to show management something pretty\n\n### Real-World Example Workflows\n\nI put some example workflows in the repo that you can just copy:\n\nBasically just copy the yaml, change the paths to match your repo, commit it. Done.\n\n## The Before & After: Show Me the Numbers\n\nAlright let me show you actual numbers from a real project I fixed.\n\n**Setup:** Node.js app, 342 npm packages (yeah it's a lot don't judge), changed one line in a component\n\n**Before (the bad way):**\n\n```\nFROM node:18\nCOPY . .\nRUN npm ci\nRUN npm run build\n```\n\nBuild time: **8 minutes 32 seconds**\n\nEvery. Single. Time.\n\n**After (fixed it):**\n\n```\nFROM node:18\nCOPY package.json package-lock.json ./\nRUN npm ci\nCOPY . .\nRUN npm run build\n```\n\nBuild time: **1 minute 12 seconds**\n\nThat's 7 minutes saved per build. I push like 10 times a day sometimes (don't we all?), so that's an hour saved. Every day. Just from fixing the Dockerfile.\n\nDo the yearly math on that and it's honestly kinda crazy. That's like... multiple work weeks just sitting there waiting for npm install.\n\nAnd this is actual time. Not theoretical. Real minutes you get back to:\n\n- Actually write code\n- Read docs (lol who am I kidding)\n- Get coffee\n- Scroll through Twitter/X or whatever we're calling it now\n- Take a walk\n- Pet your dog\n- Literally anything that isn't watching a progress bar slowly fill up\n\n## Other Platforms (Because Not Everyone Uses GitHub)\n\nLayerLint works on whatever CI system you're using:\n\n**GitLab CI:**\n\n```\ndocker-lint:\n  script:\n    - curl -sSL https://raw.githubusercontent.com/vviveksharma/layerLint/main/install.sh | sh\n    - ./layerlint scan --dockerfile Dockerfile\n```\n\n**CircleCI:**\n\n```\n- run:\n    name: Lint Dockerfile\n    command: |\n      curl -sSL https://raw.githubusercontent.com/vviveksharma/layerLint/main/install.sh | sh\n      ./layerlint scan --dockerfile Dockerfile\n```\n\n**Jenkins:**\n\n```\nsh 'curl -sSL https://raw.githubusercontent.com/vviveksharma/layerLint/main/install.sh | sh'\nsh './layerlint scan --dockerfile Dockerfile'\n```\n\nMore platforms in the [CI/CD guide](https://github.com/vviveksharma/layerLint/blob/main/docs/ci-cd-integration.md).\n\n**Pre-commit hook** (catch it before you even commit):\n\nAdd this to `.pre-commit-config.yaml`\n\n:\n\n```\nrepos:\n  - repo: local\n    hooks:\n      - id: layerlint\n        name: LayerLint\n        entry: layerlint scan --dockerfile\n        language: system\n        files: Dockerfile.*\n```\n\nNow it runs every time you commit. Catches mistakes before they even get pushed.\n\n## Pro-Tips for Maximum Speed\n\n### 1. Use `.dockerignore`\n\n(seriously please)\n\nI cannot stress this enough. Create a `.dockerignore`\n\nfile. Just do it:\n\n```\nnode_modules/\n.git/\ndist/\nbuild/\n*.log\n.env*\n*.md\n.github/\ntests/\n```\n\nDocker won't send all that junk to the build context. Faster builds, smaller images, and you won't accidentally leak your `.env`\n\nfile into production.\n\n(I may or may not have done that once. Not fun. Learn from my mistakes.)\n\n### 2. Enable BuildKit Cache Mounts\n\nIf you're not using BuildKit yet... start. And then use cache mounts:\n\n```\nRUN --mount=type=cache,target=/root/.npm \\\n    npm ci\n\nRUN --mount=type=cache,target=/go/pkg/mod \\\n    go mod download\n\nRUN --mount=type=cache,target=/root/.cache/pip \\\n    pip install -r requirements.txt\n```\n\nThis caches package downloads **between builds**. Not just within one build - between ALL of them. Absolute game changer.\n\nFirst time I set this up I thought something was broken because the build finished so fast. Nope, just working correctly for once.\n\n### 3. Multi-Stage Builds for Production\n\nAlso if you're shipping to prod, use multi-stage builds:\n\n```\n# Build stage\nFROM node:18 AS builder\nCOPY package*.json ./\nRUN npm ci\nCOPY . .\nRUN npm run build\n\n# Production stage\nFROM node:18-alpine\nCOPY --from=builder /app/dist ./dist\nCOPY --from=builder /app/node_modules ./node_modules\nCMD [\"node\", \"dist/server.js\"]\n```\n\nYour final image ends up way smaller, and deploys are faster. Win-win.\n\n## Contributing (Or: \"I Found a Bug / Have an Idea\")\n\nCode's pretty straightforward if you wanna contribute. Each rule is just its own file in `internal/rules/`\n\n. Wanna add a new rule?\n\n- Create\n`internal/rules/your_rule.go`\n\n- Implement the\n`Rule`\n\ninterface:\n\n```\ntype Rule interface {\n    ID() string\n    Check(dockerfile *parser.Result) []Finding\n}\n```\n\n- Register it in\n`internal/scanner/scanner.go`\n\n- Add a test case in\n`testFiles/`\n\nLook at [ broad_copy_before_deps.go](https://github.com/vviveksharma/layerLint/blob/main/internal/rules/broad_copy_before_deps.go) to see how it's done.\n\nPRs welcome. If you find a caching anti-pattern that LayerLint misses, definitely add it. I'm sure there's stuff I haven't thought of. The Go parser stuff is pretty straightforward once you get into it.\n\n(First few times looking at the Dockerfile parser I was confused but it makes sense eventually.)\n\n## Why I Built This\n\nHonestly? I got tired of:\n\n- Sitting around waiting for builds (so much time wasted)\n- Explaining the same Docker caching stuff over and over\n- Forgetting these rules myself and having to relearn them every few months\n- Watching our CI bill go up because of dumb mistakes (my manager wasn't happy about that one)\n\nSo yeah. Built a tool that explains it for me. Now when someone asks I just go \"run layerlint\" and we're good. Plus I can share the repo instead of typing the same explanation in Slack for the 100th time.\n\nIf this saves you 5 minutes a day, cool. If it saves your whole team hours every week? Even better. That's the goal.\n\n## The Bottom Line\n\n**Before:**\n\n- Builds take forever\n- Burning through CI minutes (and money)\n- Everyone's annoyed\n- \"Why is this so slow?\" (nobody knows)\n\n**After:**\n\n- Builds are actually fast\n- Cache works like it's supposed to\n- CI catches bad Dockerfiles automatically\n- You get your time back\n\nThe tool's free, it's open source, and it literally takes 30 seconds to run. So like... just try it? Worst case you wasted 30 seconds. Best case you save hours.\n\nI mean you've read this far, might as well give it a shot right?\n\n## Get Started\n\nAlright so if you wanna try it:\n\n```\n# Install it\ncurl -sSL https://raw.githubusercontent.com/vviveksharma/layerLint/main/install.sh | sh\n\n# Point it at your Dockerfile\n./layerlint scan --dockerfile Dockerfile\n\n# Fix whatever it complains about\n\n# Add it to your CI\n\n# Profit (aka faster builds)\n```\n\nUseful links:\n\n-\n**GitHub:**[github.com/vviveksharma/layerLint](https://github.com/vviveksharma/layerLint) -\n**All the rules:**[docs/rules.md](https://github.com/vviveksharma/layerLint/blob/main/docs/rules.md) -\n**CI setup guide:**[docs/ci-cd-integration.md](https://github.com/vviveksharma/layerLint/blob/main/docs/ci-cd-integration.md) -\n**Download page:**[github.com/vviveksharma/layerLint/releases](https://github.com/vviveksharma/layerLint/releases)\n\nIf it helps, star the repo maybe? If it doesn't help, open an issue and tell me what's broken.\n\nAnyway. Go fix your Dockerfiles. Future you will be grateful.\n\n*MIT License. Built it because I got lazy and tired of explaining Docker caching.*\n\n## Comments Section Starter\n\nWhat's your worst Dockerfile horror story? I wanna hear it. Drop it in the comments.\n\nBonus points if it involved `npm install`\n\nin production or a 2GB Docker image for a hello world app.", "url": "https://wpnews.pro/news/your-docker-builds-are-slow-because-you-re-doing-it-wrong-and-i-built-a-tool-to", "canonical_source": "https://dev.to/vivek_sharma_cb3c863dec26/your-docker-builds-are-slow-because-youre-doing-it-wrong-and-i-built-a-tool-to-prove-it-5f5c", "published_at": "2026-05-23 13:24:04+00:00", "updated_at": "2026-05-23 14:05:24.911349+00:00", "lang": "en", "topics": ["developer-tools", "open-source"], "entities": ["Docker", "LayerLint", "AWS", "Go"], "alternates": {"html": "https://wpnews.pro/news/your-docker-builds-are-slow-because-you-re-doing-it-wrong-and-i-built-a-tool-to", "markdown": "https://wpnews.pro/news/your-docker-builds-are-slow-because-you-re-doing-it-wrong-and-i-built-a-tool-to.md", "text": "https://wpnews.pro/news/your-docker-builds-are-slow-because-you-re-doing-it-wrong-and-i-built-a-tool-to.txt", "jsonld": "https://wpnews.pro/news/your-docker-builds-are-slow-because-you-re-doing-it-wrong-and-i-built-a-tool-to.jsonld"}}