Optimizing Docker Images Using Multistage Builds
Disclaimer: This is not your regular blog for learning purpose, I just wanted to write it so that I can remember things that I did by teaching some imaginary viewer.
I wanted to deploy an application on docker because it is bugging me why I am not using docker as I have heard a lot about how it eases the deployment process of modern software, so I decided to do the labour work and tried to figure out how to do it (The docker community on dicord is amazing). I am fairly new to the container world and I just wanted to put this blog up on my personal portfolio so that i can look cool and have all the SEO in the Google Search Engine....
Oh sorry, we are here to deploy the application more specifically an application that is running inside a container.
Write a Dockerfile in the root of your project, I am using a simple vite-react application for deployment because I am a simple person. You can clone the project if you love your time.
Now run these:
cd vite-docker-gcp
npm install
npm run dev
Your application is running on http://localhost:5173, go check in your browser. In the root of your project create a Dockerfile. A dockerfile is necessary to build an image of the application. An image is like a blueprint for the container. We are going to use this image to run our container and the amazing thing is you can run multiple containers with the same image, awesome.
FROM node:20-alpine as base
WORKDIR ./src
COPY package*.json .
RUN npm install
COPY . .
RUN npm run build
npm i -g serve
EXPOSE 3000
CMD ["serve", "-s", "dist"]
What is happening ?
- We are pulling a node:20-alpine image from docker hub
- Telling docker, set working directory inside the container to src/
- Copying package.json and package-lock.json in docker image's filesystem
- Running npm install to install dependecies at build time
- Copying the files in docker image's filesystem
- Building our application (this will give us a dist folder)
- Installing serve package to serve dist's content on the port 3000
- Exposing the port 3000
- CMD command always run inside a container, so we are going to serve our dist using this command
This is enough to build the image, now if we run:
docker build . -t vite-docker:latest
It will build an image and you can run containers
But it is not a good approach to run a container with the whole code since the only thing we need is the build directory after npm run build
command. So to optimize we are going to use multistage Dockerfile. In a multistage Dockerfile, different stages are created to serve different purposes, for example to separate development and production environment.
FROM node:20-alpine as base
WORKDIR ./src
COPY package*.json .
RUN npm install
COPY . .
FROM base as build
RUN npm run build
FROM node:20-alpine as main
WORKDIR ./app
COPY --from=build ./src/dist ./dist
RUN npm i -g serve
EXPOSE 3000
CMD ["serve", "-s", "dist"]
In this file we have three stages: base - this stage will setup the whole thing that we did in above Dockerfile upto step 5 build - this stage will build the whole code main - this stage will serve the dist (which has post-build code)
Now when we run the image build process we specify a target like this:
docker build . -t vite-docker:latest --target=main
What this command will do is, it will start building the image from main stage and as it will reach the COPY command, it goes to build stage and since the build stage is itself running from base stage so it will run the base stage first. After getting the dist folder from build, it comes back to the COPY command and will just copy the content of dist folder which is present in src directory at build stage to the app/dist in main stage. The next steps are same as steps 7, 8, 9 from the first Dockerfile that we created.