Getting Started with Multi-Container Apps

Up your container game with Docker Compose
Nicholas Bohorquez
Oct 28, 2021


You can find images to use in your app and check important details about them via the Slim Developer Portal.

After setting up your first single container, you’ll likely see anything and everything as a candidate for containerization. That’s usually a good thought, but given that a container is isolated from the outside world by default, how can you structure a solution that will allow you to have several components working together? You could just put everything inside one container, but that would violate the single-responsibility principle, among other ideals.

Another option would be to have each component live in its own container and then fit those containers together like building blocks to form your solution. One of the many advantages of a multi-container, “microservices” approach is that it provides a centralized and simple configuration that controls all of your components in one place and can be started and stopped with a single command. In this article, we will show you how to build a simple solution with a data store, a data API layer, and a client that will have access to the data through the API.

A Sample Multi-Container Application

Let’s start by defining the scope of each component. One of the most common templates for web applications includes a data storage solution. In this case, a relational database like PostgreSQL is a good choice. Mozilla Kinto offers a simple, extensible data model that’s ready to use with PostgreSQL can serve as a backend. Finally, depending on your use case, you can build a frontend application like a CMS, a static site, or a low-code tool like n8n, which lets you use Kinto’s REST API or a host of other services. The following diagram shows the general architecture of the latter:

As you can see, the API client container doesn't have direct access to the data storage container, and all of the communication is done through the data API container, which has access to both the backend and frontend networks.

General Rule: If two containers are on the same network, they can talk to each other. If they aren’t, they can’t. —Docker Documentation

But before we get into the details, you need to understand the basics of networking in multi-container solutions. According to the first rule stated in the Docker documentation, “if two containers are on the same network, they can talk to each other. If they aren’t, they can’t.”

Docker Compose

You can start building multi-container applications directly by creating the resources you need with the Docker CLI. For this example, we’ll use Docker Compose, which is a small Python tool on the Docker engine that provides a super simple, yet powerful way to define resources and services with only a YAML text file. First, you need to install Docker Compose. Windows and MacOS users can install Docker Desktop or follow the alternative installation options; otherwise, you can install it from your terminal using pip or the shell script for the current stable release:

$ sudo curl -L "[https://github.com/docker/compose/releases/download/1.29.2/docker-compose-](https://github.com/docker/compose/releases/download/1.29.2/docker-compose- "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-")$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

Then, create a docker-compose.yml file with the following contents:

version: '3'
networks:
 backend:
   name: backend_NW
 frontend:
   name: frontend_NW
services:
  db:
    image:  postgres
    networks:
    - backend
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres

This short file sets the syntax version for docker-compose, creates two standard networks (backend and frontend), and then defines a container named db which attaches the backend network to the latest PostgreSQL image. Notice that the default port for PostgreSQL (5432) is not exposed to the host, so you will not have direct access to this container. Also, the environment variables POSTGRES_USER and POSTGRES_PASSWORD are fixed, and you should use an environment variable file or a secrets manager to keep them secure. For this example, we’ll keep it simple and launch this configuration using Docker Compose in the command line:

$ docker-compose up -d

It’s not very useful at this point, so you can just verify that everything is ok and then shut the container down as follows:

$ docker-compose down

Data API

To make the data storage useful, let’s connect the data API as a new container in the same docker-compose.yml file and add the following services (extracted from Kinto’s documentation):

  cache:
    networks:
    - backend
    image:  library/memcached
  api:
    image:  kinto/kinto-server
    links:
    - db
    - cache
    ports:
    - "8888:8888"
    networks:
      - backend
      - frontend
    environment:
      KINTO_CACHE_BACKEND: kinto.core.cache.memcached
      KINTO_CACHE_HOSTS: cache:11211 cache:11212
      KINTO_STORAGE_BACKEND: kinto.core.storage.postgresql
      KINTO_STORAGE_URL: postgresql://postgres:postgres@db/postgres
      KINTO_PERMISSION_BACKEND: kinto.core.permission.postgresql
      KINTO_PERMISSION_URL: postgresql://postgres:postgres@db/postgres

