In many projects that are containerized, especially in cases where development is also done locally with docker-compose, teams often have two Dockerfiles, 1 for Development, the other for Production. If you happen to have multiple environments like pre-prod
, staging
and so on, some teams could have different Dockerfiles for these environments.
docker build has a very nice --target
option, that could be used to target a specific stage from the Dockerfile.
Let's take a very simple example: let's say we are trying to build a simple frontend app with npm, on Development, we start the app with npm run dev
but on production, we need to run npm run generate
and then copy the artefacts to Nginx's html folder to run, we would need to have a Dockerfile as below for Development:
FROM node:18-alpine
WORKDIR /usr/src
COPY ./package.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
EXPOSE 3000
This is simply a Dockerfile that starts a frontend app with npm run dev
exposing port 3000.
However, we typically do not run npm run dev
on production, extra steps are required to generate the static files from the application and then these files are served by nginx
or any other webserver.
To write the production Dockerfile for this app, we need to build the app and also serve with the desired web server.
Docker Multi-Stage builds
Docker multi-stage builds allows us to use multiple FROM
commands in a single Dockerfile, it allows us to selectively copy files from different stages into another.
For our case, we need a base, dev, build and prod stages, we are going to build our app for production with npm run generate
and we need to serve the built files with nginx. Our production Dockerfile will therefore look like this:
FROM node:18-alpine AS build
WORKDIR /usr/src
COPY ./package.json ./
RUN npm install
COPY . .
RUN npm run generate
FROM nginx:1.25.1-alpine
COPY --from=build /usr/src/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Notice the AS build
on the first stage, all commands before any other FROM
is considered a stage on their own.
After running npm run generate
in the app, it generates files into /usr/src/dist
.
In the second stage, where we are basing that on nginx
, we simply copy those files from the build stage using
COPY --from=build /usr/src/dist /usr/share/nginx/html
Updating this Dockerfile to serve both Dev and Production
Now that we have seen multi-stage docker builds in action, we can leverage this by having a base
stage, build
stage and finally a prod
stage.
To achieve this we update the docker file to this:
FROM node:18-alpine as base
WORKDIR /usr/src
COPY ./package.json ./
RUN npm install
COPY . .
FROM base AS dev
EXPOSE 3000
CMD ["npm", "run", "dev"]
FROM base AS build
RUN npm run build
FROM nginx:1.25.1-alpine as prod
COPY --from=build /usr/src/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
There are 4 stages in this docker file.
The base
stage
In this stage, we build the base image, copying the files from the project and simply doing npm install
The dev
stage
This is the stage that we want to target if we are building this image for dev
. More on how to do this in a bit.
The build
stage
This stage exists so that the prod
stage would be able to copy the generated static files for serving.
The prod
stage
This is the stage that we would target when building this image for production.
How to target a stage
in Dockerfile
Finally, now that we have a Dockerfile that can serve dev
and production
, to build an image that is specific for dev, we use the --target argument as so:
docker build -t mutistage:latest --target=dev
This would make Docker only build up till the dev
stage and nothing else.
Similarly to build for production we simply do
docker build -t mutistage:latest --target=prod
With this, we can maintain the same file for these two environments.
Top comments (2)
Thanks for sharing this! I knew about docker stages but not how to use them with the target switch. Nice. 😎
This was really helpful! Does this method mean a Docker container without a specified target will be running both? Do prod stage builds have an unused dev server running too, since it builds all the way through?