Imagine you’re building an application, but when it’s time to deploy, you are faced with a dilemma: the Docker image size is huge, the build process is slow, and you’re concerned about container security. What if I told you that there’s a way to solve all those problems in one go? – Time for Multistage Docker build, a powerful technique that not only reduces images sizes, but also speeds up deployment process. In this article you’ll discover why is a great strategy and how to implement it in your projects.
We will cover:
- What is a Multistage Docker Build?
- Advantages of Multistage builds
- How multistages builds works
- Best practices
What is a Multistage Docker Build?
A multistage docker build is a feature in Docker which allows you to have more than one ‘FROM’ statements within a single Dockerfile. Each stage can build a different image and perform a specific part of the build process.
Advantages of Multistage Builds
- Reduce image size: The image size is smaller than the one in a normal build, due to the image contains just what the app needs to run.
- Improve build time: Thanks to smaller images the deployment is faster and the performance is better.
- Increase containers security: The container will be more secure because the final image contains only what it’s needed to run the app.
How Multistage Builds Work
We are going to check the difference between single and multistage builds with a simple counter app for this example, you can find the code source here: https://github.com/Citrux-Systems/counterApp-multistage-demo
Make sure you have Docker o Docker destop:
1. Create a docker file: Create a docker file in your app’s root and add this code for a single-stage-build:
# Use node:18-alpine image as a parent image
FROM node:18-alpine
# Create app directory
WORKDIR /usr/src/app
# Copy package.json files to the working directory
COPY package*.json ./
# Install app dependencies
RUN npm install
# Copy the source files
COPY . .
# Build the React app for production
RUN npm run build
# Expose port 3000 for serving the app
EXPOSE 3000
# Command to run the app
CMD ["npm", "start"]
2. Build a docker image: Build the docker image with this command docker build -t single-stage-build .
3. Verify image size: Check the image size with this command docker images, in our example we have an image size of 646MB.
4. Run docker image: Run the image created using this command docker run -d -p 3000:3000 single-stage-build
. Now you can check in localhost:3000 to see your app:
5. Restructure docker file for multi-stages build: Now, restructure the docker file with different FROM statements like this:
# First stage - Building the application
# Use node:18-a;pine image as a parent image
FROM node:18-alpine AS build
# Create app directory
WORKDIR /usr/src/app
# Copy package.json files to the working directory
COPY package*.json ./
# Install app dependencies
RUN npm install
# Copy the source files
COPY . .
# Build the React app for production
RUN npm run build
# Second stage - Serve the application
FROM nginx:alpine
# Copy build files to Nginx
COPY --from=build /usr/src/app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
6. Build the docker image with multi-stages: Build the docker image with this command docker build -t multi-stage-build .
7. Verify image size: Check the image size again with docker images, and now you can see the image size is smaller than the one with single-stage.
8. Run docker image: Run the image created using this command docker run -d -p 80:80 multi-stage-build
, you should be able to access the application just as before.
As you could see, when using multi stages the size of docker images is reduced, because unnecessary files and dependencies are excluded from the final image, this also helps the build process to be faster and the app continues to work in the same way.
The files that are commonly excluded from the final image can be:
- Source code and development files like *.c, *.cpp, *.h, those are needed for compilation in the building process, but once the app is built those file are no longer needed, the final image will contain the assest required to run the app.
- Node.js and NPM modules are just needed to compile the app, but then the app will be built and served as static files so it won’t be necessary those dependencies or modules at runtime.
- Build tools and temporary files which are created in building process are only required for compilation, they aren’t needed once the app is built.
Basically, the final image will have just the files and dependencies to run the app at final stage. Everything required for compilation is only used in building stage and then excluded as they are not directly used to run the app.
Best Practices
There are some things that you can keep in mind:
- Create reusable stages including shared components when there is a lot in common between the multiple stages.
- Use appropiates and oficial base images from trusted sources to reduce image sizes and avoid security vulnerabilities.
- Separate build and runtime environments by different stages, the build stage should contain dependencies and configurations for app building and the runtime stage should have only the needed to run your app.
Conclusion
Implementing a multistage Docker build not only reduces image sizes and speeds up deployment but also enhances container security. By following best practices and structuring your Dockerfile effectively, this technique becomes an essential tool for any project aiming for efficiency and security in deployments. It's time to take your applications to the next level!