Best Practices for Dockerizing Your Apps.

ksb
5 min readSep 3, 2024

--

This document outlines essential best practices for Dockerizing applications. By adhering to these guidelines, you can create efficient, secure, and maintainable Docker images. These best practices cover various aspects of Dockerization, including base image selection, layer optimization, dependency management, security, health checks, and image size reduction. Following these recommendations will help you build high-quality Docker images that streamline your development and deployment processes.

Best Practices for Dockerizing Applications

1. Use Official Base Images

  • Start with official Docker images from Docker Hub when possible.
  • These images are maintained, secure, and optimized for Docker.
  • Example: FROM node:14.17.0-alpine3.13 for a Node.js application.
# Good: Using an official Node.js image
FROM node:14.17.0-alpine3.13

# Avoid: Using a generic OS image and installing Node.js manually
# FROM ubuntu:20.04
# RUN apt-get update && apt-get install -y nodejs npm

2. Use Specific Image Tags

  • Avoid using the latest tag in production.
  • Use specific version tags to ensure consistency and avoid unexpected changes.
  • Example: FROM python:3.9.5-slim-buster instead of FROM python:latest.
# Good: Using a specific version tag
FROM python:3.9.5-slim-buster

# Avoid: Using the 'latest' tag
# FROM python:latest

3. Minimize the Number of Layers

  • Combine RUN commands using && to reduce the number of layers.
  • Each instruction in a Dockerfile creates a new layer.
  • Example: docker file
# Good: Combining commands to reduce layers
RUN apt-get update && \
apt-get install -y \
package1 \
package2 \
package3 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

# Avoid: Multiple RUN commands for related operations
# RUN apt-get update
# RUN apt-get install -y package1
# RUN apt-get install -y package2
# RUN apt-get install -y package3
# RUN apt-get clean
# RUN rm -rf /var/lib/apt/lists/*

4. Order Dockerfile Instructions Properly

  • Place instructions that change less frequently at the top of the Dockerfile.
  • This optimizes caching and speeds up builds.
  • Example order: FROM, WORKDIR, COPY (dependency files), RUN (install dependencies), COPY (application code).
FROM node:14.17.0-alpine3.13

WORKDIR /app

# Copy dependency files first
COPY package.json package-lock.json ./

# Install dependencies
RUN npm ci

# Copy the rest of the application code
COPY . .

# Build the application
RUN npm run build

# Set the command to run the application
CMD ["npm", "start"]

5. Use .dockerignore

  • Create a .dockerignore file to exclude unnecessary files from the build context.
  • This reduces build time and image size.
  • Example contents: .git, node_modules, *.log.
  • Create a .dockerignore file in the same directory as your Dockerfile:
.git
node_modules
npm-debug.log
Dockerfile
.dockerignore
.gitignore
README.md

6. Use Multi-stage Builds for Compiled Languages

  • Use multi-stage builds to create smaller production images.
  • Build your application in one stage and copy only the necessary artifacts to the final stage.
  • Example for a Go application:
# Build stage
FROM golang:1.16 AS builder
WORKDIR /app
COPY . .
RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

# Final stage
FROM alpine:3.14
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
CMD ["./main"]

7. Minimize the Number of Installed Packages

  • Install only the necessary packages to reduce image size and potential vulnerabilities.
  • Use minimal base images like Alpine when possible.
  • Remove package manager caches after installation.
FROM python:3.9.5-slim-buster

RUN apt-get update && \
apt-get install -y --no-install-recommends \
gcc \
python3-dev && \
pip install --no-cache-dir numpy pandas && \
apt-get purge -y --auto-remove gcc python3-dev && \
rm -rf /var/lib/apt/lists/*

COPY . /app
WORKDIR /app

CMD ["python", "app.py"]

8. Don’t Run Applications as Root

  • Create a non-root user and use the USER instruction to switch to it.
  • This improves security by following the principle of least privilege.
  • Example:
FROM node:14.17.0-alpine3.13

# Create app directory
WORKDIR /app

# Install app dependencies
COPY package*.json ./
RUN npm ci --only=production

# Bundle app source
COPY . .

# Create a non-root user
RUN addgroup -g 1001 -S appuser && \
adduser -u 1001 -S appuser -G appuser

# Change ownership of the app files to the non-root user
RUN chown -R appuser:appuser /app

# Switch to non-root user
USER appuser

EXPOSE 8080
CMD [ "node", "server.js" ]

9. Use Environment Variables for Configuration

  • Use ENV instructions to set environment variables.
  • This allows for easier configuration management and runtime customization.
  • Example: ENV NODE_ENV=production
FROM node:14.17.0-alpine3.13

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

# Set environment variables
ENV NODE_ENV=production
ENV PORT=8080

EXPOSE $PORT
CMD [ "node", "server.js" ]

10. Optimize Caching of Dependencies

  • Copy dependency files (e.g., package.json, requirements.txt) separately before copying the entire codebase.
  • Install dependencies in a separate layer.
  • This allows Docker to cache dependencies and speed up builds when only application code changes.
  • Example for a Node.js application:
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

11. Use HEALTHCHECK Instruction

  • Implement a health check in your Dockerfile to enable Docker to monitor the health of your container.
  • Example:
FROM nginx:1.21-alpine

COPY nginx.conf /etc/nginx/nginx.conf
COPY index.html /usr/share/nginx/html

HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost/ || exit 1

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

12. Keep Images Small

  • Use minimal base images (e.g., Alpine-based images).
  • Remove unnecessary files after each step.
  • Use multi-stage builds to separate build-time dependencies from runtime dependencies.
# Build stage
FROM node:14.17.0-alpine3.13 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM nginx:1.21-alpine
COPY --from=builder /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

13. Document Exposed Ports and Volumes

  • Use the EXPOSE instruction to document which ports your application uses.
  • Use the VOLUME instruction to define persistent data volumes.
  • Example:
FROM python:3.9.5-slim-buster

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# Document that the container listens on port 5000
EXPOSE 5000

# Define a volume for data persistence
VOLUME ["/app/data"]

CMD ["python", "app.py"]

14. Set Default Commands

  • Use the CMD instruction to set the default command for your container.
  • Use the ENTRYPOINT instruction if you want to use the container as an executable.
  • Example:
FROM python:3.9.5-slim-buster

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# Set the default command to run the application
CMD ["python", "app.py"]

# Alternatively, use ENTRYPOINT for a more rigid command
# ENTRYPOINT ["python", "app.py"]
# CMD ["--config", "default.conf"]

15. Version Control Your Dockerfile

  • Keep your Dockerfile in version control along with your application code.
  • This ensures reproducibility and makes it easier to track changes over time.

By following these best practices, you can create more efficient, secure, and maintainable Docker images for your applications. Remember to always test your Dockerized applications thoroughly before deploying to production.

--

--

ksb

Passionate about learning new technologies and implementing them. Enjoy contributing ideas to projects. Strong written and verbal communication skills;