Run multi-container applications with Docker Compose
Marco Franssen

Loading...
Marco Franssen

In this article, I want to show you how to run a multi-container application with Docker Compose. Compose lets you define and run multiple Docker containers with a single command. You configure your application's services in a Compose file, then create and start them together.
In a previous article, I showed you how to set up a Docker development environment on Windows or Mac. If you don't have a working Docker environment yet, start with that article.
You might be wondering why you'd ever want to run your application across multiple containers. Do I really need this for my development environment? The short answer is that it's best practice to run one main process per container. The longer answer is:
You want your development environment to be as close to production as possible. You also want to scale the parts of your platform independently. If you need a bigger MySQL cluster, for example, you can spin up more MySQL containers without bundling your application code into them. Smaller containers might look like overkill, but they let you redeploy only the components that have changed. Docker images also help us work toward immutable infrastructure: we build an image once and use that same image in each environment. Testing the images we deploy to production reduces the risk of differences between environments.
If you're not yet comfortable building and running a single container, check out my previous article on running your Angular app in an Nginx container. Before going nuts with multiple containers, make sure you understand how to run one.
As in the previous article, I'll use a package manager to install the tools: Chocolatey for Windows or Homebrew for macOS. Run the following commands in PowerShell or Bash, respectively.
cinst -y docker-composebrew install docker-composeNow that we've installed the tools, we can start using them.
As you should know by now, a Dockerfile defines how to build a Docker image. To coordinate the building and running of multiple containers, we use a docker-compose.yml file. As the extension suggests, this file contains YAML. It defines which images we'll build ourselves and which we'll retrieve from a registry. Here's an example.
version: "2"
services:
web:
build: web/
ports:
- "8080:80"
volumes:
- logvolume01:/var/log/nginx
links:
- api
api:
build: api/
ports:
- "3000:3000"
volumes:
- logvolume01:/var/log
dependens_on:
- cache
links:
- cache
cache:
image: redis
volumes:
logvolume01: {}This configuration builds the web application from our previous blog post. The Angular 2 sample app lives in the web subfolder, with its Dockerfile at the root of that directory. For more detail on the web image, have a look at my previous article. We also build an api image using the Dockerfile in the api directory. The configuration maps host port 3000 to port 3000 in the API container and mounts the shared log volume in both containers. Volumes let us preserve data when a container is removed. The API also depends on a cache container. For Redis, we'll use an existing image from the registry rather than build our own.
To make things a little clearer, here's the API's Dockerfile.
FROM node:6.3.1-onbuild
MAINTAINER marco.franssen@email.nl
EXPOSE 3000As you can see, we're using a Node.js image and exposing port 3000. Now we just need to add a Node.js web server in the same folder. Make sure the npm start command is defined in your package.json. This npm script runs when the container starts, unless you override that behavior in your Dockerfile. See CMD ["npm", "start"] in the Dockerfile we're building on.
Here's one way to organize your project's platform. Assuming you're using Git, I suggest creating one repository for your docker-compose.yml file and using Git submodules for the platform's components. Imagine your platform has a web frontend and several backend services. We can develop each component in its own repository, then use the main repository to version the platform as a whole and coordinate deployments.
Let's look at a possible folder structure.
my-platform
|-- .git
|-- .gitmodules
|-- angular-web
| |-- .git
| |-- .dockerignore
| |-- Dockerfile
| |-- gulpfile.js
| |-- package.json
| `-- src
| |-- html
| | `-- index.html
| |-- css
| | `- style.css
| |-- img
| `-- js
| `-- app.js
|-- microservice-php
| |-- .git
| |-- .dockerignore
| |-- composer.json
| |-- composer.lock
| |-- Dockerfile
| |-- docker-compose.yml
| `-- src
| `-- index.php
|-- microservice-python
| |-- .git
| |-- .dockerignore
| |-- Dockerfile
| |-- docker-compose.yml
| |-- requirements.txt
| `-- runserver.py
|-- microservice-nodejs
| |-- .git
| |-- .dockerignore
| |-- Dockerfile
| |-- docker-compose.yml
| |-- gulpfile.js
| |-- package.json
| `-- server.js
|-- microservice-java
| |-- .git
| |-- .dockerignore
| |-- build.gradle
| |-- Dockerfile
| |-- docker-compose.yml
| |-- gradle.properties
| |-- gradlew
| |-- gradlew.bat
| |-- settings.gradle
| `-- src
`-- docker-compose.ymlAs you can see, each backend service also has a docker-compose.yml file so we can run and test it independently. That file contains the configuration for building the service's image and running its dependencies, such as Redis or MongoDB.
The main docker-compose.yml file, which brings all the projects together, could look like this.
version: "2"
services:
web:
build: angular-web
ports:
- "8080:80"
oauth2:
build: microservice-java
ports:
- "3000:3000"
environment:
KAFKA: http://backbone:9092
MONGO: mongodb://mongodb:27017/oauth2
depends_on:
- cache
- mongodb
- backbone
links:
- cache
- mongodb
- backbone
user-profile:
build: microservice-nodejs
ports:
- "3001:3000"
environment:
KAFKA: http://backbone:9092
MONGO: mongodb://mongodb:27017/users
depends_on:
- cache
- mongodb
- backbone
links:
- cache
- mongodb
- backbone
blogs:
build: microservice-php
ports:
- "3002:3000"
environment:
KAFKA: http://backbone:9092
MONGO: mongodb://mongodb:27017/blogs
depends_on:
- cache
- mysql
- backbone
links:
- cache
- mysql
- backbone
marketing-reports:
build: microservice-python
ports:
- "3003:3000"
environment:
KAFKA: http://backbone:9092
depends_on:
- cache
- graphs
- backbone
links:
- cache
- graphs
- backbone
cache:
image: redis
mongodb:
image: mongodb
mysql:
image: mysql
graphs:
image: neo4j
backbone:
image: kafkaI'll leave the individual backend services' docker-compose.yml files to you. Just remove the parts that aren't needed by that service ;-).
This docker-compose.yml runs our web application on port 8080 and the backend services on ports 3000, 3001, 3002, and 3003. It pulls images for the cache, MongoDB, MySQL, Neo4j, and backbone services from Docker Hub. For our own code, it builds images using the Dockerfiles in the individual projects. The depends_on property controls startup order; it doesn't wait for a dependency to be ready to accept connections. The links provide names the containers can use to reach each other on the internal network, for example in database connection strings. In this example, the services receive their connection settings through environment variables.
To start all the containers defined in your docker-compose.yml file, run the following command. It also streams the logs to your terminal. Press Ctrl+C to stop the containers and return to your shell.
docker-compose upTo stop and remove the containers defined in your docker-compose.yml file, run:
docker-compose downTo view the containers' logs, run the following command. This shows output like the logs you see when running docker-compose up.
docker-compose logsThis is only the tip of the iceberg. Use docker-compose --help to explore more commands and options. I hope this article gives you a good starting point for experimenting with Docker Compose.
Marco Franssen
Explore responsive and adaptive web design, progressive web apps and how I improved my blog with a static site, caching, manifests and service workers.
Marco Franssen
Follow my Raspberry Pi upgrade from Raspbian Jessie to Stretch, including backups, firmware updates, APT sources, Bluetooth audio changes, and cleanup.
Marco Franssen
Package a static Angular app in an Nginx Docker image, build it, and run it with port and log volume mappings in this historical 2016 walkthrough.
Marco Franssen
Set up a Docker development environment on Windows or Mac using Docker Machine and VirtualBox, then configure your shell and run a test container.