Docker Level 4, Task 4: "Dockerizing" a Node.js Application from Scratch¶
Today's task was the most complete and realistic developer workflow I've encountered so far. I was given the source code for a Node.js application and had to "Dockerize" it. This meant writing a Dockerfile from scratch, building a custom image containing the application, and then running that image as a container.
This process is the absolute core of how modern applications are packaged and deployed. It taught me the standard pattern for building Node.js images, including how to optimize the build process using Docker's layer caching. This document is my detailed, first-person guide to that entire end-to-end process.
Table of Contents¶
- The Task
- My Step-by-Step Solution
- Why Did I Do This? (The "What & Why")
- Deep Dive: A Line-by-Line Explanation of My Node.js
Dockerfile - Common Pitfalls
- Exploring the Commands Used
The Task¶
My objective was to take a Node.js application located in /node_app on App Server 3 and create a running container from it. The specific requirements were:
1. Create a Dockerfile in the /node_app directory.
2. Use a node image as the base.
3. The Dockerfile must install the app's dependencies using the package.json file.
4. The container's default command should be to run server.js.
5. The image must expose port 3004.
6. Build an image from this Dockerfile and name it nautilus/node-web-app.
7. Run a container from this image named nodeapp_nautilus.
8. Map the host port 8094 to the container's port 3004.
My Step-by-Step Solution¶
The solution followed a logical progression: write the recipe (Dockerfile), bake the cake (build the image), and then serve it (run the container).
Phase 1: Writing the Dockerfile¶
First, I needed to create the blueprint for my image.
1. I connected to App Server 3: ssh banner@stapp03.
2. I navigated to the application directory: cd /node_app.
3. I created and edited the Dockerfile: sudo vi Dockerfile.
4. Inside the editor, I wrote the following optimized instructions for a Node.js app:
# Start from an official Node.js runtime as the base image.
FROM node:18-alpine
# Set the working directory inside the container to /app.
WORKDIR /app
# Copy package.json (and package-lock.json if it exists) first.
COPY package*.json ./
# Install the application dependencies.
RUN npm install
# Copy the rest of the application's source code.
COPY . .
# Expose the port the application will run on.
EXPOSE 3004
# The command to run when the container starts.
CMD ["node", "server.js"]
Phase 2: Building the Custom Image¶
With the recipe written, I could now build the image.
1. Ensuring I was still in the /node_app directory, I ran the build command, giving it the required tag (-t).
sudo docker build -t nautilus/node-web-app .
sudo docker images, which showed nautilus/node-web-app at the top of the list.
Phase 3: Running the Container¶
The final step was to launch the application. 1. I ran the container using the image I just built, specifying the name and port mapping.
sudo docker run -d --name nodeapp_nautilus -p 8094:3004 nautilus/node-web-app
sudo docker ps, which showed the nodeapp_nautilus container and the 0.0.0.0:8094->3004/tcp port mapping.
3. Finally, I performed the required test with curl.
curl http://localhost:8094
Why Did I Do This? (The "What & Why")¶
- "Dockerizing" an Application: This is the core process of creating a self-contained, portable package for an application. By putting my app in a Docker image, I ensure that it runs the same way everywhere—on my laptop, on a testing server, or in production—because it always brings its own environment (the correct Node.js version and dependencies) with it.
- package.json: This is the standard manifest file for any Node.js project. It lists metadata about the project and, most importantly, the exact dependencies (libraries) the application needs to run.
- npm install: This is the command for the Node Package Manager. When run inside the Dockerfile, it reads the package.json file and downloads all the necessary libraries into a node_modules directory inside the image, making them available to the application.
- Layer Caching Optimization: The order of operations in my Dockerfile is very deliberate. By copying package.json first and running npm install before copying the rest of the source code, I take advantage of Docker's layer caching. If I later change my server.js file but not the dependencies, Docker can reuse the cached layer from the npm install step, making my rebuilds much, much faster.
Deep Dive: A Line-by-Line Explanation of My Node.js Dockerfile¶
This Dockerfile follows a standard and highly optimized pattern for Node.js applications.
[Image of a Node.js Dockerfile build process]
# 1. Start from a lightweight and secure base image.
# 'node:18-alpine' gives me Node.js version 18 on a minimal Alpine Linux base.
FROM node:18-alpine
# 2. Set the working directory inside the image.
# All subsequent commands (COPY, RUN, CMD) will be run from this /app directory.
WORKDIR /app
# 3. Copy the dependency manifest(s).
# This copies package.json and package-lock.json if it exists.
COPY package*.json ./
# 4. Install dependencies.
# This creates a new layer that only contains the node_modules.
# This layer will be cached and reused as long as package.json doesn't change.
RUN npm install
# 5. Copy the application source code.
# This copies server.js and any other files into the /app directory.
COPY . .
# 6. Expose the port.
# This is documentation; it tells Docker the container is intended to listen on this port.
EXPOSE 3004
# 7. Define the startup command.
# This tells the container to run 'node server.js' when it starts, launching the app.
# It runs in the foreground, which is essential for keeping the container alive.
CMD ["node", "server.js"]
Common Pitfalls¶
- Inefficient Layering: A common mistake is to copy all files (COPY . .) before running npm install. This is bad because a small change to any source file would invalidate the cache for the npm install layer, forcing a slow re-download of all dependencies on every build.
- Forgetting package.json: If the package.json file is not copied into the image, npm install has nothing to do and will fail or do nothing, and the application will crash at runtime from missing dependencies.
- Wrong WORKDIR: If WORKDIR is not set, files are copied to the root (/), which is messy. npm install would also run in the wrong place.
- Forgetting the . in docker build: The final . is critical. It tells Docker that the current directory is the "build context" from which to find the Dockerfile and the files to be copied.
Exploring the Commands Used¶
- sudo vi Dockerfile: The command to create and edit my Dockerfile.
- sudo docker build -t nautilus/node-web-app .: The command to build my custom image.
- -t: Tags the image with the specified name.
- .: Sets the build context to the current directory.
- sudo docker run -d --name nodeapp_nautilus -p 8094:3004 nautilus/node-web-app: The command to run my application.
- -d: Runs the container in detached mode.
- --name: Assigns a specific name to my container.
- -p 8094:3004: Publishes the port, mapping the host's port 8094 to the container's port 3004.
- curl http://localhost:8094: My final verification step to test that the running application was accessible and responding correctly.