Using Webpack to slim your Docker image

sibelius

Sibelius Seraphini

Posted on July 19, 2023

Using Webpack to slim your Docker image

There are many ways to deploy a Node.js application using a Docker image.

I will show many approaches until we finally reach the most optimized one

Docker image for a simple JavaScript Node.js application

FROM node:20

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

COPY package.json /usr/src/app/

RUN yarn install

COPY ./src /usr/src/app/

EXPOSE 8080
CMD [ "node", "src/index.js" ]
Enter fullscreen mode Exit fullscreen mode

This Dockerfile will copy package.json to the image, run yarn install, then copy source code, and start it using node src/index.js

There are two problems with this Dockerfile.
It only works for JavaScript projects, most projects are using TypeScript right now.
It can generate a big docker image if you have many dependencies in your node_modules.
It does not work with monorepos.

As your code and dependencies grow you can reach almost 4GB in a docker image

Using Webpack to slim your Docker image

To solve many of these issues, we are going to use a bundler, Webpack in our case, to join all code files in a single file.
The benefit of this is that it reduces the complexity of the deployment. It also remove unused code using tree shaking.
And it works well for TypeScript and monorepos.

module.exports = {
  context: process.cwd(),
  mode: 'production',
  devtool: 'source-map',
  entry: {
    server: [
      './src/server/index.ts',
    ],
  },
  output: {
    path: path.resolve('build'),
    libraryTarget: 'commonjs2',
    filename: 'server.js',
  },
  target: 'node',
  node: {
    __dirname: true,
    __filename: true,
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.json', '.mjs'],
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx|ts|tsx)?$/,
        use: {
          loader: 'babel-loader?cacheDirectory',
        },
        exclude: [/node_modules/],
      },
    ],
  },
  optimization: {
    minimize: false,
    minimizer: [
      new TerserPlugin({
        parallel: 4,
      }),
    ],
  },
  externals: {
    sharp: 'sharp',
    bull: 'bull',
    'bull-arena': 'bull-arena',
    // do not bundle to enable instrumentation
    'elastic-apm-node': 'commonjs elastic-apm-node',
    ioredis: 'commonjs ioredis',
    '@koa/router': '@koa/router',
    'mongodb': 'mongodb',
  },
}
Enter fullscreen mode Exit fullscreen mode

The above Webpack config will bundle our backend code, it will bundle most dependencies from node_modules, except for a few that can't be bundled, because they are native dependencies or have some code that does not work well with bundlers, like lua or ejs.

We also moved the image from node:20 to node:20-alpine to reduce the image size.

FROM node:20-alpine

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

COPY ./build/server.js /usr/src/app/

EXPOSE 8080
CMD [ "node", "server.js" ]
Enter fullscreen mode Exit fullscreen mode

Our current image size is just 200mb, and it takes only 15 seconds to build using cache.
I do believe we can decrease even more, but it is good enough for now

In Conclusion

As your codebase grows, you need to rethink your deployment strategy.
Even a simple Dockerfile does not scale wells.
Bundling our backend improved our DX for development.
It reduced the time required to build our docker images, and also the size.


Woovi
Woovi is a Startup that enables shoppers to pay as they like. To make this possible, Woovi provides instant payment solutions for merchants to accept orders.

If you want to work with us, we are hiring!

💖 💪 🙅 🚩
sibelius
Sibelius Seraphini

Posted on July 19, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related