๐ณ Mastering Dockerfile: Advanced Syntax, Base Images, Multi-Stage Builds & Production-Grade Optimizations
Most tutorials teach Docker with:
FROM node
COPY . .
RUN npm install
CMD ["npm", "start"]
But real-world Dockerfiles used in production are far more nuanced.
This article goes deep into:
Advanced Dockerfile syntax (not just basics)
Environment-specific configs (dev, staging, prod)
Base image choices (Alpine vs Bullseye vs Distroless)
Multi-stage builds
Performance, security, and image-size optimizations
How professional DevOps engineers design Dockerfiles
1๏ธโฃ Understanding Dockerfile as a Build Recipe
A Dockerfile is not just a script; itโs a declarative build recipe for constructing immutable, layered images.
Each line:
Creates a new layer
Is cached independently
Impacts final image size, security, and runtime behavior
Why Layers Matter (Real-World Example)
If you do:
COPY . .
RUN npm install
Every small code change invalidates the npm install layer โ slow builds.
Correct layering:
COPY package*.json ./
RUN npm install
COPY . .
Now dependencies are cached unless package.json changes.
2๏ธโฃ Advanced Dockerfile Instructions (Deep Dive)
๐น FROM (Base Image Strategy)
FROM node:20-bullseye
This defines:
OS (Debian Bullseye)
Runtime (Node.js 20)
libc implementation (glibc)
Popular Base Images
| Base Image | OS | libc | Size | Use Case |
node:bullseye | Debian | glibc | Large | Native deps, stability |
node:alpine | Alpine | musl | Small | Microservices, APIs |
distroless | Minimal | glibc | Tiny | Security-first prod |
scratch | None | None | Tiny | Static Go/Rust binaries |
๐ง Real-Life Choice
If your app uses native modules (
better-sqlite3,sharp) โ BullseyeIf you need smallest possible image โ Alpine
If security is critical โ Distroless
๐น WORKDIR (Context Isolation)
WORKDIR /app
Sets working directory for all future instructions
Prevents messy absolute paths
Creates directory if not exists
Best practice:
WORKDIR /usr/src/app
๐น ENV (Environment Configuration)
ENV NODE_ENV=production
ENV PORT=1337
This defines environment variables inside the image.
ENV Across Environments (Dev / Staging / Prod)
# default values
ENV NODE_ENV=production
ENV LOG_LEVEL=info
Override at runtime:
docker run -e NODE_ENV=staging -e LOG_LEVEL=debug my-app
Real-Life Example
ENV NODE_ENV=production
This:
Disables dev-only dependencies
Improves performance
Changes framework behavior (Node, Next.js, Strapi, etc.)
๐น ARG (Build-Time Variables)
ARG APP_VERSION
ENV APP_VERSION=${APP_VERSION}
Usage:
docker build --build-arg APP_VERSION=1.2.3 -t myapp .
Difference between ARG and ENV:
| Feature | ARG | ENV |
| Available at runtime | โ No | โ Yes |
| Used during build | โ Yes | โ No |
| Secrets | โ Unsafe | โ Unsafe |
๐น RUN (Build-Time Commands)
RUN apt-get update && apt-get install -y curl
Best practice:
RUN apt-get update \
&& apt-get install -y curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
Why?
Reduces layer size
Removes cache
๐น COPY vs ADD
COPY . .
Use COPY for normal file transfer.
Avoid ADD unless you need:
Auto-extract tar files
Remote URL downloads (not recommended)
Professional rule:
Prefer COPY. ADD is rarely needed.
๐น USER (Security Hardening)
RUN useradd -m appuser
USER appuser
Never run production apps as root.
๐น HEALTHCHECK (Production Monitoring)
HEALTHCHECK --interval=30s --timeout=5s \
CMD curl -f http://localhost:1337/health || exit 1
Used by:
Docker
Kubernetes
ECS
๐น ENTRYPOINT vs CMD
ENTRYPOINT ["node"]
CMD ["server.js"]
ENTRYPOINT = fixed executable
CMD = default arguments
Override CMD easily:
docker run myapp other.js
3๏ธโฃ Multi-Stage Builds (Professional Standard)
๐ฅ Why Multi-Stage Builds Exist
Build tools (compilers, npm, dev deps) are not needed at runtime.
Example (Node.js / Strapi / React)
FROM node:20-bullseye AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM node:20-bullseye
WORKDIR /app
COPY --from=builder /app .
ENV NODE_ENV=production
CMD ["npm", "run", "start"]
Benefits
Smaller images
Faster startup
No build tools in prod
Less attack surface
Real-Life Example
CI builds assets โ prod image only contains runtime files.
4๏ธโฃ Environment-Aware Dockerfiles (Dev, Staging, Prod)
Dev Dockerfile
CMD ["npm", "run", "develop"]
Prod Dockerfile
ENV NODE_ENV=production
CMD ["npm", "run", "start"]
Staging
ENV NODE_ENV=staging
CMD ["npm", "run", "start"]
5๏ธโฃ Advanced Production Optimizations
๐น Use .dockerignore
node_modules
.git
.cache
dist
.env
๐น Reduce Image Size
RUN npm install --omit=dev
๐น Non-root user
USER node
๐น Immutable Config
Pass secrets via env:
docker run -e JWT_SECRET=xxx myapp
6๏ธโฃ How a Professional DevOps Engineer Thinks
Before writing Dockerfile:
What OS does my app depend on?
Are there native binaries?
Do I need dev tools at runtime?
What env vars change per environment?
How will this run in Kubernetes/ECS?
7๏ธโฃ Production-Grade Dockerfile Example (Complete)
FROM node:20-bullseye AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM node:20-bullseye
WORKDIR /app
COPY --from=builder /app .
ENV NODE_ENV=production
EXPOSE 1337
USER node
HEALTHCHECK --interval=30s --timeout=5s CMD curl -f http://localhost:1337/_health || exit 1
CMD ["npm", "run", "start"]
๐ Final Thoughts
Dockerfiles are not just build scripts โ they encode:
Security decisions
Performance tradeoffs
OS compatibility
Environment behavior
A strong DevOps engineer treats Dockerfiles like infrastructure code, not just boilerplate.
Entry point vs CDM
๐น What do these two lines mean?
ENTRYPOINT ["node"]
CMD ["server.js"]
Together, they define how your container starts.
When Docker runs the container, it combines them like this:
node server.js
So effectively, your container runs:
๐ข Node.js with
server.jsas the default script
๐ง How Docker Combines ENTRYPOINT + CMD
Docker treats them like:
Final command = ENTRYPOINT + CMD
So:
ENTRYPOINT ["node"]
CMD ["server.js"]
Becomes:
node server.js
๐ Why Not Just Use CMD Alone?
You could do:
CMD ["node", "server.js"]
This also runs node server.js.
But the behavior when you override commands is different ๐
๐ Difference in Behavior (Real-Life Example)
Case A: Only CMD
CMD ["node", "server.js"]
Run normally:
docker run myapp
# runs: node server.js
Override command:
docker run myapp node other.js
# runs: node other.js
Here, CMD is fully replaced.
Case B: ENTRYPOINT + CMD (Your Example)
ENTRYPOINT ["node"]
CMD ["server.js"]
Run normally:
docker run myapp
# runs: node server.js
Override CMD only:
docker run myapp other.js
# runs: node other.js
ENTRYPOINT stays fixed (node), and you only change the script.
๐ This is powerful when you want to lock the executable but allow flexible arguments.
๐ฏ Real-Life Use Cases
โ 1๏ธโฃ CLI Tool Containers
Example: A Node-based CLI tool
ENTRYPOINT ["node", "cli.js"]
CMD ["--help"]
Run default:
docker run my-cli
# node cli.js --help
Custom args:
docker run my-cli build --prod
# node cli.js build --prod
โ 2๏ธโฃ Scripting Containers
ENTRYPOINT ["node"]
CMD ["migrate.js"]
Now the container is a Node runtime wrapper:
docker run my-migrator cleanup.js
# node cleanup.js
โ When NOT to Use ENTRYPOINT + CMD
For typical web servers (Express, Strapi, Next.js), this pattern is not common. Youโll usually see:
CMD ["npm", "run", "start"]
Because:
Youโre not building a CLI tool
You donโt need to override script names at runtime
โ๏ธ Exec Form vs Shell Form (Important Detail)
Your example uses exec form:
ENTRYPOINT ["node"]
CMD ["server.js"]
This is better than shell form:
ENTRYPOINT node
CMD server.js
Why exec form is better:
Proper signal handling (SIGTERM, SIGINT)
No extra shell process
Cleaner process tree
Works better with Docker stop, Kubernetes, ECS
๐ง Interview-Ready One-Liner
ENTRYPOINT defines the fixed executable for the container, while CMD provides default arguments that can be overridden at runtime. When combined, Docker runs ENTRYPOINT with CMD as parameters.
๐งช Quick Demo You Can Try Locally
Dockerfile:
FROM node:20-alpine
WORKDIR /app
COPY server.js .
ENTRYPOINT ["node"]
CMD ["server.js"]
Run:
docker run myapp
# node server.js
docker run myapp other.js
# node other.js
๐ Final Takeaway
Think of it like this:
ENTRYPOINT= what program is this container?CMD= what arguments should it run by default?