Tips Use Docker Compose for Local Node.js Development

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.

version: '2.4'
services:
  workflowdb:
    image: 'mongo:4.0.14'
    environment:
      - MONGO_INITDB_ROOT_USERNAME=mveroot
      - MONGO_INITDB_ROOT_PASSWORD=2020minivideoencoder!
      - MONGO_INITDB_DATABASE=workflow-db
    volumes:
      - workflowdatabase:/data.db
    ports:
      - '27017:27017'
volumes:
  workflowdatabase:

Mounting a MongoDB database using a named volume

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](https://docs.docker.com/docker-for-mac/osxfs-caching/) 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:

  1. 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.:

FROM node:12-alpine 

WORKDIR /app

COPY . .

CMD [ "node", "index.js"]

Standard Dockerfile for a Node.js app

version: '2.4'

services: 
  workflowengine:
    build: .
    ports: 
      - 8080:8080
    volumes:
      - .:/app

Standard 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.

FROM node:12-alpine
  
WORKDIR /node

COPY package*.json ./

RUN npm install && npm cache clean --force --loglevel=error

WORKDIR /node/app

COPY ./index.js index.js

CMD [ "node", "index.js"]

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.

version: '2.4'

services: 
  workflowengine:
    build: .
    ports: 
      - 8080:8080
    volumes:
      - .:/node/app
      - /node/app/node_modules

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](https://www.npmjs.com/package/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.

version: '2.4'

services: 
  workflowengine:
    build: .
    command: /app/node_modules/.bin/nodemon ./index.js
    ports: 
      - 8080:8080
    volumes:
      - .:/app
    environment:
      - NODE_ENV=development

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.

version: '2.4'
services:
  workflowengine:
    image: 'workflowengine:0.6.0'
    depends_on: 
      workflowdb:
        condition: service_healthy
    environment: 
      - STORAGE_HOST=mongodb://mve-workflowengine:mve-workflowengine-password@workflowdb:27017/workflow-db?authMechanism=DEFAULT&authSource=workflow-db
    ports:
      - '8181:8181'
    networks:
      - mve-network
  workflowdb:
    image: 'mongo:4.0.14'
    healthcheck:
      test: echo 'db.runCommand("ping").ok' | mongo localhost:27017/test --quiet
    environment:
      - MONGO_INITDB_ROOT_USERNAME=mveroot
      - MONGO_INITDB_ROOT_PASSWORD=2020minivideoencoder!
      - MONGO_INITDB_DATABASE=workflow-db
    volumes:
      - ./WorkflowDatabase/init-mongo.js:/docker-entrypoint-initdb.d/init-mongo.js:ro
      - ./WorkflowDatabase/data/workflow-db.db:/data.db
    ports:
      - '27017:27017'
    networks: 
      - mve-network

networks:
    mve-network:

Combining dependson with a health check

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.

version: '2.4'

# template:
x-base: &base-service-template
  build: .
  networks:
    - mve-network
  
services: 
  workflowengine:
    <<: *base-service-template
    build: .
    ports: 
      - 8080:8080
    volumes:
      - .:/node/app
      - /node/app/node_modules

networks:
  mve-network:

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.

version: '2.4'
services:
  nginx-proxy:
    image: jwilder/nginx-proxy
    port:
      - "80:80"
    volumes:
      - /var/run/docker/docker.sock:/tmp/docker.sock

  workflowengine:
    image: 'workflowengine:0.6.0'
    depends_on: 
      workflowdb:
        condition: service_healthy
    environment: 
      - VIRTUAL_HOST=workflowengine.localhost
      - STORAGE_HOST=mongodb://mve-workflowengine:mve-workflowengine-password@workflowdb:27017/workflow-db?authMechanism=DEFAULT&authSource=workflow-db
    ports:
      - '8181:8181'
    networks:
      - mve-network

  workflowencoder:
    image: 'videoencoder:0.6.0'
    depends_on: 
      workflowdb:
        condition: service_healthy
    environment: 
      - VIRTUAL_HOST=videoencoder.localhost  
    ports:
      - '8181:8181'
    networks:
      - mve-network

Using jwilder/nginx-proxy as a reverse proxy for your services

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.

version: '2.4'
services:
  traefik:
    image: traefik:v1.7.20-alpine
    port:
      - "80:80"
    volumes:
      - /var/run/docker/docker.sock:/tmp/docker.sock
    command:
      - --docker
      - --docker.domain=traefik
      - --docker.watch
      - --api
      - --defautlentrypoints=http,https
    labels:
      - traefik.port=8080
      - traefik.frontend.rule=Host:traefik.localhost

  workflowengine:
    image: 'workflowengine:0.6.0'
    depends_on: 
      workflowdb:
        condition: service_healthy
    environment:
      - STORAGE_HOST=mongodb://mve-workflowengine:mve-workflowengine-password@workflowdb:27017/workflow-db?authMechanism=DEFAULT&authSource=workflow-db
    labels:
      - traefik.port=8080
      - traefik.frontend.rule=HOST:workflowengine.localhost
    ports:
      - '8181:8181'
    networks:
      - mve-network

  workflowencoder:
    image: 'videoencoder:0.6.0'
    depends_on: 
      workflowdb:
        condition: service_healthy
    labels:
      - traefik.port=8081
      - traefik.frontend.rule=HOST:videoencoder.localhost
    ports:
      - '8181:8181'
    networks:
      - mve-network

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!

#docker #node-js #javascript #nginx #programming

Tips Use Docker Compose for Local Node.js Development
1 Likes19.90 GEEK