Q&A
How to create an image using a Dockerfile
Overview
A Docker image is an immutable, layered file system built from a Dockerfile. Each instruction adds a layer; layers are cached, so re-building after small changes is fast. The final image contains everything the application needs to run - OS base, runtime, dependencies, and code.
graph TD
A[Dockerfile] --> B["FROM base-image"]
B --> C["WORKDIR /app"]
C --> D["COPY/ADD files"]
D --> E["RUN install deps / build"]
E --> F["CMD/ENTRYPOINT - default command"]
F --> G["docker build → Docker image"]
G --> H["docker run → running container"]
style G fill:#1e1b4b,color:#fff
Core Dockerfile Instructions
| Instruction | Purpose |
|---|---|
FROM |
Base image (required first instruction) |
WORKDIR |
Set the working directory inside the image |
COPY |
Copy files from build context into image |
ADD |
Like COPY but also handles URLs and auto-extracts tarballs |
RUN |
Execute commands during build (installs, builds) |
ENV |
Set environment variables |
EXPOSE |
Document which port the app uses |
CMD |
Default command when container starts (overridable) |
ENTRYPOINT |
Fixed executable; CMD provides default args |
ARG |
Build-time variables (not in the final image) |
A Practical Node.js Example
# 1. Base image - use a specific version tag, not 'latest'
FROM node:20-alpine
# 2. Working directory inside the container
WORKDIR /app
# 3. Copy dependency manifest first (cache optimization)
COPY package*.json ./
# 4. Install dependencies (cached layer unless package.json changed)
RUN npm ci --omit=dev
# 5. Copy application code
COPY . .
# 6. Document the port (informational; doesn't publish it)
EXPOSE 3000
# 7. Run the app
CMD ["node", "server.js"]
A Python Example
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV FLASK_ENV=production
EXPOSE 5000
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:5000"]
Building the Image
docker build -t myapp:1.0 . # build, tag as myapp:1.0, context = current dir
docker build -t myapp:1.0 -f path/to/Dockerfile . # custom Dockerfile path
docker images # list local images
Layer Caching - the Key Optimization
Docker caches each layer. When you rebuild, it reuses cached layers up to the first change. This is why you copy package.json / requirements.txt and install dependencies before copying application code - your npm install or pip install layer is only invalidated when dependencies change, not on every code edit.
Multi-Stage Builds (production best practice)
Multi-stage builds use one stage to compile/build and copy only the artifacts into a lean final image - keeping build tools out of production:
# Stage 1: Build
FROM node:20 AS builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build
# Stage 2: Production image (lean)
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]
The final image contains only the alpine Node runtime + built output - no dev tools, no source, no build cache.
.dockerignore
Like .gitignore but for the Docker build context - exclude node_modules, .git, test files, secrets:
node_modules
.git
*.test.js
.env
Without .dockerignore, the entire project (including node_modules) gets sent to the Docker daemon on every build.
Why This Matters in Interviews
DevOps/backend interviews use this to test containerization knowledge. Key points: each instruction creates a cached layer; copy manifests and install deps before copying code (cache efficiency); use specific image version tags not latest; CMD is overridable, ENTRYPOINT is fixed; multi-stage builds produce lean images; and .dockerignore prevents sending unnecessary context. Explaining layer caching and multi-stage builds separates basic from advanced knowledge.
Common Pitfalls
- Using
FROM node:latest- tag changes break reproducible builds; pin a version. - Copying all files before
RUN npm install- busts the install cache on every code change. - Forgetting
.dockerignore- sendsnode_modules(hundreds of MB) to the daemon each build. - Installing dev dependencies in a production image (use
--omit=dev/--only=production). - Running the container as root (use
USER nonrootfor security). - Large image sizes from not using
-slimor-alpinebase variants.
References
- Dockerfile reference: https://docs.docker.com/engine/reference/builder/
- Docker build best practices: https://docs.docker.com/develop/develop-images/dockerfile_best-practices/