In this tutorial, we will discuss the steps on how you can properly run a Docker container for your local development. I will be using an Express server running on NodeJS as an example. We will configure the Docker container to update and restart the server inside the container whenever there is a change in the code.

If you are new to Docker, you may want to read my article "Docker in 1 minute" to quickly get an understanding of Docker.

Docker in 1 minute
Docker, a tool that can package software into containers that run independently in any environment. What is a container, and for what reasons do you need one?

Initialising a sample express application

For this example, we will be using a simple Express server on nodeJS as our application. This will allow us to know whether our Docker setup is configured correctly or not.  

1. Installing packages

First,  we will need to install "Express" and "nodemon". "Express" is a popular nodeJS server application in the javascript community. "nodemon" is used to automatically restart the server whenever there is a change in any files and for this reason, it is commonly used in local development.

npm install express nodemon
Command for installing packages.

2. Configuring the package.json file

{
  "name": "docker-api",
  "version": "1.0.0",
  "author": "Sharooq Salaudeen",
  "type": "module",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js localhost 80 "
  },
  "dependencies": {
    "express": "^4.18.1",
    "nodemon": "^2.0.19"
  }
}
package.json

Open the package.json file and copy-paste the above JSON data.

Keep the type and scripts configuration as shown in the above package.json file for the purposes of this example. You may change the remaining if you know what you are doing.

3. Setting up our express server

import express from "express";

const app = express();
const PORT = 80;

app.use(express.json({ limit: "30mb", extended: true }));
app.use(express.urlencoded({ limit: "30mb", extended: true }));

app.listen(PORT, () => console.log(`server is listening to PORT ${PORT}`));

app.get("/", (req, res) => {
  res.send("Server is running!");
});
server.js

Create a file named server.js and copy the above code in it. We have our simple express server ready to run.

4. Run our Express server

npm run dev
Command for running the server.

If you have done everything correctly so far, you will have a running server with the following output in the console.

console 1

Setting up Docker for local development

Now, we are ready to create a docker container from which we will run our server.

Here are the steps involved: First, we need to create a docker image from a "Dockerfile". Second, we will spin up a docker container from our docker image. Finally, we will create a "docker-compose.yaml" file specifically for our local development purpose. We will go through each step one by one.

1. Creating a Dockerfile

# syntax=docker/dockerfile:1

FROM node:16

WORKDIR /app

COPY ["package.json", "package-lock.json*", "./"]

RUN npm install 
RUN npm install -g nodemon 

COPY . .

CMD [ "node", "server.js" ]
Dockerfile

In your code editor, create a file named Dockerfile without any filename extension  and copy the above code into the new file

A Dockerfile is a set of instructions for creating a Docker image ( A standalone environment which can be replicated easily).

In the above Dockerfile, we are using a node image of version 16. We then defined a new default working directory called "app" for all the subsequent operations to be performed. Next, we copy the "package.json" and "package-lock.json" files to our "app" directory and run the install packages command.

We are installing "nodemon" seperately as a global package using the tag "-g". This is done for the local development purpose, which comes later on when configuring the docker-compose.yaml file.

Next, we copy all the files from our development folder to the docker image. Once all the above tasks are completed, Docker will run the node server.js command to start the server.

2. Create a .dockerignore file

If you have used "git", you may already be familiar with what this does. The  .dockerignore file tells Docker to avoid certain folders or files from being considered when running operations.

node_modules
.dockerignore

Create a file named.dockerignore and paste the above code into it. Here, we are ignoring the "node_modules" folder containing all the installed packages.  This is because we will be installing all the packages inside the Docker image itself.

We could build the contanier right now and we will be having a running server using Docker. But, our goal to enable this container as a local debugging server. So that any time we make a change in the code, the server running inside the container automatically restart to reflect the changes. This is where docker-compose.yaml comes in.

3. Creating a docker-compose.yaml file

Normally in the local development of a NodeJS application, we will be using a package called "nodemon" to automatically restart the server whenever there is a change in the code. The following command will enable the auto-restart on code change

nodemon server.js
nodemon command

You might have noticed that we didn't use "nodemon" to run the server inside the Dockerfile command,  because in most cases Dockerfile is used as a base configuration for running an actual production server. A production server doesn't require the restarting facility like in development as we are not making any changes to the code frequently. This is where thedocker-compose.yaml file comes into play.

services:
  app:
    build: .
    container_name: hello-world-api
    command: nodemon server.js localhost 80
    ports:
      - 3000:80
    volumes:
      - .:/app
docker-compose.yaml

Create a file named docker-compose.yaml and copy the above code in it.

docker-compose.yaml contains a collection of services that can be spun in a single docker-compose build command. In our case, we are only using a single service with the name "app".

The instructions in docker-compose will overwrite the Dockerfile instruction, if the same exists.

In this case, we have written a new "command" nodemon server.js localhost 80 which will replace the "command" node server.js in the Dockerfile. Then, we made a port mapping from port 80 (inside the docker container) to port 5000 (actual port in our system) and also mapped the "/app" directory (inside the docker container) to "." (actual project directory in our system).

Now, let's run the following docker-compose command to build and run our container:

docker-compose up --build
In the above command, the "up" tag enables the syncing of the files and folders between the container and the developemnt directory.

If you have done everything correctly, you will have the following logs in the console.

console.2

If you are using Docker Desktop, you will notice something similar in the "Containers/Apps" section as below.

docker desktop

4. Testing whether the code is synced

If you open the link localhost:3000 in your web browser, you should be geting the message "Server is running!".

server response 1

Let's make a change to this message in our local code and see if the server gets updated inside the Docker. Go to the server.js file and make the following change to the response message.

app.get("/", (req, res) => {
  res.send("Server is running! I have edited this.");
});

Now, if you go to your browser and refresh the page (localhost:3000), you will get the updated response from the server.

server response2 

Voila!! We have our local development server running inside Docker.

Bonus Tip - Setting up Docker for production

Since we have already configured a proper Dockerfile for production where we are running the command node server.js. We can now, straight up use the Docker build command to run a production server.

Run the following Docker command to run a production server:

docker build -t hello-world-api .
The "-t" tag is used to attach a reference name to the Docker container. And the "." in the end represent the build diretory inside the container.

Conclusion

We have built a simple Express server and have set up Docker for local development and debugging with the functionality of "nodemon". We also configured the Dockerfile to run a production server using the docker build command.

If you found this useful, you will find other interesting articles on my blog. See you in the next one.