DEV Community

Akshay Gupta
Akshay Gupta

Posted on

Docker in development (with Node.js)

This post is going to help you find out how to setup docker in such a way that you can easily and quickly get started using docker in development environment with Node.js without much hassle!

We will be learning basics of Docker Volumes first and then move on to how to use volumes during the development phase!

Volumes are the preferred mechanism for persisting data generated by and used by Docker containers.

Basics Of Volumes

Creating volume is pretty simple using the docker create command

$ docker volume create myvol
Enter fullscreen mode Exit fullscreen mode

We can also remove the volume straight away by using the remove command

$ docker volume remove myvol
Enter fullscreen mode Exit fullscreen mode

You can also verify that the volume has been created by using list command to list volumes on your system:

$ docker volume ls
DRIVER    VOLUME NAME
local     88b0dd3439a42b08ab161dfb718b1fdcb548d776521f0e008a0e6b002ecd1ee7
local     96a6b003a662d7461c100e3bef816322f036adba8eef1483755551aa463ba7b4
local     myvol
Enter fullscreen mode Exit fullscreen mode

As we can see our volume myvol is created with local driver. We can also go ahead and get some more information regarding the volume with the inspect command

$ docker inspect myvol
[
    {
        "CreatedAt": "2021-09-13T18:20:00Z",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/myvol/_data",
        "Name": "myvol",
        "Options": {},
        "Scope": "local"
    }
]
Enter fullscreen mode Exit fullscreen mode

Among other information this command show the Mountpoint for our volume data, which is /var/lib/docker/volumes/myvol/_data. We can very well cd into this dir and see the data for the volume. This data could be your codebase, or the metadata or any other data that you store in the volume

But there is a catch!!

Are you a mac user ? If you're not a mac user you can skip this section but if you are this might be helpful. You can't directly cd into the /docker folder if you do try to do that it would give

$ cd /var/lib/docker
cd: no such file or directory: /var/lib/docker
Enter fullscreen mode Exit fullscreen mode

Why is that ?!

That is because Docker Desktop (on mac) actually runs a VM behind the scenes because docker, because of the way its made, is not directly compatible with mac. But there are ways to access the underlying data in the VM.

  • One option is to log into the shell using netcat
$ nc -U ~/Library/Containers/com.docker.docker/Data/debug-shell.sock

Enter fullscreen mode Exit fullscreen mode

You can then cd into the data directory

/ # cd /var/lib/docker/volumes
Enter fullscreen mode Exit fullscreen mode

You can exist the shell by typing exit command or pressing ctrl+c on keyboard

  • Another option is using nsenter in privileged container like below
docker run -it --privileged --pid=host debian nsenter -t 1 -m -u -n -i sh
Enter fullscreen mode Exit fullscreen mode

This will open the shell same way as the first option.
Checkout this gist by Bret Fisher to know more :)

Note: For windows users, docker artifacts can be found at \\wsl$\docker-desktop-data\version-pack-data\community\docker\. If this does not work, I would suggest going through related discussions on stackoverflow and docker forums (example: here) to see how to access data

Cool! Now that we are done with basics of volumes 🎉 Let's jump onto the code!

A Node.js Express API

Let's quickly setup an express application. We won't waste much time here we'll pull sample "hello world" example from express.js website

$ mkdir node_docker_demo
$ cd node_docker_demo
$ yarn init -y
$ yarn add express
$ touch index.js
Enter fullscreen mode Exit fullscreen mode

In index.js let's paste the following sample code

const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})
Enter fullscreen mode Exit fullscreen mode

Now that we have an express application running .. let's write our Dockerfile!!

Dockerfile Setup

