How can I reduce my Docker Image size for deployment?

I took one of my current projects and basically ran your Dockerfile...and got a 1.1 GB docker save tar file. It compresses well, but a 345 MB gzipped tar file still isn't what you're after. "Use an Alpine base image" is somewhat helpful, but not a silver bullet; switching it to be FROM node:8-alpine reduces it to a 514 MB uncompressed tar file.

If you don't already have a .dockerignore file, then your entire existing node_modules directory will get copied into the image. (In particular, the COPY . . step will copy your entire working tree in, overwriting the installed modules from the previous step.) It can just contain the single line

node_modules

The Alpine base image, plus not emitting a duplicate node_modules tree, gets me down to 382 MB uncompressed.

Notice in your example that your npm install step includes all of the development dependencies as well as the runtime dependencies. That can be a significant cost. If your application doesn't need any precompilation (it is plain JavaScript that Node can run directly) then you can add that --only=production flag and it will help.

If you do need some level of precompilation (Babel, Webpack, Typescript, ...) then you need a multi-stage build. My actual Dockerfile has three stages. The first does the compilation, and produces a dist directory with runnable JavaScript. The second produces the node_modules tree I need at runtime. The third (since we're counting bytes here) copies only the parts we really need from the first two stages.

The sum looks like:

FROM node:8-alpine AS build
WORKDIR /usr/src/app
# Installing dependencies first can save time on rebuilds
# We do need the full (dev) dependencies here
COPY package.json yarn.lock ./
RUN yarn install
# Then copy in the actual sources we need and build
COPY tsconfig.json ./
COPY src/ ./src/
RUN yarn build

FROM node:8-alpine AS deps
WORKDIR /usr/src/app
# This _only_ builds a runtime node_modules tree.
# We won't need the package.json to actually run the application.
# If you needed developer-oriented tools to do this install they'd
# be isolated to this stage.
COPY package.json yarn.lock ./
RUN yarn install --production

FROM node:8-alpine
WORKDIR /usr/src/app
COPY --from=deps /usr/src/app/node_modules ./node_modules/
COPY --from=build /usr/src/app/dist ./dist/
EXPOSE 3000
CMD ["node", "dist/index.js"]

docker save on this produces a 108 MB uncompressed tar file.

One thing you will notice, if you docker save node:8-alpine, is that that image is 71 MB on its own. When you docker save you are forced to copy that every time, and when you docker load it you will get a distinct copy of it every time. The only way around this is to have a registry server of some sort (Docker Hub, a cloud-hosted thing like Google GCR or AWS ECR, something you run yourself) and docker push and docker pull from there.

It turns out the node:8 image is huge; half its size is a full C toolchain (that single layer is 320 MB alone) (try running docker history node:8). The standard node image also has a node:8-slim variant which is still Debian-based (and so larger than the Alpine image) but much tidier. I get a 221 MB docker save tar file adapting my Dockerfile to be based on this image. (And again if you docker pull it you'll get the base Node runtime once, but if you docker load it you'll get that over and over again.)

Tags:

Docker

Node.Js