The first container is an instance of Memcached that is wired to the backend network. The api container uses the latest Kinto image and links it to the db and cache containers. Notice that the api container is wired to both the backend and frontend networks and that it exposes port 8888 to the host.

Testing Data

To make sure that there is connectivity between the data API and the data storage, we’ll launch the containers again and create a user for Kinto. If you like GUIs, you can use Postman. If you prefer the terminal, you can use curl or any other HTTP client. In this case, we’ll use httpie.

$ echo '{"data": {"password": "s3cr3t"}}' | http PUT http://localhost:8888/v1/accounts/bob -v

In the image above, we created a user named bob with a toy password. The response from the data API shows that the containers can talk to each other:

Consuming the Data

Finally, we will set up a new container wired to the frontend network that uses the latest n8n image and exposes port 5678 for an end-user interface. We’ll stop the containers from the command line with docker-compose down, and then we’ll add the following lines to the docker-compose.yml file:

n8n:
   image: n8nio/n8n
   networks:
     - frontend
   links:
     - api
   ports:
     - "5678:5678"

Once you launch the containers, you will be able to access the n8n web interface though http://localhost:5678 and create a simple workflow. Since the containers that we set up are stateless (we didn’t define any volumes in the docker-compose.yml file), we will have to create Kinto’s again like we did before.

A Simple Data Workflow

N8n is a low-code visual tool with many integrations. For the sake of simplicity, we’ll use the REST client. First, we’ll define a POST to Kinto’s API to insert a record. Then, if it’s successful, we’ll GET all of the stored data:

Both operations take advantage of the docker-compose network configuration. Notice that the host defined is the name of the container (api) with the port exposed. You can configure all important aspects of the network through the docker-compose.yml file, including the use of a custom network adapter or any other special requirements.

Note: You'll have to create Kinto credentials for n8n's Basic Auth to work. Use the username ("bob") and password ("s3cr3t") that you used to create the Kinto above.

You can also create your own client in your local computer, since the data API is exposed to you at port 8888. This way, you can create a complex solution in a simple manner. If you don’t have experience with n8n, you can just import the following JSON file that defines the entire workflow:

{
 "name": "My workflow",
 "nodes": [
   {
     "parameters": {},
     "name": "Start",
     "type": "n8n-nodes-base.start",
     "typeVersion": 1,
     "position": [
       250,
       300
     ]
   },
   {
     "parameters": {
       "authentication": "basicAuth",
       "requestMethod": "POST",
       "url": "=http://api:8888/v1/buckets/default/collections/tasks/records",
       "jsonParameters": true,
       "options": {},
       "bodyParametersJson": "{\"data\": {\"description\": \"Check your containers with Slim.ai\", \"status\": \"todo\"}}"
     },
     "name": "POST new record",
     "type": "n8n-nodes-base.httpRequest",
     "typeVersion": 1,
     "position": [
       450,
       300
     ],
     "credentials": {
       "httpBasicAuth": "KintoAuth"
     }
   },
   {
     "parameters": {
       "authentication": "basicAuth",
       "url": "http://api:8888/v1/buckets/default/collections/tasks/records",
       "options": {}
     },
     "name": "GET all records",
     "type": "n8n-nodes-base.httpRequest",
     "typeVersion": 1,
     "position": [
       650,
       300
     ],
     "credentials": {
       "httpBasicAuth": "KintoAuth"
     }
   }
 ],
 "connections": {
   "Start": {
     "main": [
       [
         {
           "node": "POST new record",
           "type": "main",
           "index": 0
         }
       ]
     ]
   },
   "POST new record": {
     "main": [
       [
         {
           "node": "GET all records",
           "type": "main",
           "index": 0
         }
       ]
     ]
   }
 },
 "active": false,
 "settings": {}
}

Once it's working, you should be able to use n8n to POST data to the Kinto API and GET a response from the API.

Finally, the full docker-compose.yml file should look like this:

version: '3'
networks:
 backend:
   name: backend_NW
 frontend:
   name: frontend_NW
