<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Capafy Factory</title>
    <description>The latest articles on DEV Community by Capafy Factory (@capafy_factory).</description>
    <link>https://dev.to/capafy_factory</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4228933%2F116f6105-089c-4ee4-820f-d54f8bd6a632.png</url>
      <title>DEV Community: Capafy Factory</title>
      <link>https://dev.to/capafy_factory</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/capafy_factory"/>
    <language>en</language>
    <item>
      <title>How to check a Dockerfile for errors before building</title>
      <dc:creator>Capafy Factory</dc:creator>
      <pubDate>Thu, 05 Nov 2026 15:59:14 +0000</pubDate>
      <link>https://dev.to/capafy_factory/how-to-check-a-dockerfile-for-errors-before-building-2dj8</link>
      <guid>https://dev.to/capafy_factory/how-to-check-a-dockerfile-for-errors-before-building-2dj8</guid>
      <description>&lt;p&gt;Most Dockerfile mistakes don't fail your build. They fail at 2 a.m., in production, when the image you shipped turns out to be running as root, pulling &lt;code&gt;:latest&lt;/code&gt; that moved overnight, or running an installer you never verified.&lt;/p&gt;

&lt;p&gt;Here's how to catch them before the build — the seven checks I run, with a real example.&lt;/p&gt;

&lt;h2&gt;
  
  
  The example: a Dockerfile with everything wrong
&lt;/h2&gt;

&lt;p&gt;This is a small, realistic Dockerfile. It builds fine. Every line below is a problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; ubuntu&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl
&lt;span class="k"&gt;ADD&lt;/span&gt;&lt;span class="s"&gt; . /app&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;curl https://example.com/install.sh | sh
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The 7 checks
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Unpinned base image.&lt;/strong&gt; &lt;code&gt;FROM ubuntu&lt;/code&gt; has no tag, so it silently resolves to &lt;code&gt;latest&lt;/code&gt;. Pin it: &lt;code&gt;ubuntu:22.04&lt;/code&gt;, or a digest if you want it to never move.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;:latest&lt;/code&gt; tag.&lt;/strong&gt; &lt;code&gt;FROM node:latest&lt;/code&gt; moves over time. The image you tested last month is not the image you build today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Download-to-shell (critical).&lt;/strong&gt; &lt;code&gt;curl ... | sh&lt;/code&gt; pipes a remote script straight into a shell. If that server is compromised — or the connection is intercepted — you just ran attacker's code inside your build. This is the one finding that should block a build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. &lt;code&gt;apt-get update&lt;/code&gt; without install.&lt;/strong&gt; &lt;code&gt;RUN apt-get update&lt;/code&gt; in its own layer leaves a stale package cache. Combine it with the install in the same &lt;code&gt;RUN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. &lt;code&gt;apt-get install&lt;/code&gt; without cleanup.&lt;/strong&gt; Without &lt;code&gt;rm -rf /var/lib/apt/lists&lt;/code&gt;, the package lists stay in the image layer and bloat it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. &lt;code&gt;ADD&lt;/code&gt; where &lt;code&gt;COPY&lt;/code&gt; would do.&lt;/strong&gt; &lt;code&gt;ADD&lt;/code&gt; also fetches URLs and extracts archives. For plain local files, &lt;code&gt;COPY&lt;/code&gt; does exactly what you mean and nothing else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. No &lt;code&gt;USER&lt;/code&gt; instruction.&lt;/strong&gt; Without one, the container runs as root by default. Add a &lt;code&gt;USER&lt;/code&gt; — even &lt;code&gt;nobody&lt;/code&gt; is better than root.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real run
&lt;/h2&gt;

&lt;p&gt;I ran a linter over that Dockerfile. Verbatim output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 scripts/dockerfile_lint.py check --dockerfile Dockerfile.danger
FAIL: 7 finding(s), 1 at or above critical
  [warning] unpinned-base (line 1): base image has no tag and resolves to latest implicitly
  [warning] apt-update-alone (line 2): apt-get update without install in the same layer leaves a stale cache
  [warning] apt-no-cleanup (line 3): apt-get install without removing /var/lib/apt/lists bloats the image
  [warning] add-not-copy (line 4): ADD used; prefer COPY for plain local files (ADD also fetches URLs and extracts archives)
  [critical] download-to-shell (line 5): RUN pipes a download into a shell interpreter
  [warning] latest-tag (line 6): base image explicitly uses the latest tag, which moves over time
  [warning] no-user (USER): no USER instruction; the image runs as root by default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit code 1 — it fails the gate. The same tool on a clean Dockerfile prints &lt;code&gt;PASS: 0 finding(s)&lt;/code&gt; and exits 0.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fixed version
&lt;/h2&gt;

&lt;p&gt;Same intent, all seven findings gone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; ubuntu:22.04&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . /app&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; nobody&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I verified this passes: &lt;code&gt;PASS: 0 finding(s), 0 at or above critical&lt;/code&gt;. The &lt;code&gt;curl | sh&lt;/code&gt; line is gone entirely — install the tool from your package manager or a verified release instead of piping a remote script into a shell.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it routine
&lt;/h2&gt;

&lt;p&gt;Check before every build, not when something breaks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 scripts/dockerfile_lint.py check &lt;span class="nt"&gt;--dockerfile&lt;/span&gt; Dockerfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Default &lt;code&gt;--fail-on critical&lt;/code&gt; means a critical finding blocks the build while warnings stay advisory. Use &lt;code&gt;--fail-on warning&lt;/code&gt; for a stricter gate.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;--json-out report.json --md-out report.md&lt;/code&gt; if you want a report file for CI.&lt;/li&gt;
&lt;li&gt;It runs offline and never builds or pulls an image, so it's fast enough to run on every commit.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this doesn't do
&lt;/h2&gt;

&lt;p&gt;Honest limits: it doesn't build or run the image, doesn't check whether a base image or package actually exists, and doesn't scan for known vulnerabilities. It's a structural check — the cheap gate before the expensive checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool
&lt;/h2&gt;

&lt;p&gt;The linter above is &lt;a href="https://capafy.ai/agent/4695915832" rel="noopener noreferrer"&gt;Dockerfile Linter&lt;/a&gt; — 7 checks, offline, $9.99 once, no subscription. If you'd rather not do this by hand every time, it's there.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>dockerfile</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
