Use same Dockerfile for Dev & Production
Tags: docker, production, deployment
Published: 2024-02-08 12:02:18

In many projects that are containerized, especially in cases where development is also done locally with [docker-compose](https://docs.docker.com/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](https://docs.docker.com/engine/reference/commandline/image_build/#target) has a very nice `--target` option, that could be used to target a specific [stage](https://docs.docker.com/build/building/multi-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:

```dockerfile
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](https://docs.docker.com/build/building/multi-stage/) 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:

```dockerfile
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

```dockerfile
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:

```dockerfile
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](https://docs.docker.com/engine/reference/commandline/image_build/#target) argument as so:

```bash
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 

```bash
docker build -t mutistage:latest --target=prod
```

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