<?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: Shivam Pawar</title>
    <description>The latest articles on DEV Community by Shivam Pawar (@shivam-pawar).</description>
    <link>https://dev.to/shivam-pawar</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3810435%2Fa2eb4b45-c618-4f7c-a6fa-51d8d64f822a.jpg</url>
      <title>DEV Community: Shivam Pawar</title>
      <link>https://dev.to/shivam-pawar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shivam-pawar"/>
    <language>en</language>
    <item>
      <title>I Shaved 600 MB Off a Production Docker Image Here's What I Learned</title>
      <dc:creator>Shivam Pawar</dc:creator>
      <pubDate>Fri, 06 Mar 2026 19:02:54 +0000</pubDate>
      <link>https://dev.to/shivam-pawar/i-shaved-600-mb-off-a-production-docker-image-heres-what-i-learned-4fka</link>
      <guid>https://dev.to/shivam-pawar/i-shaved-600-mb-off-a-production-docker-image-heres-what-i-learned-4fka</guid>
      <description>&lt;p&gt;I've been contributing to &lt;a href="https://github.com/fossasia/eventyay" rel="noopener noreferrer"&gt;Eventyay&lt;/a&gt;  an open-source event management platform by FOSSASIA and the first thing I did wasn't write a feature or fix a bug. I looked at the Dockerfile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1.42 GB.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For a Django app. One point four two gigabytes. That's almost the size of a small Linux distro.&lt;/p&gt;

&lt;p&gt;So naturally, I had to fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was going wrong
&lt;/h2&gt;

&lt;p&gt;If you've worked with Docker and Django, you've probably made the same mistakes. I've definitely made them in my own projects before I knew better. The Eventyay Dockerfile had the classics:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything was in one stage.&lt;/strong&gt; The image that gets shipped to production had &lt;code&gt;build-essential&lt;/code&gt;, &lt;code&gt;gcc&lt;/code&gt;, &lt;code&gt;git&lt;/code&gt;, and a bunch of other tools that are only needed to &lt;em&gt;compile&lt;/em&gt; packages  not to &lt;em&gt;run&lt;/em&gt; them. Once your Python packages are installed, you don't need a C compiler sitting in your production container.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The base image was heavy.&lt;/strong&gt; The full &lt;code&gt;python:3.12&lt;/code&gt; image is around 900 MB by itself. That's before you add a single line of your own code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;apt cache was sticking around.&lt;/strong&gt; Every &lt;code&gt;apt-get install&lt;/code&gt; was leaving behind cached package lists. Small individually, but they add up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual fix
&lt;/h2&gt;

&lt;p&gt;The core idea is dead simple: &lt;strong&gt;build in one container, run in another.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Builder stage — do the heavy lifting
&lt;/h3&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="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.12-slim-trixie&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /home/app/web&lt;/span&gt;

&lt;span class="c"&gt;# These are ONLY needed to compile Python packages&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; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    git build-essential gettext make nodejs

&lt;span class="c"&gt;# Install Python deps — this is where uv shines&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/root/.cache/uv &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;bind&lt;/span&gt;,source&lt;span class="o"&gt;=&lt;/span&gt;uv.lock,target&lt;span class="o"&gt;=&lt;/span&gt;uv.lock &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;bind&lt;/span&gt;,source&lt;span class="o"&gt;=&lt;/span&gt;pyproject.toml,target&lt;span class="o"&gt;=&lt;/span&gt;pyproject.toml &lt;span class="se"&gt;\
&lt;/span&gt;    uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--locked&lt;/span&gt; &lt;span class="nt"&gt;--no-install-project&lt;/span&gt; &lt;span class="nt"&gt;--no-editable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quick note on &lt;a href="https://github.com/astral-sh/uv" rel="noopener noreferrer"&gt;uv&lt;/a&gt; if you haven't tried it — it's a Python package manager written in Rust, and it's &lt;em&gt;fast&lt;/em&gt;. Like, "blink and it's done" fast. The &lt;code&gt;--mount=type=cache&lt;/code&gt; line means it caches downloads between builds, so if you haven't changed your &lt;a&gt;pyproject.toml&lt;/a&gt;, the entire dependency step is basically instant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final stage — only ship what runs
&lt;/h3&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; python:3.12-slim-trixie&lt;/span&gt;

&lt;span class="c"&gt;# No build-essential. No git. No gcc. Just runtime stuff.&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; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    netcat-traditional nodejs gettext less

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . /home/app/web&lt;/span&gt;