We will start with pull node:latest image from the registry (It doesn't matter the version we pull from registry in our case because it is a simple express app but you might want to stick to a version for backward-compatibility issues or do the node.js and dependencies upgrade accordingly)

FROM node:latest
Enter fullscreen mode Exit fullscreen mode

Let's also set our work directory in the image so that we don't have to mention absolute path everytime

WORKDIR /app
Enter fullscreen mode Exit fullscreen mode

Next up, we will install node_modules in our image and for that we would need package.json and either yarn.lock or package-lock.json file (depending on if you used yarn or npm) in the image

COPY ["package.json", "yarn.lock", "./"]

RUN yarn install
Enter fullscreen mode Exit fullscreen mode

This would copy both package.json and yarn.lock into the current working directory (specified by ./).
Note: our current working directory has been set to /app

Running yarn install after that would install all the required dependencies in node_modules

Now our directory structure inside the image looks something like this

app
 |_ package.json
 |_ yarn.lock
 |_ node_modules
Enter fullscreen mode Exit fullscreen mode

Next let's copy everything else we have in our project with

COPY . .
Enter fullscreen mode Exit fullscreen mode

This will copy everything from our host's current working (.) dir to image's working dir (.)

All there's left to do is run the server with

RUN ["node", "index.js"]
Enter fullscreen mode Exit fullscreen mode

All in all our Dockerfile looks like this

FROM node:latest

# setting work dir
WORKDIR /app

## Following steps are done before copying the remaining file
## to make use of docker's caching capabilities
# copying files required to install node modules
COPY ["package.json", "yarn.lock", "./"]

# install node_modules 
RUN yarn install

# copy everything else
COPY . .

# mention the port which we'll expose with port-mapping 
EXPOSE 3000

# run server
RUN ["node", "index.js"]
Enter fullscreen mode Exit fullscreen mode

Gotcha! There is a small issue here, and that is that we are installing node modules with yarn install before copying every other file but then when we do COPY . . we would be again copying node_modules into the image. To prevent this we will make a .dockerignore file and tell docker to ignore node_modules while copying data inside the image

.dockerignore

node_modules
Enter fullscreen mode Exit fullscreen mode

Let's build this with docker build command and then run it

$ docker build -t myapp .
$ docker run -it --rm -p 3000:300 --name myapp_container myapp
Example app listening at http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

We have now successfully containerized our node.js application but there is one issue that we have:

If we make any change in our codebase, as we do hundreds of thousands of times during development, we would need to rebuild the image and run the container again (hundreds of thousands of times)

That can't be a good strategy. There must be a better way to do this.
Thankfully, there is! VOLUMES! 😍

For the purposes of this use-case we will use bind mounts. Essentially we will bind our host's current working directory to the image's working dir (/app) and attach a file watcher (e.g. nodemon) so that as soon as we save a change in development, that change get's propagated to the image (because volume!), so nodemon would detect that change and reload our node.js server

We can configure bind-mount while running our container

$ docker run -it --rm \
  -p 3000:300 \
  -v $(pwd):/app \
  --name myapp_container \
  myapp
Enter fullscreen mode Exit fullscreen mode

-v $(pwd):/app above would mount the current working dir to /app. Another way to do it is using --mount flag

$ docker run -it --rm \
  -p 3000:3000 \
  --mount type=bind,source=$(pwd),target=/app \
  --name myapp_container 
  myapp
Enter fullscreen mode Exit fullscreen mode

This is fine and dandy, but it's not enough! We also need to configure a file watcher like we discussed. Along with the file watcher another thing to keep in mind is since we are using bind-mounts now, there is no need to actually COPY anything from our local host to image !! So let's remove that and add nodemon into our image and see how things look

FROM node:latest

# setting work dir
WORKDIR /app

# added nodemon globally
RUN npm i -g nodemon

# run the server with watcher
CMD ["nodemon", "index.js"]
Enter fullscreen mode Exit fullscreen mode

That's it!! Let's build this file and run it

$ docker build -t myapp .
$ docker run -it --rm \
  -p 3000:300 \
  -v $(pwd):/app \
  --name myapp_container \
  myapp
Enter fullscreen mode Exit fullscreen mode

Now when we make a code change, the watcher will detect it and restart the node.js server automatically!

And, that is how you can start with developing Node.js applications on docker!

🥳 🥳 🥳

Discussion (0)