DEV Community

Cover image for Dockerfiles: Building and Running a Node.js Application in a Container
Rahimah Sulayman
Rahimah Sulayman

Posted on

Dockerfiles: Building and Running a Node.js Application in a Container

Introduction

Containers provide a consistent way to package applications together with the dependencies and configuration required to run them.

A Dockerfile defines how that container image is built.

In this project, a Node.js application was containerized using a custom Dockerfile, with the image built, tested, inspected, modified, and rebuilt throughout the implementation.

The project demonstrates how Dockerfile instructions influence the resulting image and, ultimately, the behavior of the running container.


What Is a Dockerfile?

A Dockerfile is a text file containing a sequence of instructions Docker uses to build an image.

A simplified workflow looks like this:

Application Source Code
        │
        ▼
    Dockerfile
        │
        │ docker build
        ▼
   Docker Image
        │
        │ docker run
        ▼
 Docker Container
        │
        ▼
 Running Application
Enter fullscreen mode Exit fullscreen mode

The Dockerfile describes the environment and instructions required to turn application source code into a runnable container image.


Dockerfile vs Docker Image vs Container

These three concepts are closely related but serve different purposes.

Dockerfile

The Dockerfile is the build blueprint.

It defines instructions such as:

FROM
WORKDIR
COPY
RUN
EXPOSE
CMD
Enter fullscreen mode Exit fullscreen mode

Docker Image

The image is the built artifact produced from the Dockerfile.

It contains the application, dependencies, filesystem layers, and configuration required to create a container.

Docker Container

The container is a running instance of an image.

In simple terms:

Dockerfile
    │
    │ docker build
    ▼
Docker Image
    │
    │ docker run
    ▼
Docker Container
Enter fullscreen mode Exit fullscreen mode

Project Overview

The application used for this project is a simple Node.js web application.

The containerized application consists of:

  • A Node.js application
  • A server.js entry point
  • A package.json file
  • Installed Node.js dependencies
  • A Dockerfile defining the image build process

The Dockerfile is responsible for packaging these components into a reproducible container image.


Project Structure

Dockerfiles-lab/
├── screenshots/
├── .gitignore
├── Dockerfile
├── package.json
├── package-lock.json
├── README.md
└── server.js
Enter fullscreen mode Exit fullscreen mode

The repository also contains implementation screenshots documenting the different stages of the Dockerfile and container lifecycle.

The complete implementation is available in the GitHub repository linked at the end of this article.


The Node.js Application

Before building the container, there needs to be an application for Docker to package.

The application entry point is:

server.js
Enter fullscreen mode Exit fullscreen mode

A simplified Node.js server can look like:

const http = require("http");

const server = http.createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "text/plain" });
  res.end("Hello from Docker!");
});

server.listen(3000, () => {
  console.log("Server running on port 3000");
});
Enter fullscreen mode Exit fullscreen mode

The application listens on port 3000.

This gives Docker something concrete to package and run inside the container.


Why package.json Matters

The project also contains:

package.json
Enter fullscreen mode Exit fullscreen mode

For Node.js applications, package.json provides project metadata and dependency information.

It can also define scripts used to start or manage the application.

For example:

{
  "scripts": {
    "start": "node server.js"
  }
}
Enter fullscreen mode Exit fullscreen mode

The associated lock file:

package-lock.json
Enter fullscreen mode Exit fullscreen mode

records the resolved dependency versions.

Together, these files make the Node.js application's dependencies and runtime configuration reproducible.


Building the First Docker Image

The first stage of the implementation involved building a Docker image from the Dockerfile.

The basic command is:

docker build -t docker-node-app:1.0 .
Enter fullscreen mode Exit fullscreen mode

The components of the command are:

docker build
     │
     ├── -t docker-node-app:1.0
     │       └── image name and tag
     │
     └── .
         └── current directory as build context
Enter fullscreen mode Exit fullscreen mode

The . tells Docker to use the current directory as the build context.

This makes files in the project directory available to Docker during the build, subject to .dockerignore rules where applicable.


