As a follow-up on last week’s article on tools for inspecting Docker images, and Docker image sizes in particular, today I’d like to introduce another – more custom – approach for reducing the size of Docker images for production deployments.
As a means of shaving off an additional 50 MB from the packaged application in question I came up with an idea somewhat reminiscent of the self-extracting archives of yore (of the WinRAR and 7-Zip flavours, for instance) for reducing the size of Docker images for containerized Node.js applications:
The basic idea is to tar-gzip a Node.js application’s node_modules
directory at build time and to unpack that directory again at runtime.
First, we have to provide a RUN
instruction like the following to our Dockerfile
, specifically its build stage, if we’re using multi-stage builds (which we should …):
1 2 3 4 5 | ... RUN tar -czf node_modules.tar.gz ./node_modules && rm -rf ./node_modules ... |
Then, we need to add an ENTRYPOINT
or CMD
instruction like this one, for example, to the stage in our Dockerfile
responsible for starting our application at runtime:
1 2 3 4 5 | ... CMD ["npm", "run", "start:production"] ... |
Finally, we insert that start:production
NPM script we referenced above in our application’s package.json
file:
1 2 3 4 5 6 7 8 9 | ... "scripts": { ... "start:production": "tar -xzvf node_modules.tar.gz && rm node_modules.tar.gz && node ./bin/www", ... }, ... |
This start:production
script contains the tar -xzvf node_modules.tar.gz && rm node_modules.tar.gz
responsible for unpacking our Node.js application’s node_modules
directory again, without which the application of course wouldn’t be able to run because it’d lack its dependencies. By chaining these commands – and the node
command following them – with the logical AND operator &&
provided by POSIX-compatible operating systems (e.g.: a Unix such as macOS, Linux, or BSD-based operating systems) we make sure that each subsequent command will only be executed if its predecessor has been executed successfully.
Consequently, the process takes slightly longer to start, but the overall Docker image size in this particular case was reduced by roughly 50 MB, which, of course, depending your use case might not matter at all or be an absolute boon. As often is the case in software development and, in fact, computer science at large, such considerations are a matter of trade-offs.
In this case, we’re trading in a faster application start-up for a smaller image size, which, depending on your target environment (e.g., embedded systems) might be a key concern.