This Dockerfile builds and runs a Node.js application. It works. It also produces a 2.1GB image. Here are 10 things wrong with it.
The Dockerfile
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -y nodejs npm python3 build-essential curl wget git vim
COPY . /app
WORKDIR /app
RUN npm install
RUN npm run build
ENV NODE_ENV=production
ENV DATABASE_URL=[postgresql](/blog/what-is-postgresql/)://admin:secretpass@db.example.com:5432/prod
ENV API_KEY=sk-1234567890abcdef
EXPOSE 3000
USER root
CMD npm start
Find the problems, then check below.
The 10 problems
1. π¦ Using Ubuntu instead of Alpine
# β Ubuntu base: ~75MB
FROM ubuntu:latest
# β
Node Alpine: ~50MB, includes Node.js
FROM node:20-alpine
Ubuntu doesnβt even include Node.js. Youβre installing it manually on top of a larger base image.
2. π Using latest tag
# β "latest" changes without warning
FROM ubuntu:latest
# β
Pin to a specific version
FROM node:20-alpine
Your build might work today and break tomorrow when latest updates.
3. π§ Installing unnecessary packages
# β vim, wget, git, python3 in a production image?
RUN apt-get install -y nodejs npm python3 build-essential curl wget git vim
# β
Only what you need (and with Alpine, Node is already there)
# If you need build tools for native modules:
RUN apk add --no-cache python3 make g++
Every extra package increases image size and attack surface.
4. π Too many RUN layers
# β Each RUN creates a layer
RUN apt-get update
RUN apt-get install -y ...
# β
Combine and clean up in one layer
RUN apt-get update && \
apt-get install -y --no-install-recommends nodejs npm && \
rm -rf /var/lib/apt/lists/*
5. π COPY before dependency install (cache busting)
# β Any file change invalidates npm install cache
COPY . /app
RUN npm install
# β
Copy package files first, install, then copy source
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
This way, npm ci is cached unless package.json changes.
6. π¦ Using npm install instead of npm ci
# β npm install can modify package-lock.json
RUN npm install
# β
npm ci uses lockfile exactly, faster, deterministic
RUN npm ci --only=production
7. π Secrets in environment variables
# β Secrets baked into the image (visible with docker inspect)
ENV DATABASE_URL=postgresql://admin:secretpass@db.example.com:5432/prod
ENV API_KEY=sk-1234567890abcdef
# β
Pass at runtime
# docker run -e DATABASE_URL=... myapp
Anyone with access to the image can extract these secrets.
8. ποΈ No multi-stage build
# β Dev dependencies and build tools in production image
# β
Multi-stage: build in one stage, run in another
FROM node:20-alpine AS builder
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
9. π€ Running as root
# β Container runs as root
USER root
# β
Create and use a non-root user
RUN addgroup --system app && adduser --system --ingroup app app
USER app
If the container is compromised, the attacker has root access.
10. π Using shell form for CMD
# β Shell form: runs via /bin/sh, doesn't receive signals properly
CMD npm start
# β
Exec form: process receives SIGTERM for graceful shutdown
CMD ["node", "dist/index.js"]
Shell form means your app wonβt shut down gracefully in Kubernetes.
The fixed Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
RUN addgroup --system app && adduser --system --ingroup app app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER app
EXPOSE 3000
CMD ["node", "dist/index.js"]
Image size: ~180MB instead of 2.1GB. Build time: ~30 seconds instead of 10 minutes. No secrets baked in. Non-root user. Proper signal handling.
Related: Docker Cheat Sheet Β· Code Review Sql Query