<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Vaishnavi Pawar]]></title><description><![CDATA[Technical articles on software engineering, cloud infrastructure, DevOps, and system reliability, covering practical approaches, engineering trade-offs, and real-world problem-solving.]]></description><link>https://pawarvaishnavi.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Thu, 12 Nov 2026 07:16:57 GMT</lastBuildDate><atom:link href="https://pawarvaishnavi.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[What Should Actually Ship in Your Production Docker Image?]]></title><description><![CDATA[Try this on the last image you shipped: Now look around. In many images you'll find a package manager, compilers, your full source code, test files, and…]]></description><link>https://pawarvaishnavi.hashnode.dev/what-should-actually-ship-in-your-production-docker-image</link><guid isPermaLink="true">https://pawarvaishnavi.hashnode.dev/what-should-actually-ship-in-your-production-docker-image</guid><category><![CDATA[Docker]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Dockerfile]]></category><category><![CDATA[Docker Security]]></category><dc:creator><![CDATA[vaishnavi]]></dc:creator><pubDate>Tue, 03 Nov 2026 17:50:26 GMT</pubDate><content:encoded><![CDATA[<p>Try this on the last image you shipped:</p>
<pre><code class="language-dockerfile">docker run -it --entrypoint sh &lt;your-image&gt;
</code></pre>
<p>Now look around. In many images you'll find a package manager, compilers, your full source code, test files, and sometimes a stray .env. None of it helps your application run, and all of it helps an attacker.</p>
<p>Your application may work perfectly on your machine and pass every test. But a production-ready Dockerfile is about more than getting the application to run. It is about deciding what gets built, what gets shipped, and how the application runs.</p>
<p>Here are nine practices that help you make those decisions well. They apply to any language or framework, so you can use them with whatever stack you build on.</p>
<h1>1. Use Multi-Stage Builds</h1>
<p>Building an application often requires tools that running it does not: compilers, build dependencies, and package managers. These belong in the build process, not necessarily in production.</p>
<p>Multi-stage builds separate these concerns. One stage builds the application, while another contains only what is needed to run it.</p>
<pre><code class="language-plaintext"># Build stage
FROM &lt;build-image&gt; AS build
WORKDIR /app

COPY &lt;dependency-manifest&gt; &lt;lockfile&gt; ./
RUN &lt;install-build-dependencies&gt;

COPY . .
RUN &lt;build-command&gt;

# Runtime stage
FROM &lt;runtime-image&gt;
WORKDIR /app

COPY --from=build &lt;build-output&gt; &lt;runtime-destination&gt;
CMD ["&lt;application&gt;", "&lt;arguments&gt;"]
</code></pre>
<p>Anything left in the build stage does not reach the final image unless you explicitly copy it. This keeps build tools and other unnecessary components out of the runtime image.</p>
<p>The image names, commands, and paths above are placeholders. Replace them with the values your application needs.</p>
<h1>2. Choose Minimal, Pinned Base Images</h1>
<p>Your base image provides the environment your application runs in. Choose a trusted image that includes the runtime and libraries your application needs, without unnecessary tools or packages.</p>
<ul>
<li><strong>Use trusted sources:</strong> Prefer Docker Official Images, verified publisher images, or Docker Hardened Images.</li>
<li><strong>Prefer smaller variants:</strong> A slim or alpine image has fewer components to patch than a full OS image.</li>
<li><strong>Consider distroless for the final stage:</strong> Distroless images contain only the runtime and its dependencies, without a shell or package manager. This reduces the attack surface, but makes debugging harder.</li>
<li><strong>Pin the version:</strong> Floating tags like latest can change between builds. Pin a specific version or digest for predictable builds.</li>
</ul>
<pre><code class="language-dockerfile">FROM &lt;image&gt;:&lt;version&gt;@sha256:&lt;digest&gt;
</code></pre>
<p>The digest is a placeholder. Use the real digest of your chosen image. When both are specified, Docker uses the digest to select the image; the tag is there for readability.</p>
<p>A pinned image won't receive security fixes automatically. Use tools like Dependabot or Renovate to open pull requests when a new version or digest is available, and rebuild regularly.</p>
<h1>3. Use Layer Caching Effectively</h1>
<p>Docker stores the result of build steps in a cache and can reuse them when their instructions and inputs remain unchanged. If a step changes, the steps after it may need to run again.</p>
<p>To avoid unnecessary rebuilds:</p>
<ul>
<li><strong>Order instructions carefully:</strong> Place steps that change less often before steps that change frequently.</li>
<li><strong>Keep expensive steps early:</strong> Steps such as installing system packages or dependencies can take time. Arrange the Dockerfile so unrelated changes do not invalidate them unnecessarily.</li>
<li><strong>Keep the build context focused:</strong> Use .dockerignore to exclude files that the build does not need. Unnecessary files can cause cache invalidation when they change.</li>
</ul>
<p>For example, copy dependency manifests and install dependencies before copying frequently changing source code. This way, changing application code does not normally trigger dependency installation again.</p>
<p>Effective caching reduces repeated work and makes builds faster.</p>
<h1>4. Ship Only What the Application Needs at Runtime</h1>
<p>The final image should contain what the application needs to run, not everything used to build it.</p>
<ul>
<li><strong>Include only runtime files:</strong> A compiled application needs its binary or JAR. An interpreted application needs its source code and production dependencies.</li>
<li><strong>Leave build tools behind:</strong> Compilers, test frameworks, and development dependencies are needed to build the application, not to run it.</li>
<li><strong>Copy only what is required:</strong> In a multi-stage build, use COPY --from=build to bring specific files into the final stage, rather than the whole build directory. Everything else stays behind.</li>
<li><strong>Clean up installation files:</strong> Package managers leave downloaded package lists and caches behind. Remove them in the same RUN instruction that creates them.</li>
</ul>
<p>The goal is to keep the final image focused on what the application actually needs to run.</p>
<h1>5. Run as a Non-Root User</h1>
<p>Containers run as root unless you configure another user. If an attacker compromises your application while it runs as root, they may have more permissions inside the container than the application needs.</p>
<p>Create an unprivileged user and switch to it:</p>
<pre><code class="language-plaintext"># Debian-based image
RUN useradd --system --uid 10001 appuser

