Build1 publisher3 min readPublished
Dropping curl, git and vim explains 74 MB of a 385 MB Docker image cut
A dev.to writeup takes a 15-line Flask app from 442 MB to 56.6 MB and publishes its layer sizes. The measured layers cover 74.4 MB of the cut, and the base image swap has to account for the rest.
The Engineer · Build desk

What happened
- A 15-line Flask service with two routes built to 442 MB according to docker images, and about 1.75 GB of uncompressed disk usage.
- The baseline Dockerfile started from python:3.12, installed curl, git and vim across two RUN layers, copied the whole directory before pip install, and launched the app with python app.py.
- The reworked build reports 56.6 MB of image content and 256 MB on disk, which the author gives as reductions of 87.19 percent and 85.37 percent.
- The base image became python:3.12-slim, with Alpine rejected because musl builds often lack prebuilt wheels for C extension packages such as numpy and cryptography.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint The 87 percent is available because the application is fifteen lines; on a service whose site-packages are dominated by compiled wheels, the same base image swap moves a much smaller share of the total.
- decision Only two of these changes alter how the process runs, the non-root user and the WSGI server, so those are the ones a team has to test before copying the Dockerfile into a service that already works.
- cost The registry, bandwidth and pipeline savings are asserted without a figure, so anyone making the internal case has to measure their own pull and build times to know what the 385 MB buys.
- exposure Whatever gets a shell in the baseline container also gets a version control client and a text editor already installed; removing them narrows what a compromised process can do without fetching its own tools.
Start with the subtraction. Between the two builds the image loses 385.4 MB [1]. The only per-layer figures the dev.to post publishes are the two apt layers: about 21.3 MB for the `apt-get update` and about 53.1 MB for curl, git and vim [5], both of which the rework deleted outright [18]. Together that is 74.4 MB [2], or 19.3 percent of the saving [3]. The remaining 311 MB [4] has no layer-level attribution in the text, and the one other change of that scale is the base image, `python:3.12` to `python:3.12-slim` [6].
The hardening sits on a different axis. Moving off root to a non-root system user changes who the process is [8]. Swapping Flask's single-threaded dev server for Gunicorn changes how requests are served [9]. Neither removes a byte, and Gunicorn is a dependency the baseline never installed, so the 56.6 MB image ships a package the 442 MB one did not [3].
Whether the 87 percent transfers depends on how much of the image is your application. Here it is fifteen lines and two routes [1], so the app contributes nothing measurable and the base image is very nearly the whole image. The post's own argument against Alpine names the packages that break that assumption: numpy and cryptography, C extensions that frequently lack prebuilt musl wheels and compile during the build instead [7]. A service that installs those carries a large site-packages, and site-packages does not shrink when you swap Debian for Debian slim.
The available text of the post ends mid-Dockerfile and contains no multi-stage build [17]. That matters for the same dependency-heavy case: when a wheel has to be compiled, the toolchain that compiles it stays in the shipped image until a second stage leaves it behind.
The CI claim is the thinnest part of the writeup. Improved iterative build speed from layer caching is listed among the outcomes, with no before or after timing anywhere in the published text [10]. The mechanism is the ordering: `requirements.txt` is copied and `pip install` runs before the application code, so editing `app.py` reuses the dependency layer, where the baseline's `COPY . .` forced `pip install` to run again on any change [11].
The added `.dockerignore` covers `.git`, `.gitignore`, `__pycache__`, `.pytest_cache`, `.venv`, `tests`, `*.pyc`, `README.md` and `Dockerfile*` [12]. Without it, the build context hands the daemon git logs, a local `.venv` and `.pytest_cache` [13]. On debugging tools in general, the author's instruction is to use "ephemeral debugging sidecars or mount volumes" and "don't permanently ship development utilities to production" [14].
Both headline percentages check against the sizes given: 385.4 of 442 MB is 87.19 percent [5], and 1,494 of 1,750 MB is 85.37 percent [6].
What to watch
- A per-layer accounting for the unexplained 311 MB, or a multi-stage variant of the same Dockerfile.
- Any before-and-after build or pull timings that would put a number on the layer-caching claim.
- The same treatment applied to a service that installs numpy or cryptography, where site-packages dominates the image.