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.
- How small is it?
- What is in it?
- Who built it?
- 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.

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.

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.

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.
One command for the whole stack

App plus database, one command.

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.

Data survived a full container destroy and recreate, because it lives in a volume.
Question 2: What is in it?

The first scan. Dozens of findings from the base image.
Most shared one root cause: outdated OpenSSL. I bumped the base image and rescanned.

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?

Signed and verified with cosign. A wax seal proving origin and integrity.
Two registries
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.

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)