DEV Community

Cover image for I Shrank My Docker Image From 1.11GB to 133MB, and That Was the Easy Part
Vivian Chiamaka Okose
Vivian Chiamaka Okose

Posted on • Originally published at vivianokose.hashnode.dev

I Shrank My Docker Image From 1.11GB to 133MB, and That Was the Easy Part

My first Docker image was 1.11GB. The app inside it was a tiny delivery tracker that could
have fit in a fraction of that. By the end of this module the image was 133MB, signed,
scanned, non-root, and sitting in two registries. Here is the whole journey, including the
parts that fought me.

I gave myself four questions, borrowed from how real teams judge a production image. A
naive image fails all four. A good one passes all four.

  1. How small is it?
  2. What is in it?
  3. Who built it?
  4. Will it run as root?

The app

SwiftMove Logistics: a Node.js server reading deliveries from MySQL. Small enough to
understand, real enough to need a database, a private network, volumes, and health checks.

The app running
Live data from the MySQL container beside it.

Question 1: How small is it?

I built the worst reasonable Dockerfile first, on purpose, to see the cost.

Naive image size
1.11GB. A whole build toolchain that the running app never touches.

The fix is a multi-stage build. Ship the sandwich, not the whole kitchen: do the messy
building in a throwaway stage, copy only the finished app into a tiny clean stage.

Size comparison
1.11GB to 133MB.

dive layers
99 percent efficiency confirmed with dive.

Question 4: Will it run as root?

No. A non-root user, created in the Dockerfile and switched to, so the app has almost no
power inside its own container.

Non-root
Running as UID 10001.

Hadolint clean
A clean Dockerfile lint.

One command for the whole stack

compose up
App plus database, one command.

Both healthy
MySQL has no port exposed to the host. Secure by not opening a door.

The first run showed "table doesn't exist", the app started before MySQL was ready. The
fix was a retry loop so the app waits patiently for the database. That bug taught me more
than a clean run would have.

Volume persistence
Data survived a full container destroy and recreate, because it lives in a volume.

Question 2: What is in it?

Trivy before
The first scan. Dozens of findings from the base image.

Most shared one root cause: outdated OpenSSL. I bumped the base image and rescanned.

Trivy after
OS findings dropped from 50 to 2. My own dependencies scan clean.

The goal is not zero. It is: know, fix what you can, document the rest in a SECURITY.md.

Question 3: Who built it?

Cosign verified
Signed and verified with cosign. A wax seal proving origin and integrity.

Two registries

Docker Hub
Public on Docker Hub.

The Nexus push threw a 403 first: my least-privilege deploy user could not create a new
repo, exactly as designed. Least privilege, felt from the other side.

In Nexus
The same image in my private Nexus registry.

The takeaway

The naive image took minutes. The production one took hours. That gap is the whole module,
and the bugs along the way were the best teachers.

Full code and screenshots: https://github.com/vivianokose/nexaops-operations-lab/tree/main/07-docker

Top comments (0)