Understanding the Dockerfile

A Dockerfile is processed from top to bottom.

A typical Node.js Dockerfile can contain instructions such as:

FROM node:20-alpine

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 3000

CMD ["node", "server.js"]
Enter fullscreen mode Exit fullscreen mode

Each instruction has a specific purpose.


FROM: Choosing the Base Image

FROM node:20-alpine
Enter fullscreen mode Exit fullscreen mode

FROM specifies the base image from which the new image is built.

For a Node.js application, using a Node.js base image provides the Node.js runtime and supporting environment required to execute the application.

The alpine variant is based on Alpine Linux and is commonly used when a smaller image footprint is desirable.

The base image is therefore the starting point for the container environment.


WORKDIR: Setting the Working Directory

WORKDIR /app
Enter fullscreen mode Exit fullscreen mode

WORKDIR establishes the working directory for subsequent Dockerfile instructions.

Instead of placing application files throughout the container filesystem, the application can be organized under:

/app
Enter fullscreen mode Exit fullscreen mode

It also determines the default working directory when the container starts unless another directory is explicitly configured.


COPY: Adding Application Files

The application files need to be transferred from the build context into the image.

For example:

COPY package*.json ./
Enter fullscreen mode Exit fullscreen mode

This copies the Node.js package definition files into the working directory.

Later:

COPY . .
Enter fullscreen mode Exit fullscreen mode

copies the remaining application files into the image.

This separation is useful because dependency installation and application source code can be handled as separate build layers.


RUN: Installing Dependencies

RUN npm install
Enter fullscreen mode Exit fullscreen mode

RUN executes a command while the image is being built.

Here, npm installs the dependencies defined by the Node.js project.

The resulting files become part of the image layer produced by this instruction.

This is different from CMD.

RUN happens during image build time.


EXPOSE: Documenting the Application Port

EXPOSE 3000
Enter fullscreen mode Exit fullscreen mode

The Node.js application listens on port 3000, so the Dockerfile declares:

EXPOSE 3000
Enter fullscreen mode Exit fullscreen mode

It is important to understand that EXPOSE does not publish the port to the host by itself.

It documents the port the application expects to use inside the container.

Host-to-container port publishing is configured when the container is started.

For example:

docker run -d -p 3000:3000 --name docker-node-container docker-node-app:1.0
Enter fullscreen mode Exit fullscreen mode

The mapping is:

Host                  Container
 3000       ───────►    3000
Enter fullscreen mode Exit fullscreen mode

CMD: Defining the Default Container Command

One of the most important Dockerfile instructions in this project was:

CMD ["node", "server.js"]
Enter fullscreen mode Exit fullscreen mode

CMD defines the default command executed when a container is started from the image.

In this case, Docker starts:

node server.js
Enter fullscreen mode Exit fullscreen mode

inside the container.

This is what launches the Node.js application.


Why CMD Matters

During the implementation, the application command was changed between:

CMD ["node", "server.js"]
Enter fullscreen mode Exit fullscreen mode

and:

CMD ["node", "app.js"]
Enter fullscreen mode Exit fullscreen mode

This demonstrated an important container behavior.

If the file referenced by CMD does not exist in the image, the container cannot successfully start the application.

This can result in a container that starts and immediately exits.

For example, if the Dockerfile contains:

CMD ["node", "app.js"]
Enter fullscreen mode Exit fullscreen mode

but the image does not contain:

/app/app.js
Enter fullscreen mode Exit fullscreen mode

Node.js cannot execute the requested entry point.

This is why examining container logs is an important troubleshooting step.


Building After a Dockerfile Change

A Docker image is a built artifact.

Changing the Dockerfile does not automatically modify an existing image.

After making changes, the image must be rebuilt:

docker build -t docker-node-app:2.0 .
Enter fullscreen mode Exit fullscreen mode

This creates a new image version containing the updated Dockerfile instructions.

The general workflow is:

Modify Dockerfile
       │
       ▼
docker build
       │
       ▼
New Image
       │
       ▼
docker run
       │
       ▼
