Skip to content

Multi-Stage Dockerfile Tutorial

Outline

  • πŸ› οΈ Introduction
  • πŸ” Creating a Multi-Stage Dockerfile
  • βš™οΈ Managing Development and Production Environments
  • πŸ“‚ Sample Project Structure
  • πŸ“‹ Best Practices for Multi-Stage Builds
  • πŸ“Š Advanced Dockerfile Features
  • πŸ› οΈ Practical Example
  • πŸš€ Conclusion

πŸ› οΈ How to Create a Multi-Stage Dockerfile?

A Multi-Stage Dockerfile allows you to optimize your Docker images by using multiple stages to manage dependencies and build artifacts efficiently. It helps to keep your production image clean by only including what's necessary for running the application.

Example Multi-Stage Dockerfile

# Stage 0: Base Image
FROM node:latest AS base
WORKDIR /app
COPY package.json .

# Stage 1: Development Stage
FROM base AS development
RUN npm install
COPY . .
EXPOSE 4002
CMD [ "npm", "run", "start-dev" ]

# Stage 2: Production Stage
FROM base AS production
ARG NODE_ENV=production
RUN if [ "$NODE_ENV" = "production" ]; \
    then npm install --only=production; \
    else npm install; \
    fi
COPY . .
EXPOSE 4002
CMD [ "npm", "run", "start" ]

Explanation of the Dockerfile

Stage Purpose Commands
Base Establishes the base environment. FROM node:latest AS base, WORKDIR /app, COPY package.json .
Development Set up for development. RUN npm install, COPY . ., EXPOSE 4002, CMD [ "npm", "run", "start-dev" ]
Production Set up for production. RUN if [ "$NODE_ENV" = "production" ]; ..., COPY . ., EXPOSE 4002, CMD [ "npm", "run", "start" ]

βš™οΈ How to Manage Development and Production Environments

To manage different configurations for development and production, you can use environment variables. This helps you control how your application behaves in each environment.

Using Environment Variables in Dockerfile

You can define an environment variable like NODE_ENV to distinguish between production and development:

ARG NODE_ENV=production
RUN if [ "$NODE_ENV" = "production" ]; \
    then npm install --only=production; \
    else npm install; \
    fi

Setting Environment Variables in docker-compose.yml

In your docker-compose.yml, you can specify the environment for each service:

version: "3"
services:
  app:
    build:
      context: .
      args:
        NODE_ENV: development # Change to production when needed
    ports:
      - "4002:4002"

You can also use a .env file to manage environment variables:

NODE_ENV=development

πŸ“‚ Sample Project Structure

Here’s an example of a project structure to organize your Docker-related files:

my-project/
β”œβ”€β”€ docker-compose.dev.yml
β”œβ”€β”€ docker-compose.prod.yml
β”œβ”€β”€ docker-compose.yml
β”œβ”€β”€ mongo-data/
β”œβ”€β”€ package-lock.json
β”œβ”€β”€ package.json
β”œβ”€β”€ src/
β”œβ”€β”€ Dockerfile
└── .env

File Descriptions

File/Directory Description
docker-compose.yml Main configuration file for Docker Compose.
docker-compose.dev.yml Development-specific configuration.
docker-compose.prod.yml Production-specific configuration.
Dockerfile Defines how to build the Docker images.
package.json Lists the project's dependencies.
src/ Contains the source code for the application.
mongo-data/ Persistent storage for MongoDB data.
.env Holds environment variables (if needed).

πŸ“‹ Best Practices for Multi-Stage Builds

  1. Minimize the Number of Layers: Combine commands in your Dockerfile to reduce the number of layers and optimize the image size.
RUN npm install && npm run build
  1. Use Specific Base Images: Instead of using node:latest, specify a version (e.g., node:14 or node:18). This ensures consistency in builds.

  2. Clean Up Unnecessary Files: After installing dependencies, remove unnecessary files to keep the image clean.

RUN npm install && rm -rf /var/cache/apk/* /tmp/*
  1. Leverage .dockerignore: Use a .dockerignore file to exclude files and directories from the build context, which helps reduce the image size.
node_modules
npm-debug.log
  1. Utilize Build Arguments: Use build arguments for configuration that may change between environments (e.g., API keys).
    ARG API_KEY
    

πŸ“Š Advanced Dockerfile Features

1. Caching Layers

Docker caches layers to speed up builds. If a layer hasn’t changed, Docker reuses the cached layer instead of rebuilding it. To leverage caching, order your COPY and RUN commands strategically.

2. Health Checks

Add health checks to your containers to ensure they are functioning properly. Use the HEALTHCHECK instruction to specify a command that Docker can use to check the health of your application.

HEALTHCHECK CMD curl --fail http://localhost:4002/ || exit 1

3. Using Non-Root Users

For security reasons, avoid running your application as the root user inside the container. Create a non-root user and switch to that user in your Dockerfile.

RUN useradd -m appuser
USER appuser

πŸ› οΈ Practical Example

Here’s a practical example of a simple FastAPI application using MongoDB and Redis. The following code snippets illustrate how to set up the application with a Multi-Stage Dockerfile.

Dockerfile for FastAPI App

# Base Image
FROM python:3.9 AS base
WORKDIR /app
COPY requirements.txt .

# Development Stage
FROM base AS development
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

# Production Stage
FROM base AS production
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Example docker-compose.yml

version: "3"
services:
  app:
    build:
      context: .
      args:
        NODE_ENV: development
    ports:
      - "8000:8000"
    depends_on:
      - mongo
      - redis

  mongo:
    image: mongo
    restart: always
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: example
    volumes:
      - ./mongo-data:/data/db

  redis:
    image: redis
    restart: always

πŸš€ Conclusion

Using a Multi-Stage Dockerfile allows you to efficiently manage different environments for your application. By leveraging Docker's build arguments and environment variables, you can streamline the development process while ensuring a clean production image.

This approach not only enhances performance but also simplifies the management of dependencies and configurations across environments. Implementing best practices and advanced features can further improve the security and reliability of your application. Happy coding! πŸŽ‰