====== Docker - Dockerfile - Dockerfile Build - Best Practices & Errors ======
===== Use specific base images =====
Start with a minimal, specific base image
FROM node:18-alpine
**NOTE:** Always use specific version tags rather than **latest** to ensure reproducible builds.
* Alpine-based images are significantly smaller than their Debian/Ubuntu counterparts.
* The more specific your tag, the better for consistency and security updates.
----
===== Order instructions by change frequency =====
Place instructions that change least at the top
FROM node:18-alpine
# Tools that rarely change
RUN apk add --no-cache python3 make g++
# Dependencies that change occasionally
COPY package*.json ./
RUN npm ci
# Application code that changes frequently
COPY . .
**NOTE:** The Docker build cache invalidates all subsequent layers when a layer changes.
* By placing more stable instructions at the top, you maximize cache usage and minimize rebuild time.
* This will significantly speed up your development workflow.
----
===== Combine related commands =====
Use **&&** to chain commands and reduce layers.
# Bad practice (creates 3 layers)
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# Good practice (creates 1 layer)
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
**NOTE:** Each RUN instruction creates a new layer.
* Combining related commands reduces image size and improves build performance.
* Always clean up package manager caches to keep images small.
----
===== Use .dockerignore file =====
Exclude unnecessary files from the build context.
cat .dockerignore
node_modules
npm-debug.log
Dockerfile
.git
.gitignore
README.md
**NOTE:** A **.dockerignore** file prevents the specified files from being sent to the Docker daemon during build.
* This speeds up builds and prevents sensitive files from being included in your image.
----
===== Implement multi-stage builds =====
Separate build and runtime environments.
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
CMD ["npm", "start"]
**NOTE:** Multi-stage builds lets you use one image for building (with all build tools) and another for running your application.
* This results in significantly smaller production images and improved security by not including build tools in the final image.
In the example above:
* The first stage named **builder** uses a full Node.js image which includes all build tools.
* We install dependencies and build the application in this first stage.
* The second stage starts fresh with a minimal Alpine-based image.
* Using **COPY --from=builder**, we selectively copy only the build artifacts and runtime dependencies.
* Everything else from the build stage is discarded, including node_modules with dev dependencies, source code, and build tools.
Multi-stage builds are particularly valuable for compiled languages like Go, Rust, or Java, where the final binary can be copied to a minimal image.
* For example, a Go application might use:
FROM golang:1.20 AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server
FROM alpine:3.18
RUN apk --no-cache add ca-certificates
COPY --from=builder /app/server /usr/local/bin/
CMD ["server"]
* This approach can reduce image sizes by up to 99% in some cases (from 1GB+ to ~10MB).
* You can even use more than two stages when you need separate phases for testing, security scanning, or generating different artifacts.
----
===== Set appropriate user permissions =====
Avoid running containers as root.
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
**NOTE:** Running containers as root is a security risk.
* Create a non-privileged user and switch to it before running your application.
* This limits the potential damage if the container is compromised.
----
===== Use ENTRYPOINT and CMD correctly =====
Understand their differences.
# For applications
ENTRYPOINT ["node", "app.js"]
CMD ["--production"]
# For utilities
ENTRYPOINT ["aws"]
CMD ["--help"]
**NOTE:** ENTRYPOINT defines the executable that runs when the container starts, while CMD provides default arguments to that executable.
* Using them together makes your containers more flexible and user-friendly.
----
===== Diagnose common errors =====
Understand build failures
docker build -t myapp .
**NOTE:** Common build errors include:
* Base image not found: Verify the base image exists and you have proper access
* COPY/ADD failures: Ensure source paths exist and are correctly specified
* RUN command failures: Run the commands locally to debug or use **docker build --progress=plain** for verbose output.
----
===== Optimize Docker Build Performance =====
When working with large applications, consider these additional optimizations.
* Use BuildKit by setting **DOCKER_BUILDKIT=1** before your build commands.
* Leverage build caching with **--cache-from** in CI/CD pipelines.
* For Node.js applications, use **npm ci** instead of npm install for faster, more reliable builds.
* Consider Docker layer caching services like BuildJet for CI/CD pipelines.
These techniques can reduce build times by up to 80% for complex applications.
----