Test Application
Enter fullscreen mode Exit fullscreen mode

This build-test cycle was repeated during the project to validate changes to the container configuration.


Inspecting Docker Images

After building an image, available images can be inspected with:

docker images
Enter fullscreen mode Exit fullscreen mode

This provides information such as:

  • Repository
  • Tag
  • Image ID
  • Creation time
  • Image size

For example:

REPOSITORY       TAG       IMAGE ID       CREATED        SIZE
docker-node-app  1.0       abc123...      ...            ...
Enter fullscreen mode Exit fullscreen mode

Tags such as:

1.0
2.0
final
Enter fullscreen mode Exit fullscreen mode

can be used to distinguish different image versions during development and testing.


Running the Container

Once the image has been built, it can be started with:

docker run -d -p 3000:3000 \
  --name docker-node-container \
  docker-node-app:final
Enter fullscreen mode Exit fullscreen mode

The important options are:

Option Purpose
-d Runs the container in detached mode
-p 3000:3000 Maps host port 3000 to container port 3000
--name Assigns a name to the container
docker-node-app:final Specifies the image to run

The application can then be accessed from the host through:

http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

Verifying a Running Container

After starting the container, its status can be checked with:

docker ps
Enter fullscreen mode Exit fullscreen mode

This displays currently running containers.

Typical information includes:

  • Container ID
  • Image
  • Command
  • Creation time
  • Status
  • Port mappings
  • Container name

The port mapping should show that host port 3000 is forwarded to port 3000 inside the container.


Testing the Application

Once the container is running, the application can be accessed through a browser:

http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

At this point, the request flow is:

Browser
   │
   │ localhost:3000
   ▼
Host Port 3000
   │
   │ Docker port mapping
   ▼
Container Port 3000
   │
   ▼
Node.js
   │
   ▼
server.js
   │
   ▼
HTTP Response
Enter fullscreen mode Exit fullscreen mode

This demonstrates the relationship between the host, container networking, and the application process.


Troubleshooting a Failed Container

Not every container starts successfully on the first attempt.

During the implementation, an invalid application entry point was used to demonstrate container troubleshooting.

A failed run can be identified using:

docker ps -a
Enter fullscreen mode Exit fullscreen mode

Unlike docker ps, the -a option also displays stopped containers.

This is particularly useful when a container starts and then exits immediately.


Using Docker Logs for Troubleshooting

When a container fails, logs are often the fastest way to determine why.

The logs can be viewed with:

docker logs docker-node-container
Enter fullscreen mode Exit fullscreen mode

For example, if the Dockerfile attempts to execute a file that does not exist:

CMD ["node", "app.js"]
Enter fullscreen mode Exit fullscreen mode

Node.js may report that it cannot find the specified module.

The important troubleshooting pattern is:

Container exits
      │
      ▼
docker ps -a
      │
      ▼
Identify exited container
      │
      ▼
docker logs <container>
      │
      ▼
Identify application/configuration error
      │
      ▼
Fix Dockerfile or application
      │
      ▼
Rebuild image
      │
      ▼
Run container again
Enter fullscreen mode Exit fullscreen mode

This workflow is useful well beyond simple Docker labs.


Stopping and Removing a Container

Once a container is no longer needed, it can be stopped and removed with:

docker stop docker-node-container && docker rm docker-node-container
Enter fullscreen mode Exit fullscreen mode

The first command:

docker stop docker-node-container
Enter fullscreen mode Exit fullscreen mode

stops the running container.

The second:

docker rm docker-node-container
Enter fullscreen mode Exit fullscreen mode

removes the stopped container.

The && operator ensures the second command runs only if the first command succeeds.


Rebuilding and Retesting

After correcting the Dockerfile, the image was rebuilt and the container started again.

A typical sequence is:

docker build -t docker-node-app:final .
Enter fullscreen mode Exit fullscreen mode

followed by:

docker run -d -p 3000:3000 \
  --name docker-node-container \
  docker-node-app:final
Enter fullscreen mode Exit fullscreen mode

