9 Tips for Containerizing Your Node.js Application
December 8, 2025 · 1844 words · 9 min
Over the last five years, Node.js among professional developers. It’s an open source, cross-platfo
Over the last five years, Node.js among professional developers. It’s an open source, cross-platform JavaScript runtime environment designed to maximize throughput. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient — perfect for data intensive, real-time, and distributed applications. With over 90,500 stars and 24,400 forks, developer community is highly active. With more devs creating Node.js apps than ever before, finding efficient ways to build and deploy and cross platform is key. Let’s discuss how containerization can help before jumping into the meat of our guide. Containerizing your Node application has numerous benefits. First, Docker’s friendly, CLI-based workflow lets any developer build, share, and run containerized Node applications. Second, developers can install their app from a single package and get it up and running in minutes. Third, Node developers can code and test locally while ensuring consistency from development to production. We’ll show you how to quickly package your Node.js app into a container. We’ll also tackle key concerns that are easy to forget — like image vulnerabilities, image bloat, missing image tags, and poor build performance. Let’s explore a simple todo list app and discuss how our nine tips might apply. Let’s first consider a simple todo list application. This is a basic React application with a Node.js backend and a MongoDB database. The source code of the complete project is available . Luckily, we can build our sample application in just a few steps. First, you’ll want to clone the appropriate awesome-compose sample to use it with your project: Second, enter the command to list out your services in the terminal. This confirms that everything is accounted for and working properly: Third, open your browser and navigate to to view your application in action. You’ll see your todo list UI and be able to directly interact with your application: This is a great way to spin up a functional application in a short amount of time. However, remember that these samples are foundations you can build upon. They’re customizable to better suit your needs. And this can be important from a performance standpoint — since our above example isn’t fully optimized. Next, we’ll share some general optimization tips and more to help you build the best app possible. When building Docker images, we always recommended specifying useful tags which codify version information, intended destination (prod or test, for instance), stability, or other useful information for deploying your application across environments. Don’t rely on the tag that Docker automatically pulls, outside of local development. Using is unpredictable and may cause unexpected behavior. Each time you pull a image version, it could contain a new build or untested code that may break your application. Consider the following that uses the specific Docker image as a base image instead of . This approach may be preferable since is a stable image: Overall, it’s often best to avoid using in your . With multi-stage builds, a Docker build can use one base image for compilation, packaging, and unit testing. A separate image holds the application’s runtime. This makes the final image more secure and shrinks its footprint (since it doesn’t contain development or debugging tools). Docker builds help ensure your builds are 100% reproducible and lean. You can create multiple stages within a to control how you build that image. You can containerize your Node application using a multi-layer approach. Each layer may contain different app components like source code, resources, and even snapshot dependencies. What if we want to package our application into its own image like we mentioned earlier? Check out the following to see how it’s done: We first add an label to the statement. This lets us refer to this build stage in other build stages. Next, we add a new development stage labeled . We’ll use this stage to run our development. Now, let’s rebuild our image and run our development. We’ll use the same command as above — while adding the development flag to specifically run the development build stage: Today’s developers rely on third-party code and apps while building their services. External software can introduce unwanted vulnerabilities into your code if you’re not careful. Leveraging trusted images and continually monitoring your containers helps protect you. Whenever you build a Docker image, Docker Desktop prompts you to run security scans of the image to detect any known vulnerabilities. Let’s use the to inspect our Node.js application. To begin, install on your , , or machine. Next, check the box within Settings > Extensions to . You can then browse the Extensions Marketplace by clicking the “Add Extensions” button in the left sidebar, then searching for Snyk. Snyk’s extension lets you rapidly scan both local and remote Docker images to detect vulnerabilities. Install the Snyk and enter the Node Docker Official Image into the “Select image name” field. You’ll have to log into Docker Hub to start scanning. Don’t worry if you don’t have an account — it’s free and takes just a minute to create. When running a scan, you’ll see this result within Docker Desktop: Snyk uncovered 70 vulnerabilities of varying severity during this scan. Once you’re aware of these, you can begin remediation to fortify your image. That’s not all. In order to perform a vulnerability check, you can use the command directly against your : The instruction tells Docker how to test a container and confirm that it’s still working. For example, this can detect when a web server is stuck in an infinite loop and cannot handle new connections — even though the server process is still running. When an application reaches production, an orchestrator like Kubernetes or a service fabric will most likely manage it. By using , you’re sharing the status of your containers with the orchestrator to enable configuration-based management tasks. Here’s an example: When is present in a , you’ll see the container’s health in the column after running the command. A container that passes this check is healthy. The CLI will label unhealthy containers as unhealthy: You can also define a (note the case difference) within Docker Compose! This can be pretty useful when you’re not using a . Instead of writing a plain text instruction, you’ll write this configuration in YAML format. Here’s a sample configuration that lets you define within your file: To increase build performance, we recommend creating a file in the same directory as your . For this tutorial, your file should contain just one line: This line excludes the directory — which contains output from Maven — from the Docker build context. There are many good reasons to carefully structure a file, but this simple file is good enough for now. Let’s now explain the and why it’s essential . The command builds Docker images from a and a “context.” This context is the set of files located in your specified or . The build process can reference any of these files. Meanwhile, the compilation context is where the developer works. It could be a folder on Mac, Windows, or a Linux directory. This directory contains all necessary application components like source code, configuration files, libraries, and plugins. With a file, we can determine which of the following elements like source code, configuration files, libraries, plugins, etc. to exclude while building your new image. Here’s how your file might look if you choose to exclude the from your build: Running applications with user privileges is safer since it helps mitigate risks. The same applies to Docker containers. By default, Docker containers and their running apps have root privileges. It’s therefore best to run Docker containers as non-root users. You can do this by adding instructions within your . The instruction sets the preferred user name (or UID) and optionally the user group (or GID) while running the image — and for any subsequent , , or instructions: Your CPU can only run binaries for its native architecture. For example, Docker images built for an x86 system can’t run on an Arm-based system. With Apple fully transitioning to their custom Arm-based silicon, it’s possible that your x86 (Intel or AMD) container image won’t work with Apple’s M-series chips. Consequently, we always recommended . Below is the Docker image that lets you query the multi-platform status of any public image in any public registry: We introduced the command to help you build multi-architecture images. is a Docker component that enables many powerful build features with a familiar Docker user experience. All Buildx builds run using the engine. BuildKit is designed to excel at multi-platform builds, or those not just targeting the user’s local platform. When you invoke a build, you can set the flag to specify the build output’s target platform (like , , , etc.): Docker containers are ephemeral in nature. They can be stopped and destroyed, then either rebuilt or replaced with minimal effort. You can terminate containers by sending a notice signal to the process. This little grace period requires you to ensure that your app is handling ongoing requests and cleaning up resources in a timely fashion. On the other hand, Node.js accepts and forwards signals like and from the OS, which is key to properly shutting down your app. Node.js lets your app decide how to handle those signals. If you don’t write code or use a module to handle them, your app won’t shut down gracefully. It’ll ignore those signals until Docker or Kubernetes kills it after a timeout period. Using certain options like or within your is viable when you can’t change your app code. However, we recommend writing code to handle proper signal handling for graceful shutdowns. Check out (12:57) where he covers all three available Node shutdown options in detail. How do Node developers make their apps faster and more performant? Generally, developers rely on third-party observability tools to measure application performance. This performance monitoring is essential for creating multi-functional Node applications with top notch user experiences. Observability extends beyond application performance. Metrics, traces, and logs are now front and center. Metrics help developers to understand what’s wrong with the system, while traces help you discover how it’s wrong. Logs tell you why it’s wrong. Developers can dig into particular metrics and traces to holistically understand system behavior. Observing Node applications means tracking your Node metrics, requests rates, request error rate, and request durations. OpenTelemetry is one popular collection of tools and APIs that help you instrument your Node.js application. You can also use an open-source tool like to analyze your app’s performance. Since SigNoz offers a full-stack observability tool, you don’t need to rely on multiple tools. In this guide, we explored many ways to optimize your Docker images — from carefully crafting your to securing your image via Snyk scanning. Building better Node.js apps doesn’t have to be complex. By nailing some core fundamentals, you’ll be in great shape. If you’d like to dig deeper, check out these additional recommendations and best practices for building secure, production-grade Docker images: