# Problem:
FROM node:20
CMD ["node", "server.js"]
# Runs as root by default
example
# Fix:
FROM node:20-alpine
WORKDIR /app
COPY --chown=node:node . .
RUN npm ci --omit=dev
USER node
CMD ["node", "server.js"]
Note Most base images default to root. This means if an attacker exploits your application, they have root inside the container — and potentially on the host via privilege escalation. Always add a USER instruction before CMD.
running as rootsecurity mistakeroot containerprivilege escalation
Bloated Image Size
syntax
# Problem:
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3 build-essential ...
COPY . .
# 1.5GB image for a simple Python app
example
# Fix:
FROM python:3.12-slim AS build
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
FROM python:3.12-slim
COPY --from=build /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY . /app
CMD ["python", "/app/main.py"]
Note Common culprits: using the full Ubuntu/Debian base, leaving build tools installed, not cleaning apt/pip caches, copying node_modules into the image. Use docker history to identify the largest layers.
fat imageimage too largereduce sizebloated imageimage diet
Not Pinning Image Versions
syntax
# Problem:
FROM node:latest
FROM python
FROM nginx
example
# Fix — pin to specific versions:
FROM node:20.11.1-alpine3.19
FROM python:3.12.2-slim-bookworm
FROM nginx:1.25.4-alpine
Note An image tag like :latest or :20 can change at any time. Your build might work today and fail tomorrow because the base image was updated. In CI, a non-reproducible build means you cannot investigate a production issue with the exact same image.
Note Every time you rebuild with the same tag, the old image loses its tag and becomes <none>:<none>. These pile up fast. Volumes from removed containers also linger. Schedule regular prune commands or add them to your CI cleanup steps.
# Problem:docker run -d -p 3000:3000--name api-v1 myapp:1.0docker run -d -p 3000:3000--name api-v2 myapp:2.0# Error: port is already allocated# Fix — use different host ports:docker run -d -p 3001:3000--name api-v2 myapp:2.0
output
Error response from daemon: driver failed: Bind for 0.0.0.0:3000 failed: port is already allocated
Note Two containers cannot bind the same host port. Check what is using a port with lsof -i :3000 (macOS/Linux) or docker ps --format to see existing mappings. Remember that host services (like a locally running Node process) also count.
port conflictport already in useaddress in useport collision
Accidental Build Cache Invalidation
syntax
# Problem — COPY . . before RUN npm ci
COPY . .
RUN npm ci
example
# Every source code change re-runs npm ci (~30-60 seconds).# Fix — copy manifests first:
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
Note Docker invalidates a layer's cache when any input changes. A broad COPY . . changes whenever ANY file in your project changes, nuking the cache for all subsequent layers. Order Dockerfile instructions from least-frequently to most-frequently changing.
cache invalidationslow buildcache missbuild optimizationlayer order
# Use COPY for plain file copies (clear intent):
COPY package.json ./
COPY src/ ./src/
# Only use ADD when you specifically want extraction:
ADD assets.tar.gz /app/assets/
Note ADD auto-extracts tar archives and can fetch URLs, which is rarely what you want. COPY does exactly one thing — copies files. Using COPY makes your Dockerfile easier to reason about and avoids accidental extraction of files you wanted to keep as archives.
copy vs addadd instructionwhen to use addcopy preferred
Baking Secrets into Images
syntax
# NEVER do this:
ENV DATABASE_URL=postgres://user:pass@host/db
COPY .env /app/.env
example
# Bad — secret is in the image layer history:
ENV API_KEY=sk-abc123...
# Fix — pass secrets at runtime:docker run -e API_KEY=sk-abc123 myapp:1.0docker run --env-file .env myapp:1.0# For build-time secrets (e.g., private npm token):docker build --secret id=npmrc,src=.npmrc .# In Dockerfile:
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
Note ENV values and COPYed files are permanently visible via docker history and docker inspect. Even if you delete them in a later layer, they exist in the previous layer. Use --secret mounts for build-time secrets and runtime env vars or secret managers for runtime secrets.
secrets in imageleaked credentialsenv secretsbuild secretssecurity mistake