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" ]
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',
},
}
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" ]
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!
Top comments (0)