Then:

docker ps
Enter fullscreen mode Exit fullscreen mode

and finally:

http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

can be used to verify the result.

This build → run → inspect → troubleshoot → rebuild cycle is fundamental to working effectively with Dockerfiles.


Changing Application Output

The application response was also modified during testing.

For example:

res.end("Welcome to my Docker Node.js application!");
Enter fullscreen mode Exit fullscreen mode

After changing application source code, the image must be rebuilt if the source code is copied into the image during the build:

docker build -t docker-node-app:final .
Enter fullscreen mode Exit fullscreen mode

The updated image then needs to be used when starting a new container.

This demonstrates an important principle:

A running container does not automatically become a new image when the source code or Dockerfile changes.

The image must be rebuilt when the application contents packaged into it need to change.


Docker Build Cache

Docker builds are made up of layers.

For example:

FROM node:20-alpine

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 3000

CMD ["node", "server.js"]
Enter fullscreen mode Exit fullscreen mode

Each instruction can contribute to the image's layer structure.

Docker can reuse unchanged layers during subsequent builds, which can significantly improve build performance.

This is one reason Dockerfiles are commonly structured to copy dependency definition files before copying the rest of the application source:

COPY package*.json ./
RUN npm install

COPY . .
Enter fullscreen mode Exit fullscreen mode

If only application source code changes, Docker may be able to reuse the dependency installation layer.


Dockerfile Best Practices

A production-oriented Dockerfile should be designed with efficiency, security, and reproducibility in mind.

1. Use an appropriate base image

Choose a maintained base image that provides only what the application needs.

For example:

FROM node:20-alpine
Enter fullscreen mode Exit fullscreen mode

A smaller base image can reduce the image footprint and attack surface, although compatibility should always be considered.

2. Keep the image focused

Only copy files required by the application.

A .dockerignore file can prevent unnecessary files from being included in the build context.

Example:

node_modules
.git
.env
npm-debug.log
Enter fullscreen mode Exit fullscreen mode

3. Take advantage of layer caching

Copy dependency files before application source code:

COPY package*.json ./
RUN npm install

COPY . .
Enter fullscreen mode Exit fullscreen mode

This can prevent unnecessary dependency reinstallation when only source files change.

4. Avoid embedding secrets

Do not place passwords, API keys, tokens, or other sensitive values directly into a Dockerfile.

Use appropriate runtime configuration and secrets-management mechanisms instead.

5. Run as a non-root user

Where appropriate, configure the container to run the application as a non-root user.

This reduces the impact of a potential application compromise.

6. Use explicit image tags

Rather than relying blindly on:

FROM node:latest
Enter fullscreen mode Exit fullscreen mode

consider using an explicit version:

FROM node:20-alpine
Enter fullscreen mode Exit fullscreen mode

This makes builds more predictable and reproducible.


Dockerfile Instructions at a Glance

Instruction Purpose
FROM Defines the base image
WORKDIR Sets the working directory
COPY Copies files into the image
RUN Executes commands during image build
EXPOSE Documents a container port
CMD Defines the default runtime command

A useful distinction is:

RUN
└── Build time

CMD
└── Container runtime
Enter fullscreen mode Exit fullscreen mode

Understanding this difference is essential when diagnosing why a container behaves differently from the image build process.


Dockerfile Workflow

The complete workflow demonstrated in this project can be summarized as:

Application Source
       │
       ▼
   Dockerfile
       │
       │ docker build
       ▼
   Docker Image
       │
       │ docker images
       ▼
   Image Inspection
       │
       │ docker run
       ▼
 Docker Container
       │
       ├── docker ps
       │
       ├── Browser Test
       │
       └── docker logs
               │
               ▼
          Troubleshooting
               │
               ▼
        Modify Application
               │
               ▼
          Rebuild Image
               │
               ▼
        Run & Test Again
Enter fullscreen mode Exit fullscreen mode

This workflow provides a repeatable approach to packaging and validating containerized applications.


Dockerfile vs Docker Compose

