Use the same Dockerfile for both local development and production with multi-stage builds

This is a follow-up to my earlier post on using Docker for local development. One problem we kept running into at Anyfin was maintaining two separate Dockerfiles - one for local development and one for production. Whenever we updated a dependency, we had to update both files, risking inconsistencies between environments.

Multi-stage builds solve this cleanly.

The problem

The typical setup involved separate files:

api/
├── index.js
├── package.json
├── Dockerfile              ← production
└── Dockerfile.development  ← local dev

With a docker-compose for local development pointing at Dockerfile.development and CI pointing at Dockerfile. Two files to maintain, easy to drift out of sync.

The solution: multi-stage builds

Multi-stage Dockerfiles let you define multiple images from a single file using the target parameter. The idea: put shared dependencies in a base stage, and layer production-only steps into a prod stage.

Single Dockerfile:

FROM node:16-alpine as base

RUN apk add --update graphicsmagick

FROM base as prod

WORKDIR /home/node/app
COPY package.json yarn.lock ./
RUN yarn install --production
COPY . .

CMD ["node", "index.js"]

docker-compose.yml for local development:

api:
  build:
    context: "./api"
    target: "base"
  command: sh -c "yarn install && yarn start"
  volumes:
    - ./api:/home/node/app:cached

Production build:

docker build . -t api:latest

What changed

Key benefits

This pattern has worked well for us across multiple services at Anyfin and has saved us from several "works locally but breaks in prod" incidents that were caused by environment drift.

This post was ported over from Atul's old blog. Read the original here.