Why Your Docker Container Is 1.2GB When It Should Be 80MB | by Saandeep Baansod | Apr, 2026 | MediumSitemapOpen in appSign up<br>Sign in
Medium Logo
Get app<br>Write
Search
Sign up<br>Sign in
Why Your Docker Container Is 1.2GB When It Should Be 80MB
Saandeep Baansod
7 min read·<br>Apr 15, 2026
Listen
Share
You run docker images and see it. Your Node.js API image sitting at 1.2GB. Your colleague’s Python service at 1.8GB. A simple Go binary wrapped in a container at 900MB.<br>Press enter or click to view image in full size
Why Your Docker Container Is 1.2GB When It Should Be 80MBYou ship it anyway because it works. The app runs fine. But that 1.2GB gets pulled on every deploy, pushed to every environment, stored in your registry, and downloaded by every developer on the team. At some point someone raises a cloud bill and everyone looks confused.<br>I’ve audited production Docker setups across several projects. The same five mistakes appear every single time. Here’s what they are, why they happen, and the exact changes that took a 1.2GB Node.js image to 78MB without changing a line of application code.<br>Why Image Size Actually Matters<br>Before the fixes, here’s why this is worth caring about.<br>Every extra megabyte in your image costs you in four places. Pull time on every deploy — a 1.2GB image on a cold node takes 2–3 minutes to pull before your container even starts. Registry storage — at scale, hundreds of image versions across environments adds up fast. Attack surface — every package installed in your image is a potential vulnerability. And local developer experience — docker pull shouldn’t be something you leave to go make coffee.<br>An 80MB image pulls in seconds. It has fewer packages, so fewer CVEs. It starts faster. It’s easier to reason about. The constraint of keeping images small is one of the best forces for keeping production environments clean.<br>Mistake 1: Using the Wrong Base Image<br>This is where 80% of bloat comes from. The default node:20 image is based on Debian and ships with a full Linux environment — compilers, development headers, build tools, man pages, locales. Everything you need to build software from source. Almost none of it is needed to run your application.<br># node:20 — 1.1GB base image<br>FROM node:20<br>WORKDIR /app<br>COPY package*.json ./<br>RUN npm ci<br>COPY . .<br>CMD ["node", "src/index.js"]<br># Final image: ~1.2GBThe fix is using the Alpine variant, which is a minimal Linux distribution at around 7MB.<br># node:20-alpine — 56MB base image<br>FROM node:20-alpine<br>WORKDIR /app<br>COPY package*.json ./<br>RUN npm ci --only=production<br>COPY . .<br>CMD ["node", "src/index.js"]<br># Final image: ~110MBOne line change. 1.1GB to 110MB before you’ve done anything else.<br>For most languages there’s an official alpine or slim variant. Use it as your default unless you have a specific reason not to.<br># Python<br>FROM python:3.12-alpine # 23MB vs python:3.12 at 900MB+<br># Go - even better: scratch<br>FROM golang:1.22-alpine AS builder<br>WORKDIR /app<br>COPY . .<br>RUN CGO_ENABLED=0 go build -o app .<br>FROM scratch # literally empty - 0MB base<br>COPY --from=builder /app/app .<br>CMD ["/app"]<br># Final Go image: ~10MB - just the binary<br># Java<br>FROM eclipse-temurin:21-jre-alpine # JRE only, not JDK<br># 175MB vs openjdk:21 at 850MB+Mistake 2: Installing Dev Dependencies in Production<br>Your node_modules folder has two categories of packages. The ones your app actually needs to run — Express, Mongoose, dotenv. And the ones you only need during development — Jest, ESLint, TypeScript, ts-node, Nodemon.<br>Most Dockerfiles install all of them.<br># Installs everything including devDependencies<br>RUN npm install<br># node_modules: ~350MB<br># Production dependencies only<br>RUN npm ci --only=production<br># node_modules: ~80MBFor TypeScript projects this gets more interesting. You build TypeScript to JavaScript, then you only need the compiled output and production dependencies to run. You do not need TypeScript, ts-node, or any build tooling in the final image.<br>This is where multi-stage builds earn their keep.<br># Multi-stage TypeScript build — dev tools stay in stage 1<br>FROM node:20-alpine AS builder<br>WORKDIR /app<br>COPY package*.json tsconfig.json ./<br>RUN npm ci # installs everything including devDeps<br>COPY src ./src<br>RUN npm run build # compiles TS → dist/<br>FROM node:20-alpine AS production<br>WORKDIR /app<br>COPY package*.json ./<br>RUN npm ci --only=production # production deps only<br>COPY --from=builder /app/dist ./dist<br>CMD ["node", "dist/index.js"]<br># TypeScript compiler, ts-node, jest, eslint - all gone<br># Final image: ~85MBThe builder stage has full dev tooling. The production stage only copies the compiled output and production dependencies from it. The builder stage is discarded entirely and never shipped.<br>Mistake 3: No .dockerignore File<br>Every file in your project directory gets sent to the Docker build context — the set of files available during the build — unless you tell Docker to exclude them. Without a .dockerignore, Docker sends everything.<br>That includes your node_modules (which you’re about to...