How to Docker Compose

 by Robin Wieruch
 - Edit this Post

Docker Compose helps you run multi-container applications. In this tutorial, we expand a Docker project, which has only a Dockerfile, with Docker Compose. Before you continue, make sure you have . If you haven't installed the docker-compose dependency, you can do it with Homebrew on MacOS with the following instruction on the command line:

brew install docker-compose

We will use the following Dockerfile as the base. But feel free to use a Dockerfile from another project (e.g. or ). Our Dockerfile should look like this:

FROM node:10
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .

Notice how we have no EXPOSE or CMD statements in this Dockerfile anymore. The Dockerfile is only there to create the application as a Docker image. There are no ports exposed nor any commands to run it. That's where Docker Compose comes into play. First, create a docker-compose.yml file in your project's root directory from the command line:

touch docker-compose.yml

Then copy the following content to it:

version: '3'
services:
dev:
build:
context: .
ports:
- 4680:3000
command: npm start

In a Docker Compose file, you can define so-called services. Our first service will only start the application and expose it to a new port by remapping the origin port. Run the following commands on the command line to build the Docker service with the base image from the Dockerfile and to run the service eventually.

docker-compose build dev
docker-compose up dev

Before we can visit our application in the browser, we need to find out the IP address of our running Docker engine:

docker-machine ip default
-> 192.168.99.100

Finally you should be able to visit http://192.168.99.100:4680. Be aware that your IP address and port may vary. Congratulations, you have shipped your first application in a Docker container with Docker Compose.

However, there is not much different from the previous Docker setup. Instead of relying only on a Dockerfile, we are using Docker Compose to build and run our container. But it's still only one container (here as a service) running. Let's change this by adding another Docker Compose service in our docker-compose.yml file:

version: '3'
services:
dev:
build:
context: .
ports:
- 4680:3000
command: npm start
test:
build:
context: .
environment:
- CI=true
command: npm test

Note: If you haven't setup any test script in your Node.js project yet, or any tests at all, you may want to follow one of these Node.js testing tutorials: or . Otherwise, you may want to use any other script that you have at hand for the second service in the Docker Compose file.

We used the CI=true flag to run all our tests only once, because some test runners (e.g. Jest) would run the tests in watch mode, and thus would never exit the process. Finally, you can start this service as a Docker container. It's only there for testing purposes; which is why it's clearly separated from the dev service which starts your application for development purposes. We are using the --rm flag to remove the container automatically after this service terminates.

docker-compose build test
docker-compose run --rm test

All your tests should run through. In addition, sometimes you want your Docker container to write back to your Docker host. In other words, everything that happens in the Docker container and generated new files should be replicated in your project's source code. That's where volumes come into play:

version: '3'
services:
dev:
build:
context: .
ports:
- 4680:3000
command: npm start
test:
build:
context: .
environment:
- CI=true
command: npm test
volumes:
- './:/usr/src/app'

Now, everything that happens in the Docker container (./usr/src/app) is written to the Docker host (./) and vice versa. The relative path for the Docker container should match the WORKDIR from the Dockerfile. Personally, I had to use this technique to simulate in a Docker container while being able to write the result back into my source code.


Congratulations, you now know how to run two Docker services based on one Dockerfile with Docker Compose. Whereas one service simulates the development environment from within a Docker environment, the other service runs all your tests in a Docker environment. Now you can scale your services horizontally. In the end, your CI could take over and execute the test services, linting services, and other services in parallel.

Keep reading about 

A collection of all the Docker commands I am using on a regular basis for developing applications with Docker. Docker Machine List all Docker engines: Create a Docker engine: Set environment variables…

Just recently I had to install Docker on my MacOS machine. Here I want to give you a brief walkthrough on how to achieve it. First of all, we need Homebrew to install all the necessary Docker…

The Road to React

Learn React by building real world applications. No setup configuration. No tooling. Plain React in 200+ pages of learning material. Learn React like 50.000+ readers.

Get it on Amazon.