9 Tips for Local Node.js Development Using Docker Compose
Create an excellent local development environment

Docker Compose offers a great local development setup for designing and developing container solutions. Whether you are a tester, developer, or a DevOps operator, Docker Compose has got you covered.
If you want to create an excellent local development and test environment for Node.js using Docker Compose, I have the following 10 tips.
1. Use the Correct Version in Your Docker Compose File
The docker-compose.yml
file is a YAML file that defines services, networks, and volumes for a Docker application. The first line of the file contains the version
keyword and tells Docker Compose which version of its file format you are using.
There are two major versions that you can use, version 2 and version 3; both have a different use case.
The Docker Compose development team created version 2 for local development and version 3 to be compatible with container orchestrators such as Swarm and Kubernetes.
As we are talking about local Node.js development, I always use the latest version 2 release, at the time of writing, v2.4.
version: "2.4"
services:
web:
2. Use Bind Mounts Correctly
My first tip for your bind mounts is to always mount your Node.js source code from your host using relative paths.
Using relative paths allows other developers to use this Compose file even when they have a different folder structure on their host.volumes:
- ./src:/home/nodeapp/src
Use named volumes to mount your databases
Almost all Node.js applications are deployed to production using a Linux container. If you use a Linux container and develop your application on Windows or a Mac you shouldn’t bind-mount your database files.
In this situation, the database server has to cross the operating system boundaries when reading or writing the database. Instead, you should use a named volume, and let Docker handle the database files.
The volumes:
keyword defines the named volumes of your docker-compose file. Here, we define the named volume workflowdatabase
and use it in the workflowdb
service.
Use delegated configuration for improved performance
I always add the delegated configuration to my volume mounts to improve performance. By using a delegated configuration on your bind mount, you tell Docker that it may delay updates from the container to appear in the host.
Usually, with local development, there is no need for writes performed in a container to be reflected immediately on the host. The delegated flag is an option that is specific to Docker Desktop for Mac.volumes:
- ./src:/home/app/src:delegated
Depending on the level of consistency you need between the container and your host, there are two other options to consider, consistent
and cached
.
3. Correctly Handle Your node_modules
You can’t bind mount the node_modules
directory from your host on macOS or Windows into your container because of the difference in the operating system.
Some npm modules perform dynamic compilation during npm install, and these dynamically compiled modules from macOS won’t run on Linux.
There are two different solutions to solve this:
- Fill the
node_module
directory on the host via the Docker container.
You can fill the node_module
directory on the host via the Docker container by running npm install
via the docker-compose run
command. This installs the correct node_modules using the operation of the container.
For example, a standard Node.js app with the following Dockerfile
and docker-compose.yml
file.:
By executing the command docker-compose run workflowengine npm install
, I install the node_modules on the host via the running Docker container.
This means that the node_modules on the host are now for the architecture and operating system of the Dockerfile and cannot be used from your host anymore.
2. Hide the host’s node_modules using an empty bind mount.
The second solution is more flexible than the first one as you can still run and develop your application from the host as from the Docker container. This is known as the node_modules volume trick.
We have to change the Dockerfile so that the node_modules are installed one directory higher than the Node.js app.
The package.json
is copied and installed in the /node
directory while the application is installed in the /node/app
directory. Node.js applications look for the node_modules
directory up from the current application folder.
The node_modules from the host are in the same folder as the application source code.
To make sure that the node_modules from the host don't bind mount into the Docker image, we mount an empty volume using this docker-compose
file.
The second statement in the volumes section actually hides the node_modules directory from the host.
4. Using Tools With Docker Compose
If you want to run your tools when developing with Docker Compose, you have two options: use docker-compose run
or use docker-compose exe
. Both behave differently.
docker-compose run [service] [command]
starts a new container from the image of the service and runs the command.
docker-compose exec [service] [command]
runs the command in the currently running container of that service.
5. Using nodemon for File Watching
I always use nodemon
for watching file changes and restarting Node.js. When you are developing using Docker Compose, you can use nodemon
by installing nodemon
via the following Compose run command:docker-compose run workflowengine npm install nodemon —-save-dep
Then adding command
below the workflowengine
service in the docker-compose.yml
file. You also have to set the NODE_ENV
to development so that the dev dependencies are installed.
6. Specify the Startup Order of Services
Docker Compose does not use a specific order when starting its services. If your services need a specific startup order, you can specify this using the depends_on
keyword in your docker-compose file.
With depends_on
you can specify that your service A depends on service B. Docker Compose starts service B before service A and makes sure that service B can be reached through DNS before starting service A.
If you are using version 2 of the Docker Compose YAML, depend_on
can be combined with the HEALTHCHECK
command to make sure that the service you depend on is started and healthy.
7. Healthchecks in Combination With depends_on
If you want your service to start after the service you depend on has started and healthy, you have to combine depends on
with health checks.
The example below is from a previous article, mini video encoder part 1 in which I show how to use MongoDB and Fastify.
You have to add condition: service_healthy
to depends_on
to indicate that the service you depend on should be healthy before starting this service.
The health check specified for the MongoDB database makes sure that the database server has started and is accepting connections before reporting healthy.
8. Shrinking Compose Files Using Extension Fields
You can increase the flexibility of your Compose files using environment variables and extension fields. Environment variables can be set using the environment
keyword.
For example, to change the connection string of the database or the port that your API is listening to. See my article Node.js with Docker in production on how to configure and use environment variables in your Node.js application.
Extension fields let you define a block of text in a Compose file that can be reused in that same file. This way, you decrease the size of your Compose file and make it more DRY.
I define a template that includes build
and networks
which is the same on each service by using the syntax <<: *base-service-template
. I inject the defined template into the service definition.
9. Add a Reverse Proxy Service
Once you have multiple services defined in your Compose file that expose an HTTP endpoint, you should start using a reverse proxy. Instead of having to manage all the ports and port mappings for your HTTP endpoints, you can start performing host header routing.
Instead of different ports, you can use DNS names to route between different services. The most common reverse proxies used in container solutions are NGINX, HAProxy, and Traefik.
Using NGINX
If you plan to use NGINX, I suggest the jwilder/nginx-proxy Docker container from Jason Wilder. Nginx-proxy uses docker-gen to generate NGINX configuration templates based on the services in your Compose file.
Every time you add or remove a service from your Compose file, Nginx-proxy regenerates the templates and automatically restarts NGINX. Automatically regenerating and restarting means that you always have an up-to-date reverse proxy configuration that includes all your services.
You can specify the DNS name of your service by adding the VIRTUAL_HOST
environment variable to your service definition.
Nginx-proxy service mounts the Docker socket, this enables it to respond to containers being added or removed. In the VIRTUAL_HOST
environment variable, I use *.localhost
domains.
Chrome automatically points .localhost
domains to 127.0.0.1.
Using Traefik
Traefik is a specialized open-source reverse proxy container image for HTTP and TCP-based applications.
Using Traefik as a reverse proxy inside our Docker Compose is more or less the same as Nginx-proxy. Traefik offers an HTTP-based dashboard to show you the currently active routes handled by Traefik.
Traefik uses labels
instead of environment variables to define your DNS names. See the example above.
Traefik offers a lot more functionality than shown above, if you are interested, I direct you to their website which offers complete documentation on other features such as load balancing, and automatic requesting and renewing of Let’s Encrypt certificates.
Thank you for reading, I hope these nine tips help with Node.js development using Docker Compose. If you have any questions, feel free to leave a response.