In this tutorial, instead of creating and running a Node app locally, you'll to take advantage of the Debian Linux operating system that official Docker Node images are based on. You'll create a portable Node development environment that solves the "But it runs on my machine" problem that constantly trolls developers since containers are created predictably from the execution of Docker images on any platform.
Throughout this tutorial, you'll be working in two realms:
Local Operating System: Using a CLI application, such as Terminal or PowerShell, you'll use a local installation of Docker to build images and run them as containers.
Container Operating System: Using Docker commands, you'll access the base operating system of a running container. Within that context, you'll use a container shell to issue commands to create and run a Node app.
The container operating system runs in isolation from the local operating system. Any files created within the container won't be accessible locally. Any servers running within the container can't listen to requests made from a local web browser. This is not ideal for local development. To overcome these limitations, you'll bridge these two systems by doing the following:
Mount a local folder to the container filesystem: Using this mount point as your container working directory, you'll persist locally any files created within the container and you'll make the container aware of any local changes made to project files.
Allow the host to interact with the container network: By mapping a local port to a container port, any HTTP requests made to the local port will be redirected by Docker to the container port.
To see this Docker-based Node development strategy in action, you'll create a basic Node Express web server. Let's get started!
Removing the Burden of Installing Node
To run a simple "Hello World!" Node app, the typical tutorial asks to:
- Download and install Node
- Download and install Yarn
- To use different versions of Node, uninstall Node and install
nvm
- Install NPM packages globally
Each operating system has its own quirks making the aforementioned installations non-standard. However, access to the Node ecosystem can be standardized using Docker images. The only installation requirement for this tutorial is Docker. If you need to install Docker, choose your operating system from this Docker installation document and follow the steps provided.
Similar to how NPM works, Docker gives us access to a large registry of Docker images called Docker Hub. From Docker Hub, you can pull and run different versions of Node as images. You can then run these images as local processes that don't overlap or conflict with each other. You can simultaneously create a cross-platform project that depends on Node 8 with NPM and another one that depends on Node 11 with Yarn.
Creating the Project Foundation
To start, anywhere in your system, create a
node-docker
folder. This is the project directory.With the goal of running a Node Express server, under the
node-docker
project directory, create a server.js
file, populate it as follows and save it:// server.js const express = require("express"); const app = express(); const PORT = process.env.PORT || 8080; app.get("/", (req, res) => { res.send(` <h1>Docker + Node</h1> <span>A match made in the cloud</span> `); }); app.listen(PORT, () => { console.log(`Server listening on port ${PORT}...`); });
A Node project needs a
package.json
file and a node_modules
folder. Assuming that Node is not installed in your system, you'll use Docker to create those files following a structured workflow.Accessing the Container Operating System
You can gain access to the container OS with any of the following methods:
- Using a single but long
command.docker run
- Using a
combined with a shortDockerfile
command.docker run
- Using a
in combination with Docker Compose.Dockerfile
Using a single docker run
command
docker run
Execute the following command:
docker run --rm -it --name node-docker \ -v $PWD:/home/app -w /home/app \ -e "PORT=3000" -p 8080:3000 \ -u node node:latest /bin/bash
Let's breakdown this
docker run
command to understand how helps you access the container shell:docker run --rm -it
docker run
creates a new container instance. The --rm
flag automatically stops and removes the container once the container exits. The combined -i
and -t
flags run interactive processes such as a shell. The -i
flag keeps the STDIN (Standard Input) open while the -t
flag lets the process pretend it is a text terminal and pass along signals.Think of
as "out of sight, out of mind".--rm
Without the
team, you won't see anything on the screen.-it
docker run --rm -it --name node-docker
The
flag assigns a friendly name to the container to easily identify it in logs and tables. For example when you run --name
.docker ps
docker run --rm -it --name node-docker \ -v $PWD:/home/app -w /home/app
The
flag mounts a local folder into a container folder using this mapping as its argument:-v
<HOST FOLDER RELATIVE PATH>:<CONTAINER FOLDER ABSOLUTE PATH>
An environmental variable can print the current working directory when the command is executed:
$PWD
on Mac and Linux and $CD
on Windows. The -w
flag sets the mounting point as the container working directory.docker run --rm -it --name node-docker \ -v $PWD:/home/app -w /home/app \ -e "PORT=3000" -p 8080:3000
The
flag sets an environmental variable -e
PORT
with a value of 3000
. The -p
flag maps a local port 8080
to a container port 3000
to match the environmental variable PORT
that is consumed within server.js
:const PORT = process.env.PORT || 8080;
docker run --rm -it --name node-docker \ -v $PWD:/home/app -w /home/app \ -e "PORT=3000" -p 8080:3000 \ -u node node:latest /bin/bash
For security and to avoid file permission problems, the
flag sets the non-root user -u
node
available in the Node image as the user that runs the container processes. After setting the flags, the image to execute is specified: node:latest
. The last argument is a command to execute inside the container once it's running. /bin/bash
invokes the container shell.If the image is not present locally, Docker issues
in the background to download it from Docker Hub.docker pull
Once the command executes, you'll see the container shell prompt:
node@<CONTAINER ID>:/home/app$
Before moving to the next method, exit the container terminal by typing
exit
and pressing <ENTER>
. Using a Dockerfile
Dockerfile
The
docker run
command from the previous section is made of image build time and container runtime flags and elements: docker run --rm -it --name node-docker \ -v $PWD:/home/app -w /home/app \ -e "PORT=3000" -p 8080:3000 \ -u node node:latest /bin/bash
Anything related to image build time can be defined as a custom image using a
Dockerfile
as follows:
specifies the container base image:FROM
node:latest
definesWORKDIR
-w
definesUSER
-u
definesENV
-e
specifies to executeENTRYPOINT
once the container runs/bin/bash
Based on this, under the
node-docker
project directory, create a file named Dockerfile
, populate it as follows, and save it:FROM node:latest WORKDIR /home/app USER node ENV PORT 3000 EXPOSE 3000 ENTRYPOINT /bin/bash
EXPOSE 3000
documents the port to expose at runtime. However, container runtime flags that define container name, port mapping, and volume mounting still need to be specified with docker run
.The custom image defined within
Dockerfile
needs to be built using docker build
before it can be run. In your local terminal, execute:docker build -t node-docker .
docker build
provides your image the friendly name node-docker
using the -t
flag. This is different than the container name. To verify that the image was created, run docker images
.With the image built, execute this shorter command to run the server:
docker run --rm -it --name node-docker \ -v $PWD:/home/app -p 8080:3000 \ node-docker
The container shell prompts comes up with the following format:
node@<CONTAINER ID>:/home/app$
Once again, before moving to the next method, exit the container terminal by typing
exit
and pressing <ENTER>
.Using Docker Compose
For Linux, Docker Compose is installed separately.
Based on the
Dockerfile
and the shorter docker run
command of the previous section, you can create a Docker Compose YAML file to define your Node development environment as a service:
:Dockerfile
FROM node:latest WORKDIR /home/app USER node ENV PORT 3000 EXPOSE 3000 ENTRYPOINT /bin/bash
Command
docker run --rm -it --name node-docker \ -v $PWD:/home/app -p 8080:3000 \ node-docker
The only elements left to abstract from the
docker run
command are the container name, the volume mounting, and the port mapping.Under the
node-docker
project directory, create a file named docker-compose.yml
, populate it with the following content, and save it:version: "3" services: nod_dev_env: build: . container_name: node-docker ports: - 8080:3000 volumes: - ./:/home/app
gives the service a name to easily identify itnod_dev_env
specifies the path to thebuild
Dockerfile
provides a friendly name to the containercontainer_name
configures host-to-container port mappingports
defines the mounting point of a local folder into a container foldervolumes
To start and run this service, execute the following command:
docker-compose up
up
builds its own images and containers separate from those created by the docker run
and docker build
commands used before. To verify this run:docker image # Notice the image named <project-folder>_nod_dev_env docker ps -a # Notice the container named <project-folder>_nod_dev_env_<number>
up
created an image and a container but the container shell prompt didn't come up. What happened? up
starts the full service composition defined in docker-compose.yml
. However, it doesn't present interactive output; instead, it only presents static service logs. To get interactive output, you use docker-compose run
instead to run nod_dev_env
individually. First, to clean the images and containers created by
up
, execute the following command in your local terminal:docker-compose down
Then, execute the following command to run the service:
docker-compose run --rm --service-ports nod_dev_env
The
run
command acts like docker run -it
; however, it doesn't map and expose any container ports to the host. In order to use the port mapping configured in the Docker Compose file, you use the --service-ports
flag. The container shell prompt comes up once again with the following format:node@<CONTAINER ID>:/home/app$
If for any reason the ports specified in the Docker Compose file are already in use, you can use the
--publish
, (-p
) flag to manually specify a different port mapping. For example, the following command maps the host port 4000
to the container port 3000
:docker-compose run --rm -p 4000:3000 nod_dev_env
Installing Dependencies and Running the Server
If you don't have an active container shell, using any of the previous section methods to access it.
In the container shell, initialize the Node project and install dependencies by issuing the following commands (if you prefer, use
):npm
yarn init -y yarn add express yarn add -D nodemon
Verify that
package.json
and node_modules
are now present under your local node-docker
project directory.
streamlines your development workflow by restarting the server automatically anytime you make changes to source code. To configure nodemon
nodemon
, update package.json
as follows:{ // Other properties... "scripts": { "start": "nodemon server.js" } }
In the container shell, execute
yarn start
to run the Node server.To test the server, visit
in your local browser. Docker intelligently redirects the request from the host port http://localhost:8080/
8080
to the container port 3000
.To test the connection of local file content and server, open
server.js
locally, update the response as follows and save the changes:// server.js // package and constant definitions... app.get("/", (req, res) => { res.send(` <h1>Hello From Node Running Inside Docker</h1> `); }); // server listening...
Refresh the browser window and observe the new response.
Modifying and Extending the Project
Assuming that Node is not installed in your local system, you can use the local terminal to modify project structure and file content but you can't issue any Node-related commands, such as
yarn add
. As the server runs within the container, you are also not able to make server requests to the internal container port 3000
.In the event that you want to interact with the server within the container or modify the Node project, you need to execute commands against the running container using
and the running container ID. You don't use docker exec
docker run
as that command would create a new isolated container.Getting the running container ID is easy.
- If you already have a container shell open, the container ID is present in the shell prompt:
node@<CONTAINER ID>:/home/app$
- You can also get the container ID programmatically using
to filter containers based on name and return thedocker ps
of any match:CONTAINER ID
docker ps -qf "name=node-docker"
The
flag filters containers by the-f
condition. Thename=node-docker
(-q
) limits the output to only display the ID of the match, effectively plugging the--quiet
ofCONTAINER ID
into thenode-docker
command.docker exec
Once you have the container ID, you can use
docker exec
to:- Open a new instance of the running container shell:
docker exec -it $(docker ps -qf "name=node-docker") /bin/bash
- Make a server request using the internal port
:3000
docker exec -it $(docker ps -qf "name=node-docker") curl localhost:3000
- Install or remove dependencies:
docker exec -it $(docker ps -qf "name=node-docker") yarn add body-parser
One you have another active container shell, you can easily run
andcurl
there instead.yarn add
Recap... and Uncovering Little Lies
You've learned how to create an isolated Node development environment through different levels of complexity: by running a single
docker run
command, using a Dockerfile
to build and run a custom image, and using Docker Compose to run a container as a Docker service.Each level requires more file configuration but a shorter command to run the container. This is a worthy trade-off as encapsulating configuration in files makes the environment portable and easier to maintain. Additionally, you learned how to interact with a running container to extend your project.
You may still need to install Node locally for IDEs to provide syntax assistance, or you can use a CLI editor like
vim
within the container.Even so, you can still benefit from this isolated development environment. If you restrict project setup, installation, and runtime steps to be executed within the container, you can standardize those steps for your team as everyone would be executing commands using the same version of Linux. Also, all the cache and hidden files created by Node tools stay within the container and don't pollute your local system. Oh, and you also get
yarn
for free!JetBrains is starting to offer the capability to use Docker images as remote interpreters for Node and Python when running and debugging applications. In the future, we may become entirely free from downloading and installing these tools directly in our systems. Stay tuned to see what the industry provides us to make our developer environments standard and portable.
About Auth0
Auth0 by Okta takes a modern approach to customer identity and enables organizations to provide secure access to any application, for any user. Auth0 is a highly customizable platform that is as simple as development teams want, and as flexible as they need. Safeguarding billions of login transactions each month, Auth0 delivers convenience, privacy, and security so customers can focus on innovation. For more information, visit https://auth0.com.
About the author
Dan Arias
Staff Developer Advocate
The majority of my engineering work revolves around AWS, React, and Node, but my research and content development involves a wide range of topics such as Golang, performance, and cryptography. Additionally, I am one of the core maintainers of this blog. Running a blog at scale with over 600,000 unique visitors per month is quite challenging!
I was an Auth0 customer before I became an employee, and I've always loved how much easier it is to implement authentication with Auth0. Curious to try it out? Sign up for a free account ⚡️.View profile