DEV Community

Sohana Akbar
Sohana Akbar

Posted on

My First Dockerfile: The Horrifying "Before" vs. The Efficient "After"

We all have that one commit we’re not proud of. You know, the one pushed late on a Friday night with the message: "pls work".

For me, that commit contained my very first Dockerfile. I was fresh out of a tutorial, convinced I had mastered containerization. Spoiler: I had not.

Looking back at that file is like looking at a mullet haircut from 1987. It was functional, but messy, insecure, and bloated.

Let me show you the evolution. The cringe vs. the clean. The Before vs. the After.

The "Before": The Wild West Dockerfile
I was building a simple Node.js API. My logic was: Install everything, copy everything, run everything as root, and pray.

dockerfile

Before: The "It works on my machine" special

FROM node:latest

Create app directory (Good start, but wait for it...)

WORKDIR /usr/src/app

Copy EVERYTHING. Node modules? Yeah, copy those too. .git? Sure.

COPY . .

Install dependencies (including dev dependencies, because why not?)

RUN npm install

Install global tools nobody asked for

RUN npm install -g nodemon pm2

Expose a port (Only one? Let's guess!)

EXPOSE 3000

Run as root (Security? Never heard of her)

CMD ["npm", "start"]
Why this made me a monster
node:latest is a ticking time bomb. Today it’s Node 21; next week it’s Node 22. Your build will randomly break because dependencies don’t match.

COPY . . is lazy and dangerous. I copied node_modules from my host machine (which was built for Windows/Mac) into the Linux container. This causes segfaults and mysterious "module not found" errors. I also copied secrets, SSH keys, and my .env file into the final image.

Giant image size. My image was ~1.2GB. Pulling that over coffee shop Wi-Fi? Forget it.

Running as root. If a hacker exploited my Node app, they had full root access to the container. Yikes.

The "After": The Professional's Approach
After three production outages and a mentor slapping me (metaphorically) with a security scan, I rewrote everything. Here is the Dockerfile I use today.

dockerfile

After: Lean, mean, and secure

1. Specific version + slim variant

FROM node:20-slim AS dependencies

WORKDIR /app

2. Copy ONLY package files first (Leverage Docker caching)

COPY package*.json ./
RUN npm ci --only=production

3. Multi-stage build: Separate build from runtime

FROM node:20-slim AS runtime

4. Create a non-root user

RUN groupadd -r nodejs && useradd -r -g nodejs nodejs

WORKDIR /app

5. Copy only what we need from the previous stage

COPY --from=dependencies --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --chown=nodejs:nodejs . .

6. Security: Drop capabilities & set user

USER nodejs

7. Health check so orchestrators know it's alive

HEALTHCHECK --interval=30s --timeout=3s CMD node health.js

EXPOSE 3000

8. Production grade runtime

CMD ["node", "server.js"]
Breaking Down the Differences
Let's put these two files side-by-side in a cage match.

Feature The "Before" (Horror) The "After" (Glory)
Base Image node:latest (floating, dangerous) node:20-slim (specific, smaller)
Caching Copies everything, invalidates cache always Copies package.json first, then code. Much faster rebuilds.
Permissions Runs as root Runs as nodejs user
Multi-stage No Yes (Separates build tools from runtime)
Dependencies npm install (includes dev dependencies) npm ci --only=production (exact versions, no dev junk)
What's inside Source code, .git, .env, local node_modules Only the bare necessities
The Actual Metrics (Real numbers from my app)
Metric Before After Improvement
Image Size 1.2 GB 210 MB 83% smaller
Build Time 45 seconds 6 seconds 87% faster
Vulnerabilities (npm audit) 23 high/critical 0 Infinite % better
Container Start Time 3.2 seconds 0.8 seconds 75% faster
The 5 Lessons I Learned (So You Don't Suffer)
If you take nothing else away from this post, remember these five golden rules:

Never use latest. Pin your versions. node:20-alpine or python:3.11-slim are your friends.

Leverage layer caching. Copy dependency files before your source code. Docker caches each step. If package.json hasn't changed, Docker reuses the cached npm install. This saves minutes.

Don't run as root. Add a user. USER appuser. It takes two lines and stops 90% of container exploits.

Multi-stage builds are magic. One Dockerfile can have multiple FROM statements. Use the first stage to compile/build, the final stage to only run the binary.

.dockerignore is non-negotiable. The COPY . . is evil. Create a .dockerignore file (same syntax as .gitignore) to exclude node_modules, .git, .env, and Dockerfile itself.

The Final Verdict
My first Dockerfile was a monolith of mistakes. It was a beginner's rite of passage—like burning toast or accidentally deleting a database in dev.

But the "after" version? That image is secure, tiny, and fast. It belongs in production.

Where are you on your Docker journey? Have you committed a "before" Dockerfile recently? Drop it in the comments so we can all cringe together (and then help you fix it).

Happy containerizing, and may your builds always be cache hits. 🐳

Top comments (0)