Minimize Container Image Size for Frontend Apps with Multi-Stage Builds in Dockerfrontenddockernginx
Photo by Paul Teysen on Unsplash
While developing an authentication microservice that required a separate container for frontend, I noticed the significant file sizes of Ubuntu-based images that many developers tend to use for containerizing their applications. As a result, many of the images are more than 1GB in size. This observation sparked my curiosity and led me to investigate methods to decrease the image size specifically for a frontend application. I want to share my findings in this article.
With these optimizations, the size of the resulting image with a frontend app scaffolded using
vite became more than 30 times less!
Keep reading if you're interested in why and how.
Benefits of optimizing the size of container images
There are three main points on the surface:
- Reduced storage usage and costs: This is significant when you have many containers running concurrently or when deploying containers in resource-constrained environments.
- Faster deployment and scaling: This is important in scenarios where you must deploy multiple instances of the same container, such as in a microservices architecture or when scaling up your application.
- Improved bandwidth usage: This is useful in scenarios with limited bandwidth or when deploying containers across multiple regions or data centers.
It seems like it worth the effort, so let's create a frontend app and containerize it, trying to minimize the size of an image as much as possible.
Step one: Creating a frontend app
Scaffold a Vite project
To keep things simple, let's create a Vite project without any logic using the following
Since I prefer React and TypeScript, I specified the
react-ts template and named the project
frontend-vite. However, feel free to choose a different name or stack according to your preferences.
Configuring the app for Docker
To ensure your app will run in a Docker container, we need to modify the Vite configuration file (
vite.config.ts) to specify the desired port that will be used later inside a container. Let's use port
3333 for the test app:
Run the app
Let's run the app with the following command and look at it:
Open http://localhost:3333/ in you browser and check that our app is running and showing you a welcome page.
To stop Vite server, press
That's it! We are ready to create a Docker image with our frontend app.
Step two: Building Docker image
Writing a Dockerfile
A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image.
We will use a base image with the LTS version of Node.js for our frontend app to install dependencies and run the Vite server inside the container.
Ok, let's create it. In the root directory of our project, add a file with the name
Dockerfile and the following lines:
Make sure the exposed port is the same as specified in the frontend app config.
Building and running
Type the following command in your terminal to build an image and run it:
-t option in
docker build lets us specify a tag for our image. I used
frontend-vite as a tag for the image.
Options used with
docker run command are
--rm to clean up volumes,
-it for interactive mode with a virtual terminal to monitor app output, and
-p to assign a specific port for our app. Check docker run docs for details.
Check that our app is successfully running inside the container by opening http://localhost:3333/ in your browser.
If you are having trouble running your image and see a Segmentation fault error, look at the Troubleshooting section at the end of the article.
Checking the size of the image
Let's look at the image size using this command:
frontend-vite image size is
1.27GB. That's a lot, and we can reduce the size of the image.
Step three: Optimizing Docker image
A typical recommendation to reduce image size is to use images based on lightweight Linux distributives like Alpine. This optimization could reduce the size in half. But we can do even more!
Since we only want to run our app in a container, we don't need the source code and all the developer tools, so we can bring
NGINX as a web server to serve a production build of our frontend app.
To implement the strategy described above, we will use multi-stage builds in Docker to build our application in the first stage and serve its production build with
NGINX in the second stage.
This is the updated
I used Alpine Linux as a base image for the LTS version of Node.js (
node:lts-alpine) and NGINX (
Building and running
Let's build our optimized image with the following command:
And run the
frontend-vite-optimized image in Docker:
Reload the page in the browser and make sure the app is working, then look at your terminal for the
NGINX output with the browser requests.
Terminal logs will show you lines similar to this:
Checking the size of the optimized image
Let's use the following command again:
The size of our
frontend-vite-optimized image is
41.2MB. Wow, what an improvement!
After all the optimizations, the size of the resulting image with our frontend app scaffolded using
vite was significantly reduced from
🔥 It is more than 30 times less!
Segmentation fault running Docker container with Vite app
I run into a nasty bug on my Macbook with an Intel chip. When Docker tries to run
vite inside a container, it gives a
Segmentation fault error. It happens when Docker tries to execute
CMD ["npm", "run", "dev"] or
CMD ["yarn", "run", "dev"]
Congrats on reading to the end! Thank you for your time! Share your results with me on twitter, and if you have questions, feel free to ask.