Node.js

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 build command to create an image tagged as hello-node:1.0, packaging the app and all dependencies.
  • run the image using the docker run command, mapping port 3000 from 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

Fig. 1: Demo output 1
Fig. 1: Demo output 1

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.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button