&lt;span class="c"&gt;# Grab the venv from the builder — this is the magic line&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /home/app/web/.venv /home/app/web/.venv&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PATH="/home/app/web/.venv/bin:$PATH"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;COPY --from=builder&lt;/code&gt; line is doing all the work. It reaches into the builder container, grabs the &lt;code&gt;.venv&lt;/code&gt; folder (which has all your installed packages), and drops it into a clean, slim image. The compiler, git, build headers all left behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  What didn't make it to production
&lt;/h2&gt;

&lt;p&gt;This is the stuff that was in the old image and doesn't need to be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;build-essential&lt;/code&gt; (~200 MB) — gcc, g++, make. Only needed to compile C extensions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;git&lt;/code&gt; (~50 MB) — was used during build, not at runtime&lt;/li&gt;
&lt;li&gt;Full Python image overhead (~400 MB) — switched to slim&lt;/li&gt;
&lt;li&gt;pip/uv cache — cleaned up properly&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Image size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.42 GB&lt;/td&gt;
&lt;td&gt;820 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cold build&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~8 min&lt;/td&gt;
&lt;td&gt;~5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cached build&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~8 min&lt;/td&gt;
&lt;td&gt;~1 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cached build improvement is the one I love. If you're iterating on application code and your dependencies haven't changed, the expensive steps are completely skipped. Docker layer caching + uv's download cache = 1 minute builds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things I'd tell my past self
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;--no-install-recommends&lt;/code&gt; is not optional.&lt;/strong&gt;&lt;br&gt;
Without this flag, &lt;code&gt;apt-get&lt;/code&gt; installs "recommended" packages that you almost certainly don't need. I've seen this flag alone save 80+ MB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Slim images are fine. Really.&lt;/strong&gt;&lt;br&gt;
I used to worry that &lt;code&gt;slim&lt;/code&gt; images would be missing something I needed. In practice, I've never had a runtime issue with them. If you need a specific library, just &lt;code&gt;apt-get install&lt;/code&gt; it explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Think about layer order.&lt;/strong&gt;&lt;br&gt;
Dockerfile layers are cached top-to-bottom. Put stuff that changes rarely (system packages, Python deps) at the top. Put stuff that changes constantly (your code) at the bottom. I see a lot of Dockerfiles that &lt;code&gt;COPY . .&lt;/code&gt; early and then install packages — meaning the package install runs on every single code change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. uv is worth trying.&lt;/strong&gt;&lt;br&gt;
I was a pip loyalist for years. uv won me over in one afternoon. The lockfile (&lt;a&gt;uv.lock&lt;/a&gt;) gives you reproducible installs, and the speed is genuinely shocking the first time you see it. If you're curious, their &lt;a href="https://docs.astral.sh/uv/guides/integration/docker/" rel="noopener noreferrer"&gt;Docker integration guide&lt;/a&gt; is solid.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to do this to your own project
&lt;/h2&gt;

&lt;p&gt;Honestly, start with these three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Switch your base image from &lt;code&gt;python:3.x&lt;/code&gt; to &lt;code&gt;python:3.x-slim&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add a builder stage and &lt;code&gt;COPY --from=builder&lt;/code&gt; just your &lt;code&gt;.venv&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;--no-install-recommends&lt;/code&gt; to every &lt;code&gt;apt-get install&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That'll probably get you a 30-50% reduction right there.&lt;/p&gt;




&lt;p&gt;This was my &lt;a href="https://github.com/fossasia/eventyay/pull/2307" rel="noopener noreferrer"&gt;PR #2307&lt;/a&gt; on &lt;a href="https://github.com/fossasia/eventyay" rel="noopener noreferrer"&gt;FOSSASIA's Eventyay&lt;/a&gt;. If you're looking for open-source Django projects to contribute to, FOSSASIA is a good place to start — active maintainers, clear guidelines, and real production code.&lt;/p&gt;

&lt;p&gt;I'm currently working toward &lt;a href="https://summerofcode.withgoogle.com/" rel="noopener noreferrer"&gt;GSoC 2026&lt;/a&gt; with FOSSASIA and sharing the journey on &lt;a href="https://x.com/pawar_shiv59037" rel="noopener noreferrer"&gt;X (@pawar_shiv59037)&lt;/a&gt;. Feel free to follow along or say hi 👋&lt;/p&gt;

</description>
      <category>docker</category>
      <category>python</category>
      <category>django</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
