DEV Community

Segun Olaiya
Segun Olaiya

Posted on

Use same Dockerfile for Dev & Production

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
Enter fullscreen mode Exit fullscreen mode

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;"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

With this, we can maintain the same file for these two environments.

Top comments (3)

Collapse
 
managedkaos profile image
Michael

Thanks for sharing this! I knew about docker stages but not how to use them with the target switch. Nice. 😎

Collapse
 
boldbigflank profile image
♫Alex Swan

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?

Collapse
 
kurealnum profile image
Oscar

I'm about a year late for this, but thank you so much! Saved me a few hours.