Setting up a Node.js development environment with docker
December 31, 2022.9 min read
I had a project with a tight deadline, and I was confident of getting to the finish line before my computer crashed. I was working alone on this project, and need to fix my computer or get a new one. I spent a couple of hours installing all the required libraries and dependencies. At a point, I wished I could skip all the setup and get back to coding. It wasn't the best experience setting up Postgres, MongoDB, Node.js, Redis and other libraries a few hours into my project demo.
After this experience, I added docker as a requirement to my development setup. I struggled to get things to with docker work a few times. However, the added advantages of developing with docker outweigh the effort.
Why docker for development ?
Building with docker means you don’t need to install all the libraries and dependencies on your development machine before starting your application.
It makes software development within a team easier because everything runs with docker. A new member joining a project or an existing member setting up the project on a new computer won’t spend much time installing software.
Running an application with docker runs the application with the same version of libraries and other software independent of the operating system, be it Windows, Mac, or Linux.
In this tutorial, I will provide a step by step guide on setting up docker for development environment.
Requirements
- Docker
- Node.js
- PostgreSQL
The development setup should have the following:
- A start script
- A database server
- Server accessible on certain port
- Server restart on file changes
Developing with docker is no different from your normal development setup. However, to integrate docker into your setup you need to understand all the steps required to bring the your project up without docker.
Let’s start by creating a basic Node.js server, once we get that running, then docker will be introduced into the flow.
Create two files, src/app.js for setting up our server and src/db.js for database connection.
mdkir -p -src && touch src/app.js src/db.js
Next, we will initialize npm and install required packages.
npm init -y && npm install express nodemon pg dotenv
Let’s create express server that returns a simple json response when you visit the root path.
// src/app.js import * as dotenv from 'dotenv' dotenv.config() import express from 'express'; const app = express(); app.get('/', async(_req, res) => { res.status(200).json({ status: 'success' }) }) const port = process.env.PORT app.listen(port, () => console.log(`App listening on http://localhost:${port}`))
Next, let’s create .env file for environment variables and define the PORT variable.
# .env PORT=9001
Add a start script to package.json and set the type to module so we can use Node EcmaScript Modules import.
{ ... "scripts": { "start": "nodemon src/app.js", }, "type": "module" }
With this, let’s start the server with npm start
and test it by visiting localhost:9001 and expect to receive a json response as shown below.
Now that the server is up and running, let’s create a function connectToDb for creating a connection to a the database. We have installed node-postgres for connecting to PostgreSQL database.
// src/db.js import pg from 'pg' const { Client } = pg const dbConfig = { user: process.env.DB_USER, password: process.env.DB_PASSWORD, host: process.env.DB_HOST, port: process.env.DB_PORT, database: process.env.DB_NAME } export const connectToDb = async() => { try { const client = new Client(dbConfig) await client.connect(); return client; } catch (error) { console.log("Error: Failed to establish database connection "+ error.message); } }
We will keep this simple and import connectToDb function into app.js. The function returns a client that we can use to run a simple query against the database.
This is how the app.js should look like at this point:
// app.js import express from 'express' import * as dotenv from 'dotenv' dotenv.config() import { connectToDb } from './db.js' const app = express() app.get('/', async (_req, res) => { const dbclient = await connectToDb(); if(!dbclient) return res.status(500).json({ status: 'failed', message: 'Server error' }) const result = await dbclient.query('SELECT $1::text as message', ['Hello world!']) const message = result.rows[0].message res.status(200).json({ status: 'success', message }) }) const port = process.env.PORT; app.listen(port, () => console.log(`Server running on http://localhost:${port}`))
I have created postgres database with database name testdb, database user dbadmin, and database password mypassword. You can use your own database credentials for this step.
The updated .env file should look like so:
# .env PORT=9001 DB_USER=dbadmin DB_PASSWORD=mypassword DB_HOST=localhost DB_PORT=5432 DB_NAME=testdb
Restart the server and visit localhost:9001 . You should get a response like the one shown below if everything works correctly.
Now that the serve is up and running, and the database connection is complete, let’s add docker setup to the flow.
To start, create a Dockerfile on the root, and add the instructions for building the docker image as shown below:
# Dockerfile FROM node:16 WORKDIR /usr/api COPY package.*json /usr/api/ RUN npm install COPY . . EXPOSE 9001 CMD ["npm", "start"]
Let’s build the docker image with docker build
command.
# build the image with this command docker build -t node-api .
We are ready to run the container and test that everything works fine
# after building the image we start the container using this command docker run --name node-api -p 9002:9001 -v `pwd`:/usr/api node-api
This should start the server inside the docker container on port 9001 and should be accessible on port 9002 from the host.
If we try to access the app on http://locahost:9002 we will receive a database connection error. This is expected because our server is running inside a container and wants to connect to a local database instance inside the container which we didn’t setup.
To solve this issue, we have two options.
The first option is to allow connection from the container to the host where the postgres database is running. To do this, we run the container on the host network and the container can talk to the host directly because they are on the same network.
Modify the docker run command by specify the —network as host. One thing to note, is that the port mapping from 9001 on the container to 9002 on the host won’t work anymore as the container is running on the host network. We can access the API on the container directly on port 9001.
docker run --name node-api --network host -p 9002:9001 -v `pwd`:/usr/api node-api
This works, However, it’s taking us away from the goal, which is to make our development environment less dependent on the host machine.
The second option is to run the development database as a container. This is option aligns more with our goal because the less dependent we are on the host machine the better.
We can run the postgres container on the host network so that the node-api container and the database are on the host network and can talk to each other directly.
docker run --network host --name node-api-db \ -e POSTGRES_PASSWORD=mypassword \ -e POSTGRES_USER=dbadmin \ -e POSTGRES_DB=testdb \ -d postgres
However , this failed and the reason is that port 5432 is already occupied by the the postgres instance running on the host.
To fix this we can run the postgres container and the api on the same network different from the host.
Start by create a network with docker network create
command;
docker network create node-api-test-network
Next, remove the existing containers and proceed to run the database container on the new network.
docker rm -f node-api node-api-db
Run the database container on the new network created.
docker run --network node-api-test-network --name node-api-db \ -e POSTGRES_PASSWORD=mypassword \ -e POSTGRES_USER=dbadmin \ -e POSTGRES_DB=testdb \ -d postgres
Now that we have created a network and a database container we are ready to run the api. We need to update the DB_HOST variable to point to the node-api-db container ip. We can get the node-api-db ip address by inspecting the network.
docker network inspect node-api-test-network
Inspecting the network shows the ip of the node-api-db as 172.21.0.2. We can update the host
Instead of updating the DB_HOST with node-api-db ip, we can use the name and docker will resolve the name to the ip automatically because the database and the api are on the same network.
So, here is how the variables should look like:
.env DB_USER=dbadmin DB_PASSWORD=mypassword DB_HOST=node-api-db # 172.21.0.2 DB_PORT=5432 DB_NAME=testdb PORT=9001
Let’s rerun the api to see that everything is working
docker run --name node-api --network node-api-test-network -p 9002:9001 -v `pwd`:/usr/api node-api
When we make a request to localhost:9002 everything should be working fine.
We are done, but this seems daunting with all the commands we have to run to get the application running.
Let’s combine all this commands into one, with the power of docker compose.
Docker Compose
Docker compose is a declarative way of creating and running all the containers, networks, and volumes by describing them in YAML and docker handles takes care of everything.
Let’s turn all we have done so far into a docker-compose
version: "3.9" services: api: build: . ports: - "9002:9001" volumes: - .:/usr/api env_file: - .env depends_on: - db networks: test-network: db: image: "postgres" environment: POSTGRES_PASSWORD: mypassword POSTGRES_USER: dbadmin POSTGRES_DB: testdb networks: test-network: networks: test-network:
Now that we have the configuration setup, we can start the containers by running docker the command below.
docker compose up
Conclusion
At this point, if you decide to run this code on a another machine, postgres or Node.js installation is not required. Open your favourite terminal, type docker compose up
, hit enter, and the server is up.
Developing with docker takes a lot to get used to as a beginner. However, you can adapt the Node.js setup to any runtime. The vital thing to understand is your project setup and the steps to start it in your development machine.