Using Webpack to slim your Docker image
Tags: docker, webpack, optimization
Published: 2023-07-19 10:29:30

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

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

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.

```tsx
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',
  },
}
```

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.

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

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](https://www.woovi.com) 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](https://woovi.com/jobs/)!