Containerization has become a critical part of modern application development and deployment workflows. Docker allows developers to package their applications, including all dependencies, into a portable image. One of the best practices in Dockerfile creation is using multistage builds, which helps in creating lightweight, production-ready images. In this blog, we will walk through the process of containerizing a Node.js ToDo app using Docker, while following Dockerfile best practices.
Benefits of Docker Multistage Builds:
Reduced Image Size: Multistage builds allow you to separate the build environment from the runtime environment. This means that all build dependencies, source code, and other large assets can be excluded from the final image, significantly reducing its size.
Improved Security: By eliminating unnecessary files and dependencies, you reduce the attack surface of your containers. Only the minimal set of files needed to run the application is included in the final image.
Simplified Build Process: Multistage builds allow you to define multiple stages in a single Dockerfile, improving maintainability. This simplifies the build process by consolidating everything into one file.
Environment Optimization: By separating stages, you can use different base images to build and run your application. For example, you might use a full Node.js image to build your app but switch to an optimized Nginx image to serve the built files.
Best Practices for Writing Dockerfiles:
Before we dive into the technical steps, here are some best practices when creating a Dockerfile to ensure efficient, secure, and maintainable Docker images:
Use Official Images: Start from minimal, official base images, such as
alpine
or specific language runtime images likenode:alpine
, to minimize vulnerabilities and unnecessary software.Leverage Docker’s Layer Caching: Order your
RUN
,COPY
, andADD
commands wisely. Docker caches each layer, so placing commands that change frequently (like copying the source code) near the end will help leverage caching and speed up builds.Minimize Layers: Each
RUN
,COPY
, andADD
the command creates a new layer in the image. To optimize image size, combine multiple commands into a singleRUN
instruction when possible.Exclude Unnecessary Files: Use
.dockerignore
to exclude files and directories that are not needed in the container, such as local development files,node_modules
(if building in a multistage), or configuration files.Use Multi-Stage Builds: As discussed earlier, multistage builds allow for separate build and deployment stages, resulting in smaller, more secure images.
Now that we’ve covered the benefits and best practices, let’s dive into containerizing our Node.js application.
Step 1: Clone the Application Repository
We’ll start by cloning a sample ToDo app repository, which has a simple Node.js web application. You can use the one linked below or any other Node.js application of your choice.
git clone https://github.com/piyushsachdeva/todoapp-docker.git
cd todoapp-docker/
Step 2: Create a Dockerfile
Now, we’ll create a Dockerfile. A Dockerfile contains instructions to build a Docker image. In this file, we’ll specify how to set up the environment for our application.
touch Dockerfile
Open the Dockerfile
in your favorite text editor (e.g., nano
, vim
, or Visual Studio Code), and paste the following content:
# Stage 1: Build the application
FROM node:18-alpine AS installer
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Stage 2: Run the application using Nginx
FROM nginx:latest AS deployer
COPY --from=installer /app/build /usr/share/nginx/html
Explanation:
First Stage (installer):
We are using the
node:18-alpine
image, which is a lightweight Node.js image.The
WORKDIR
the command sets the working directory inside the container to/app
.The
COPY package*.json ./
copies both thepackage.json
andpackage-lock.json
files from the host system to the/app
directory in the container.The
RUN npm install
the command installs the necessary dependencies.The
COPY . .
the command copies the entire project to the/app
directory.The
RUN npm run build
command builds the production-ready version of the application.
Second Stage (deployer):
We are using the
nginx:latest
image to serve our static files.The
COPY --from=installer /app/build /usr/share/nginx/html
command copies the built application from the first stage into the Nginx container’s HTML directory, which serves the web application.
Step 3: Build the Docker Image
Now, let’s build the Docker image using the docker build
command.
docker build -t todoapp-docker .
This command creates an image with the tag todoapp-docker
. The -t
option is used to name the image, and the .
at the end refers to the current directory, which contains the Dockerfile and the application code.
Step 4: Verify the Docker Image
Once the image is built, you can verify that it’s created and stored locally by running:
docker images
This command lists all the images present on your local Docker environment.
Step 5: Push the Image to Docker Hub
To share the image with others or deploy it to different environments, you can push the Docker image to a public repository on Docker Hub. First, login to Docker Hub using:
docker login
Once logged in, tag the image so that it can be pushed to your Docker Hub account.
docker tag todoapp-docker:latest <username>/test-repo:todoapp-docker
Now, push the image to your Docker Hub repository:
docker push <username>/test-repo:todoapp-docker
Step 6: Pull the Image on Another Environment
To pull this Docker image from another environment or machine, use the docker pull
command:
docker pull harshitsahu2311/test-repo:todoapp-docker
Step 7: Run the Docker Container
To start a container from the Docker image and expose it on port 3000, run the following command:
docker run -dp 80:80 harshitsahu2311/test-repo:todoapp-docker
This command maps port 80 inside the container to port 80 on the host system.
Step 8: Verify the Application
If you’ve followed the steps correctly, your app should now be accessible via http://localhost
. Open a browser and navigate to that URL to see the ToDo app running inside the Docker container.
Step 9: Accessing the Running Container
To enter into the running container, use the docker exec
command:
docker exec -it containername sh
You can also use the container ID instead of the container name.
Step 10: Viewing Docker Logs
To view the logs generated by the container, you can use the following commands:
docker logs containername
# or
docker logs containerid
Step 11: Inspecting the Container
To inspect the details of the container, such as its IP address, volumes, or network settings, use:
docker inspect containername
Step 12: Clean Up Old Docker Images
Over time, old Docker images can pile up and take up space. You can remove unused images using the following command:
docker image rm image-id
You can get the image ID by running docker images
and removing the ones you no longer need.
Follow me on Linkedin.