
My Huge First Docker Image: Lessons Learned
Okay, so picture this: it was my first week diving headfirst into Docker. I was so excited, feeling like a total tech wizard ready to containerize the world. My mission? To build a simple Node.js app,...
r5yn1r4143
6h ago
Okay, so picture this: it was my first week diving headfirst into Docker. I was so excited, feeling like a total tech wizard ready to containerize the world. My mission? To build a simple Node.js app, package it up, and have it running smoothly. I’d meticulously crafted my Dockerfile, adding each instruction with the confidence of a seasoned pro (spoiler: I was not). I hit docker build ., leaned back, and waited for that sweet, sweet success message.
Then it happened. Not the success message. Instead, a progress bar that seemed to crawl at the speed of dial-up internet. And the image size? Let’s just say it rivaled the storage capacity of my first flip phone. Gigabytes. For a hello world Node.js app. My jaw dropped. This wasn't the sleek, efficient containerization I’d imagined; this was a bloated, digital behemoth. My first oops moment with Docker was officially in full swing.
TL;DR: The Bloated Blob and the Build Blunder
My first Docker image was MASSIVE because I didn't understand how layers worked and I included way too much stuff. I ended up with a multi-gigabyte image for a tiny Node.js app. The solution involved multi-stage builds, cleaning up after installations, and being smarter about what I copied into the image. This saved a ton of disk space and made builds WAY faster.
The "Uh Oh" Moment: When Your Dockerfile Gets Out of Hand
I was building a basic Express.js app. My Dockerfile looked something like this (and please, don't laugh too hard at my initial attempt):
FROM ubuntu:latestInstall Node.js and npm
RUN apt-get update && \
apt-get install -y nodejs npmSet working directory
WORKDIR /appCopy package.json and package-lock.json
COPY package.json ./Install dependencies
RUN npm installCopy the rest of the application code
COPY . .Expose port and run the app
EXPOSE 3000
CMD [ "node", "server.js" ]
This seemed logical, right? Get the base OS, install Node, set up the app directory, copy the dependencies, install them, copy the code, run it. Simple.
The build started, and I watched those lines execute. Step 1/7 : FROM ubuntu:latest — that pulled a decent-sized image. Step 2/7 : RUN apt-get update && apt-get install -y nodejs npm — okay, installing stuff, makes sense. But then, the dependencies. RUN npm install downloaded everything. Including development dependencies, testing tools, and packages I didn't even know existed. Each RUN command, each COPY, each INSTALL was creating a new layer in the Docker image. And apparently, these layers were sticky. Everything I installed, even if I didn't need it for the final running container, was being baked in.
The build finished, and I nervously typed docker images. There it was: my-node-app <none> <REALLY_LONG_HASH> 3.5GB. Three. Point. Five. Gigabytes. My laptop’s SSD started weeping. I remember thinking, "This can't be right. It's just a few lines of JavaScript!"
The Error Messages I Didn't See (But Should Have)
The frustrating part? There weren't any explicit error messages telling me why it was so big. Docker doesn't typically yell at you for having a large image. It just is. The "error" was the practical outcome: a monstrous image that would take ages to push to a registry, ages to pull down on a new server, and would hog disk space like a tourist at an all-you-can-eat buffet. My build times were also suffering; each step had to re-run if cached layers were invalidated, and my initial, unoptimized steps were often invalidated.
Operation: Docker Diet - Trimming the Fat
I knew I had to fix this. My research (and a fair bit of panicked Googling) led me to a few key concepts:
Dockerfile, you can use multiple FROM instructions. Each FROM starts a new build stage. You can use one stage (the "builder" stage) to compile your code, install development dependencies, and do all your heavy lifting. Then, you can use a second, much smaller, clean FROM image (like node:alpine) and only copy the necessary built artifacts and production dependencies from the builder stage.RUN Commands & Cleaning Up: Combine related RUN commands using && to reduce the number of layers. Crucially, clean up package manager caches within the same RUN command that installed the packages.Implementing the Fix: A Leaner, Meaner Dockerfile
Here’s how I refactored my Dockerfile using multi-stage builds:
# --- Stage 1: Builder ---
FROM node:lts-alpine AS builderWORKDIR /app
Copy only package files first to leverage caching
COPY package.json ./Install ALL dependencies (including dev) for building/testing
RUN npm installCopy the rest of the application source code
COPY . .If you have a build step (e.g., TypeScript compilation)
RUN npm run build
--- Stage 2: Production ---
FROM node:lts-alpineWORKDIR /app
Copy only production dependencies from the builder stage
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./Copy the built application code (if you had a build step)
COPY --from=builder /app/dist ./dist
Copy the source code directly if no build step
COPY . .Expose port and run the app using the lean production image
EXPOSE 3000
CMD [ "node", "server.js" ]
Wait, what happened to apt-get? For many Node.js apps, especially those using lightweight base images like alpine, apt-get isn't needed at all. The node:lts-alpine
Comments
Sign in to join the discussion.