Introduction
Docker has revolutionized how we build, ship, and run applications. By packaging code and dependencies into containers, we eliminate the dreaded "it works on my machine" bug.
However, dropping a monolithic app into a default Dockerfile can result in bloated image sizes, slow CI/CD pipelines, and hidden security vulnerabilities. Letβs look at five best practices to write clean, production-ready Dockerfiles.
1. Embrace Multi-Stage Builds
When building compiled languages or modern frontend frameworks (like React, Next.js, or Go), you need heavy build tools (compilers, npm packages, node_modules) during compilation, but only the final static artifacts or binaries in production.
The Solution: Use multi-stage builds to separate your build environment from your runtime environment.
Dockerfile
Stage 1: Build the application
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
Stage 2: Run the application
FROM node:18-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]
This keeps your final production image lean by leaving behind compilers and source code.
2. Choose the Right Base Image
Avoid using ubuntu or debian as base images unless you specifically need full operating system utilities.
Alpine Linux: Extremely lightweight (often under 5MB) and secure, making it a favorite for production containers.
Distroless Images (by Google): Contain only your application and its runtime dependencies. They don't even include a shell or package manager, drastically reducing your attack surface.
3. Order Your Instructions for Optimal Caching
Docker builds images layer by layer, caching each step. If a layer changes, every subsequent layer must be rebuilt.
Best Practice: Copy your package manifests (package.json, requirements.txt) before copying your source code. That way, if you change a line of code, Docker won't re-download and reinstall all your dependencies.
Dockerfile
Do this:
COPY package*.json ./
RUN npm ci
COPY . . # Source code changes frequently, keep it at the bottom
4. Avoid Running as Root
By default, Docker containers run commands as the root user inside the container. If an attacker manages to exploit a vulnerability in your app, they instantly gain root privileges inside that container.
Best Practice: Create a non-privileged user and switch to it before starting your app:
Dockerfile
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
**5. Use .dockerignore
**Just like .gitignore, a .dockerignore file prevents unwanted files (like node_modules, .git, local environment variables, and log files) from being sent to the Docker daemon during the docker build command. This speeds up build times and prevents secret leaks.
Conclusion
Writing efficient Dockerfiles takes minor adjustments, but the payoff is massive: smaller images, faster deployments, and a significantly tighter security posture.
Top comments (0)