How To Build And Deploy Containerized Node.js Apps
Containerization packages your application and its environment (runtime, libraries, and configuration) into a single immutable image that runs the same everywhere — your laptop, CI, staging, and production. This makes builds reproducible, deployment predictable, and scaling straightforward. We’ll use Docker to create a Node.js image and then deploy that image to Heroku’s Container Registry & Runtime. Let us delve into understanding how to containerize and deploy your nodejs applications.
1. What is Containerization? What are Containers?
Containerization is a lightweight virtualization technique that allows you to package an application together with all its required dependencies, libraries, configuration files, and runtime into a single, isolated unit called a container. This ensures that the application runs consistently across different environments—whether on a developer’s laptop, a staging server, or a cloud platform—without worrying about “it works on my machine” issues.
A container is an executable package that includes everything needed to run an application. Containers share the host OS kernel but remain fully isolated from each other, offering a secure and efficient way to deploy applications. Unlike virtual machines, containers do not require a full guest operating system, which makes them extremely fast to start, lightweight in size, and easier to scale.
Modern containerization platforms like Docker and orchestration tools such as Kubernetes have made it simple to build, distribute, and manage containers at scale. Containers also support immutable infrastructure practices, meaning once a container image is created, it can be deployed multiple times without manual configuration changes.
In summary, containerization streamlines development workflows, boosts deployment reliability, improves resource utilization, and supports seamless scaling of microservices-based applications.
2. Code Example
2.1 Pre-requisite
Before starting, ensure that you have the following installed and properly configured on your machine:
- Node.js (v16 or later) – Required to run JavaScript applications on the server.
- NPM – Comes bundled with Node.js and is used to manage dependencies.
- Docker – Required for containerizing your Node.js application into an image.
- Heroku CLI – Optional but needed if you want to deploy this containerized app to Heroku.
You can verify installations using:
# check node version node -v # check npm version npm -v # check docker version docker --version # check heroku cli version heroku --version
2.2 Create a package.json
Next, create a package.json file, which defines your project metadata, dependencies, scripts, and supported Node.js version. The start script tells Heroku and Docker how to run your app. The engines section ensures your app runs on Node.js 16 or above.
{
"name": "hello-node",
"version": "1.0.0",
"engines": {
"node": ">=16"
},
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2"
}
}
2.3 Code (server.js)
Below is the main server file of the application. It uses Express.js—one of the most popular Node.js frameworks—to create a simple HTTP server. The server listens on a port defined by the PORT environment variable. This is extremely important because platforms like Heroku dynamically assign ports, and hardcoding a port will cause your deployment to fail.
// initialize express application
const app = express();
// define port from environment variable or default to 3000
const PORT = process.env.PORT || 3000;
// create a simple route for the root URL
app.get('/', (req, res) => {
res.send('Hello from containerized Node.js!');
});
// start the server and listen on the defined port
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
Important Note: Heroku sets the port automatically via the PORT environment variable. Therefore, using process.env.PORT is mandatory when deploying Node.js applications to Heroku, especially when running inside Docker containers.
2.4 Dockerfile
Below is a production-ready Dockerfile that uses a multi-stage build to create a lightweight, secure, and optimized container image. The first stage (builder) installs dependencies and prepares the application. The second stage (runtime) copies only the necessary files, creates a non-root user for security, and runs the app in a minimal environment. This approach reduces the final image size and improves performance during deployments.
# Stage 1 FROM node:18-alpine AS builder WORKDIR /usr/src/app # Install dependencies (only package.json & package-lock first to leverage cache) COPY package*.json ./ RUN npm ci --production # Copy source COPY . . # Stage 2 FROM node:18-alpine AS runtime WORKDIR /usr/src/app # Create a non-root user RUN addgroup -S appgroup && adduser -S appuser -G appgroup # Copy dependencies and app from builder COPY --from=builder /usr/src/app /usr/src/app # Switch to non-root USER appuser ENV NODE_ENV=production EXPOSE 3000 CMD ["node", "server.js"]
2.4.1 Build Image and Run Locally
follow the steps below to build and test your containerized node.js application locally:
- build the Docker image from the project’s root directory.
- use the
docker buildcommand to create an image tagged ashello-node:1.0, packaging the app and all dependencies. - run the image using the
docker runcommand, mapping port3000from the container to your local machine. - Check for the log message that indicates the server has started successfully.
- open
http://localhost:3000/in your browser to confirm the application is running end-to-end.
3. Deploying to Heroku using the Container Registry
Heroku supports deploying Docker images via the Container Registry and Runtime. You can either push a built image to Heroku or let Heroku build images with heroku.yml. Below are the common steps to push a pre-built image and release it as a dyno.
# 1. Login to the heroku container registry heroku container:login # 2. Build the local image docker build -t registry.heroku.com/your-app-name/web . # 3. Push the image to Heroku registry docker push registry.heroku.com/your-app-name/web # 4. Release the pushed image (make it live) heroku container:release web --app your-app-name # 5. View logs / open heroku logs --tail --app your-app-name heroku open --app your-app-name
The commands above will log you into the Heroku container registry, build your Docker image, push it to Heroku, release it as the live version of your app, and allow you to check logs or open the deployed application.
> Logged in to registry.heroku.com Pushed: registry.heroku.com/your-app-name/web: digest: sha256:... Releasing: done > Release v12 created, web set to registry.heroku.com/your-app-name/web:1.0
4. Conclusion
Containerizing Node.js apps with Docker makes deployment predictable, simplifies scaling, and isolates runtime concerns. Use multi-stage builds, explicit base tags, and non-root users to keep images small and secure. When you’re ready to deploy, Heroku’s Container Registry & Runtime lets you push Docker images directly and run them as dynos — a convenient path for teams that want Heroku’s platform features with container portability. For the canonical Docker and Heroku references, see Docker’s Node.js guide and Heroku’s Container Registry docs.