# Alpine-based image
RUN adduser -S -u 10001 appuser

USER 10001
</code></pre>
<p>The commands above are alternatives for different base images, not instructions to run together. Make sure the user exists and has permission to access the application files before switching to it. Some Distroless and hardened images already provide a non-root user, so check the image before creating another user.</p>
<p>Running as non-root does not replace other security controls, but it helps limit the impact of a compromised application.</p>
<h1>6. Handle Secrets Securely</h1>
<p>Passwords, API keys, and tokens should never be written directly into a Dockerfile or included in the image. A secret stored in an image layer can be extracted by anyone who can pull the image, even if a later instruction deletes it.</p>
<ul>
<li><strong>At build time:</strong> Avoid passing secrets through ARG or ENV. If a build step needs a secret, such as a private registry token, use a BuildKit secret mount. It makes the secret available to that step without storing it in the image.</li>
<li><strong>At runtime:</strong> Store production secrets, such as database credentials, in a secret manager and provide them to the container when it runs. For example, AWS ECS can retrieve secrets from AWS Secrets Manager.</li>
</ul>
<p>Keep secrets separate from the image, give each service access only to the secrets it needs, and never log them.</p>
<h1>7. Use a .dockerignore File</h1>
<p>When you run docker build, Docker sends the build context to the builder. Without a .dockerignore file, it may include Git history, local environment files, and dependency folders from your machine.</p>
<p>A broad COPY . . can then copy unwanted files into the image.</p>
<pre><code class="language-dockerfile">.git
.env
.env.*
node_modules
target
dist
coverage
*.log
</code></pre>
<p>This is an example, not a universal list. Adapt it to your project and make sure you do not exclude files the build actually needs.</p>
<p>A good .dockerignore keeps the build context focused, reduces unnecessary data transfer, and helps prevent local files from being included by mistake.</p>
<h1>8. Use Exec Form for CMD and ENTRYPOINT</h1>
<p><strong>RUN</strong> executes commands while building the image. <strong>CMD</strong> and <strong>ENTRYPOINT</strong> define what starts when the container runs.</p>
<ul>
<li>Use <strong>CMD</strong> when you want to set a default command that can be replaced when the container starts.</li>
<li>Use <strong>ENTRYPOINT</strong> when the image should always start with a particular application or command. You can use <strong>CMD</strong> to provide its default arguments.</li>
</ul>
<p>Both support shell form and exec form:</p>
<pre><code class="language-dockerfile"># Exec form
CMD ["&lt;application&gt;", "&lt;arguments&gt;"]
ENTRYPOINT ["&lt;application&gt;", "&lt;arguments&gt;"]

# Shell form
CMD &lt;application&gt; &lt;arguments&gt;
ENTRYPOINT &lt;application&gt; &lt;arguments&gt;
</code></pre>
<p>With <strong>exec form</strong>, the application runs directly as PID 1 inside the container. When you run docker stop, Docker sends SIGTERM to PID 1, giving the application a chance to finish active requests and close connections before it exits.</p>
<p>With <strong>shell form</strong>, the command runs through a shell, which becomes PID 1. The application runs as a child process, and the shell does not forward Docker's stop signal to it by default. As a result, the application may not receive SIGTERM, so the application may not shut down gracefully.</p>
<p>For production, use exec form for the startup command and ensure the application handles SIGTERM properly.</p>
<h1>9. Scan the Image for Vulnerabilities</h1>
<p>Even a well-written Dockerfile can include software with known security vulnerabilities. Scan your Docker image before deploying it to identify vulnerable operating-system packages and application dependencies.</p>
<p>Review scan findings based on their severity, whether a fix is available, and whether the affected component is relevant to your application. Updating the base image or application dependencies can often resolve vulnerabilities.</p>
<p>Scanning is not a one-time task. Include it in your CI pipeline and rebuild images regularly, as new vulnerabilities and security fixes are discovered.</p>
<h1>Conclusion</h1>
<p>Remember the image you explored at the start of this article? Run the same command on your next build and look around again. A well-written Dockerfile should leave much less unnecessary software and fewer security risks behind.</p>
<p>A production-ready Dockerfile is about choosing what your application needs to build, what it needs to run, and what permissions it should have. Multi-stage builds, minimal pinned images, secure secret handling, and regular scanning help make your containers safer and easier to maintain.</p>
<p>You don't need to apply everything at once. Pick one habit your current Dockerfile is missing and fix it today. Each one makes your image a little smaller, safer, and easier to maintain, and they add up quickly.</p>
<h1>References</h1>
<p><a href="https://docs.docker.com/build/building/multi-stage/">Docker: Multi-stage builds</a></p>
<p><a href="https://docs.docker.com/build/cache/">Docker: Build cache</a></p>
<p><a href="https://docs.docker.com/dhi/">Docker: Docker Hardened Images</a></p>
<p><a href="https://docs.docker.com/build/concepts/context/#dockerignore-files">Docker: .dockerignore files</a></p>
<p><a href="https://github.com/GoogleContainerTools/distroless">Distroless Images</a></p>
<p><a href="https://docs.docker.com/build/building/secrets/">Docker: Build Secrets</a></p>
]]></content:encoded></item></channel></rss>