DEV Community

박준희
박준희

Posted on • Originally published at aicoreutility.com

Shrinking a Node.js Docker Image from 2.5GB to 300MB: Leveraging standalone server.js

Shrinking Node.js Docker Images from 2.5GB to 300MB: Leveraging a Standalone server.js

Ever run into a situation where your Node.js application's Docker image size balloons unexpectedly, slowing down your deployment process? This often happens, especially with complex build environments. In this post, I'll share how I managed to drastically reduce image size and speed up deployments.

Trials and Pitfalls

Initially, I focused on optimizing the build environment itself. I figured increasing the number of cores on the build machine in a CI/CD environment like Cloud Build would speed things up.

# Example Cloud Build configuration (actual setup might differ)
steps:
- name: 'gcr.io/cloud-builders/docker'
  args: ['build', '-t', 'gcr.io/my-project/my-app:${SHORT_SHA}', '.']
timeout: '1200s' # 20-minute timeout
machineType: 'n1-standard-8' # 8-core configuration
Enter fullscreen mode Exit fullscreen mode

However, no matter how much I scaled up the build environment, the image size itself didn't shrink. While build speed saw a slight improvement, it didn't address the root problem. I noticed the size kept growing as unnecessary dependencies and development tools were included in the image.

The Cause

The core issue was trying to handle everything needed for building and running the application within the Dockerfile all at once. Specifically, the npm install process installed development dependencies too, and complex build scripts lingering in the image contributed to its size. Combined with the Node.js runtime itself and necessary libraries, the final image size ballooned to nearly 2.5GB.

The Solution

The solution was to create a standalone server.js file that included only the bare minimum required to run the application. To achieve this, I used a tool like pkg to package the Node.js application into a single executable file.

First, I made sure package.json only listed essential dependencies, and then I ran npm install --production to install only the packages needed for operation.

{
  "name": "my-app",
  "version": "1.0.0",
  "main": "server.js",
  "dependencies": {
    "express": "^4.18.2",
    "body-parser": "^1.20.2"
    // ... list only production dependencies here
  },
  "devDependencies": {
    // ... exclude dependencies only needed for development/build
  }
}
Enter fullscreen mode Exit fullscreen mode

Next, I used pkg to create a single binary from the application, including server.js.

npm install -g pkg
pkg server.js --targets node18-linux-x64 --out-path dist
Enter fullscreen mode Exit fullscreen mode

With this single executable file (dist/my-app-linux-x64) generated, I built the Docker image. By using a lightweight OS like Alpine Linux and copying only this single executable, I minimized the image size.

FROM alpine:3.18

WORKDIR /app

COPY dist/my-app-linux-x64 /app/my-app

EXPOSE 3000

CMD ["/app/my-app"]
Enter fullscreen mode Exit fullscreen mode

Using this approach, unnecessary files and development tools are excluded, and I observed a significant reduction in image size, from 2.5GB down to approximately 300MB.

The Results

  • Docker image size reduced by over 8x, from 2.5GB to about 300MB.
  • Deployment time drastically decreased from about 20 minutes to approximately 7 minutes.
  • Faster image downloads and container startup times improved the overall deployment pipeline efficiency.

Key Takeaways — How to Avoid the Same Pitfalls

  • [ ] Ensure you're using the --production flag during npm install in your Dockerfile to only install production dependencies.
  • [ ] Consider using tools like pkg to package your application into a single executable file.
  • [ ] Build your Docker images based on lightweight OS images like Alpine Linux.
  • [ ] Optimize your Dockerfile to prevent unnecessary files or development tools generated during the build process from being included in the final image.

Top comments (0)