services:
  db:
    image:  postgres
    networks:
    - backend
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
  cache:
    networks:
    - backend
    image:  library/memcached
  api:
    image:  kinto/kinto-server
    links:
    - db
    - cache
    ports:
    - "8888:8888"
    networks:
      - backend
      - frontend
    environment:
      KINTO_CACHE_BACKEND: kinto.core.cache.memcached
      KINTO_CACHE_HOSTS: cache:11211 cache:11212
      KINTO_STORAGE_BACKEND: kinto.core.storage.postgresql
      KINTO_STORAGE_URL: postgresql://postgres:postgres@db/postgres
      KINTO_PERMISSION_BACKEND: kinto.core.permission.postgresql
      KINTO_PERMISSION_URL: postgresql://postgres:postgres@db/postgres
  n8n:
    image:  n8nio/n8n
    networks:
      - frontend
    links:
      - api
    ports:
      - "5678:5678"

Conclusion

Containers offer many advantages that help you create composable and scalable applications, including isolation and easy connectivity. However, you have to use them in a logical and secure way, and you should learn about the common ways of developing cloud-ready applications before you start.

In this article, we taught you how to use Docker Compose to create a simple web application with three layers as an example of a multi-container solution.

As you build multi-container apps, the Slim Developer Platform makes a great starting point for vetting container images. Add frequently used container images to your Favorites for easy access.

About the Author

Nicolas Bohorquez (@nickmancol) is a Data Architect at Merqueo. He has a Master’s Degree in Data Science for Complex Economic Systems and a Major in Software Engineering. Previously, Nicolas has been part of development teams in a handful of startups, and has founded three companies in the Americas. He is passionate about the modeling of complexity and the use of data science to improve the world.

Related Articles

5 Ways Slim Containers Save You Money

Do slim containers really save you money on your cloud bill? Are there cost advantages to smaller containers? Find out here.

Chris Tozzi

Container Insights: Dissecting the World's Most Popular Containers

Join Ayse Kaya in this series, as she creates her 2022 Container Report Chalk Full of Important Security Findings for Developers.

Ayse Kaya

Analytics & Strategy

What We Discovered Analyzing the Top 100 Public Container Images

Complexity abounds in modern development

Ayse Kaya

Analytics & Strategy

2022 Public Container Report

Vulnerabilities continue to increase and developers are struggling to keep up.

Ayse Kaya

Analytics & Strategy

Cloud Development Is Still Too Manual & Complex

Lessons we learned from interviewing more than 30 developers

John Amaral

CEO

Five Things You Should Never Ship to Production in a Container

Here is our take on five things to avoid when creating a container or shipping it to production.

Chris Tozzi

Serverless Applications and Docker

How to Scale the Latest Trend in Infrastructure

Pieter van Noordennen

Growth

The Squeak Interview

CEO John Amaral joins Chris on his livestream

Where Do You Store Your Container Images?

Container Registry Options are Growing in Number and Complexity

Pieter van Noordennen

Growth

Meet DockerSlim's Compose Mode

Optimize a multi-tier app with a single command

Ian Juma

Technical Staff

Why Developers Shouldn't Have to Be Infrastructure Experts, Too

Simplifying processes required to containerize and deploy cloud-native apps.

Chris Tozzi

A New Workflow for Cloud Development

Leverage the benefits of containerization without the headaches & hassle

John Amaral

CEO

Why Don’t We Practice Container Best Practices?

Container best practices are easy to understand, hard to do

John Amaral

CEO

Automatically reduce Docker container size using DockerSlim

REST Web Service example using Python/Flask

John Amaral

CEO

Building Apps Using Cloud Native Buildpacks

Getting started with this innovative technique

Vince Power

Contributor

Comparing Container Versions with DockerSlim and Slim.AI

See differences between your original and slimmed images

Pieter van Noordennen

Growth

Five Proven Ways to Debug a Container

When Things Just Are Not Working

Theofanis Despoudis

Contributor

Reducing Docker Image Size - Slimming vs Compressing

Know the difference

Pieter van Noordennen

Growth

Quick Start Guide

Slim Developer Platform Early Access

What’s in your container?

Why Docker Layers matter for container optimization

Pieter van Noordennen

Growth

Creating a Container Pipeline with GitLab CI

Shipping containers the easy way

Nicolas Bohorquez

Contributor