Containerization of a Node.js Service
This article discusses containerization, its overview, its benefits, and a step-by-step guide on creating and containerizing a Node.js service.
Join the DZone community and get the full member experience.
Join For FreeContainerization is bundling an application, its dependencies, and libraries together so that they can be used like plug-and-play on any kind of infrastructure. Each bundle is called a container.
Why Containerize a Node.js Service?
As discussed in the last section, containerizing a Node.js service would mean bundling the app, its dependencies, libraries, and even configuration into one container. Containerization has the following benefits:
- Portable. Since we have bundled all of the app requirements in a container, the same images can be installed in dev, staging, and production.
- Fast and lightweight. Containers tend to be much faster than virtual machines (VMs) or bare metal as they are only running the app's requirements, whereas historically, the VMs or bare metal would start up the whole machine, and all the apps
- Scalable. With the above two benefits, scalability becomes very easy as the containers are quick, efficient, and easy to deploy.
In this article, we will specifically focus on containerizing a Node.js app.
Prerequisites
Tools and Software
Docker (Docker Engine and Docker CLI)
We will need to install Docker to manage our containers. Docker Engine handles the runtime, and CLI can be used to interact through the command line.
Node.js and npm/yarn Installed (For Initial Development/Testing)
We will require Node.js and npm installed to install libraries' dependencies and run the service.
Kubernetes or Docker Compose to Manage Multiple Containers (Optional)
We will require Kubernetes or Docker Compose to manage multiple containers.
Fundamentals
Node.js application structure:
- It is expected that the reader already understands the role of the main file (
app.js
orindex.js
) in the project, along with the role of other files likepackage.json
andyarn.lock
. - This article will also not dive into other aspects of the project, like controllers, middleware, and routes.
Basic Docker Commands and Dockerfile Syntax
Docker commands:
docker ps -> Lists all the containers running on the system
docker pull -> Pulls any image from the docker hub or official registry of Docker
docker build -> Creates an image from the docker file
docker run -> Starts a container from an exiting image
docker stop -> Stops a container if it has crashed or if you want to switch the container
Core Dockerfile instructions:
FROM -> Every DockerFile
WORKDIR -> Set the working directory inside the container
COPY (or ADD) -> Transfers the application's files to the image
RUN -> Executes commands during build time
CMD -> Sets the default command to be run when the container is started from the image
EXPOSE -> Specifies the port the container listens on
ENV -> Sets environment variables used during build and runtime
Both the tables covered and the Node.js structure are enough to get started for containerization and deploy your Node.js service.
Setting Up the Node.js Service
Setting up the Node.js environment is a straightforward process. Ensure you have Node.js installed on your machine. If you have any doubts, please refer to the appendix (1). Once installed, open your terminal and verify the installation by typing.
node -v
npm -v
Create a project directory and initialize your project as follows:
npm init -y
Install the express module
npm install express
Create a server file, let's call it server.mjs, where we can add the route and the logic corresponding to the route. Since this article is more about containerization, we will keep the endpoint logic very straightforward. Something like this:
import express from "express";
const app = express();
const port = 8080;
app.get('/', (req, res) => {
res.send('Welcome to my demo service!');
});
app.listen(port, () => {
console.log(`Demo Service is running on port ${port}`);
});
Now your service is ready to be started, navigate to the project directory in the terminal and run this command:
node server.mjs
The service is up and running; if we visit http://localhost:3000, we will see:
"Welcome to my server"
Creating the Dockerfile
Lets revise what Dockerfile is, it contains the instructions to build the docker image. Let's create a Dockerfile in the root directory. During this step, as we discussed in the Dockerfile instructions, we need to do the following things:
FROM node:18-alpine => Indicate the base image to use. Here we're using the official Nodejs 14 image.
WORKDIR /usr/src/app => Sets the working directory in the container.
COPY package*.json ./ => Duplicate the package.json and package-lock.json files to the working directory.
RUN npm install => Installs the app package dependencies.
COPY . . => Copies the remaining of the app to the working directory.
EXPOSE 8080 => Exposes the port our app is listening on.
CMD ["node", "app.js"] => Defines the command to start your Node.js application.
Building and Running the Docker Image
From the root of the terminal, navigate to your project and run the following command:
docker build -t image-name .
Where image-name
is the name of the Docker image. The .
at the end sets the context to the current directory.
Once the image is built, we will create the container and run the Node.js app using the following command:
docker run --name container-name -p 8080:8080 image-name
Once the above is successful, you can verify that the service is running by running docker ps
, and then going to the same localhost URL as before.
Pushing Images to a Registry
Now that your image is ready, it's time to push it to a registry. For the scope of this article, let's push only to Docker Hub. Docker Hub is a cloud-based service for storing, sharing, and managing Docker container images.
Create an account at https://hub.docker.com/ and log in with your account.
docker login
Once logged in, locally built images can be added like:
docker tag image-name:tag dockerhub-username/repository-name:tag
Where:
tag
is eitherlatest
or a version number.repository-name
is the desired repository name.
Next, push the image as follows:
docker push dockerhub-username/repository-name:tag
Conclusion
As we can see, containerization makes complex workflows and services quick, portable, and scalable by separating the dependencies. Once implemented, the whole team benefits from it. I encourage you to explore the advanced features like multi-stage builds and container networking. Also, consider learning about orchestration tools (e.g., Kubernetes) and integrating CI/CD pipelines to optimize your development workflow.
Appendix
Opinions expressed by DZone contributors are their own.
Comments