Dockerfiles and Docker Compose solve different problems, but they complement each other.

Dockerfile Docker Compose
Defines how an image is built Defines how multiple services run together
Creates a custom image Orchestrates application services
Focuses on image construction Focuses on service configuration
Uses Dockerfile instructions Uses YAML configuration
docker build docker compose up
docker run starts the resulting image Compose manages the defined application stack

A Dockerfile might create the image for an application:

Dockerfile
     │
     ▼
Application Image
Enter fullscreen mode Exit fullscreen mode

Docker Compose can then use that image as part of a larger application:

                Docker Compose
                     │
          ┌──────────┴──────────┐
          ▼                     ▼
    Application             Database
     Container              Container
Enter fullscreen mode Exit fullscreen mode

Understanding both technologies provides a stronger foundation for building and operating containerized applications.


Key Docker Commands

The primary commands used throughout the project include:

Build an image

docker build -t docker-node-app:final .
Enter fullscreen mode Exit fullscreen mode

List images

docker images
Enter fullscreen mode Exit fullscreen mode

Run a container

docker run -d -p 3000:3000 \
  --name docker-node-container \
  docker-node-app:final
Enter fullscreen mode Exit fullscreen mode

List running containers

docker ps
Enter fullscreen mode Exit fullscreen mode

List all containers

docker ps -a
Enter fullscreen mode Exit fullscreen mode

View container logs

docker logs docker-node-container
Enter fullscreen mode Exit fullscreen mode

Stop and remove a container

docker stop docker-node-container && docker rm docker-node-container
Enter fullscreen mode Exit fullscreen mode

Skills Demonstrated

This implementation demonstrates practical experience with:

  • Dockerfile authoring
  • Docker image creation
  • Docker image tagging
  • Docker build context
  • Docker build layers and caching
  • Node.js containerization
  • Base image selection
  • Working directory configuration
  • Application file management with COPY
  • Dependency installation with RUN
  • Container port configuration
  • Runtime commands with CMD
  • Docker container lifecycle management
  • Port mapping
  • Container inspection
  • Container log analysis
  • Troubleshooting failed containers
  • Image rebuild and validation workflows
  • Linux command-line operations
  • Git and GitHub
  • Technical documentation

Conclusion

A Dockerfile provides the foundation for turning application source code into a portable, reproducible container image.

By defining the base runtime, working directory, application files, dependencies, exposed port, and startup command, the Dockerfile establishes how the application is packaged and executed.

The implementation also demonstrates that containerization is more than simply building an image. A reliable workflow includes:

Build
  ↓
Run
  ↓
Inspect
  ↓
Test
  ↓
Troubleshoot
  ↓
Rebuild
  ↓
Validate
Enter fullscreen mode Exit fullscreen mode

These concepts form a practical foundation for container-based application development and provide the building blocks for more advanced workflows involving Docker Compose, CI/CD pipelines, container registries, orchestration platforms, and cloud-native deployments.


Project Repository

The complete implementation, Dockerfile iterations, application files, and supporting screenshots are available on GitHub:

https://github.com/rahimahisah17/dockerfiles-lab

The repository provides the complete implementation and evidence of the build, run, troubleshooting, and validation process documented in this article.


Related Work

This Dockerfile implementation pairs naturally with a multi-container deployment workflow using Docker Compose.

Docker Compose can be used to take individually containerized applications and combine them with supporting services such as databases, caches, and other dependencies into a complete application stack.

Top comments (3)

Collapse
 
rahimah_dev profile image
Rahimah Sulayman

A Dockerfile may look simple, but understanding how each instruction affects the final image and container behavior makes a huge difference. Which Dockerfile instruction do you find most important in your workflow?

Collapse
 
rahimah_dev profile image
Rahimah Sulayman

The build → run → inspect → troubleshoot → rebuild cycle is a practical part of working with containers.

Collapse
 
rahimah_dev profile image
Rahimah Sulayman

Dockerfiles and Docker Compose solve different problems but work extremely well together. How are you currently using Dockerfiles in your development or deployment workflows?