Are you optimizing development efficiency within your organization? Read our whitepaper on developer velocity to learn more!

Blog

Manage networking with Docker Compose

Docker Compose is an essential tool for distributed applications. And with its robust networking features, it’s easier than ever to manage your containers.

Eric Goebelbecker Mar 23

Docker is a powerful tool for distributing, running, and managing applications. But complex applications often need more than one container, and need a way for them to communicate. This ability is essential for distributed applications. Fortunately, Docker Compose makes this information exchange simple with robust and flexible tools for managing networking.

Let’s look at what you can do with Docker Compose networking.

What is Docker Compose?

Docker Compose is a tool for defining and running multi-container Docker applications. It allows you to create an application with multiple Docker containers, networks, and volumes with a simple configuration that can start and stop an application with a single command.

You define your application with a YAML domain-specific language (DSL) that’s an intuitive interface for defining and configuring applications, volumes, and networks. It also has tools to build Docker images as part of application startup.

Here’s a sample file from the Docker Compose network documentation.

services:
  web:
    build: .
    ports:
      - "8000:8000"
  db:
    image: postgres
    ports:
      - "8001:5432"

This file defines two services:

  • web is a container Compose builds from a Dockerfile. It looks for in the working directory that the user runs it from.
  • db is a PostgreSQL data server created from the postgres Docker image.

Since the configuration doesn’t specify a custom network, Compose creates one for the containers to share. Let’s take a close look at that.

How does Docker Compose networking work?

Docker Compose’s default approach to networking is a default network that all the containers in the application share. They join the network and can locate each other by service name. This is one of the features that makes Compose so easy to use, since getting containers to connect to each manually can be a lot of work.

Compose also manages how external programs connect to the containers in your application. Let’s look more closely at the sample Compose file above.

db:
  image: postgres
  ports:
    - "8001:5432"

The db service has a ports entry that tells Compose to allow external access via port 8001 on the host. When an application connects to HOST_PORT 8001 on the host, Docker will route them to CONTAINER_PORT 5432 on the database container.

This HOST_PORT:CONTAINER_PORT syntax is the same as the command line docker syntax.

$ docker run -p 8001:5432 postgres

It’s worth reiterating that Compose makes it possible for the containers to reach each other by name. In the example, web and db are valid host names that resolve to the internal IP address of the two containers. So, the web service can connect to db with a connection string of postgres://db:5432. So, Docker can assign IP addresses at runtime while you configure your applications using names.

Docker Compose networking has access to all of Docker’s networking features. So, you can create additional networks and load custom or standard Docker network drivers.

Let’s use an example to introduce a few advanced concepts.

Docker Compose networking example

Let’s go over a Docker Compose example from Docker’s Awesome Compose repository. You’ll need a host with Docker installed. Recent versions of Docker Desktop come with Compose built-in. I’ll be showing shell sessions from Linux, but everything works the same on macOS and Windows with WSL.

We’ll use the nginx-flask-mysql project. Check out the project from GitHub so you can follow the example.

This app defines two custom networks at the bottom of the file: backnet and frontnet.

networks:
  backnet:  
  frontnet:

All you need to define two custom networks are names. Compose takes care of allocating address space for you.

The application has three services:

The db service is a MariaDB server. The service is configured with:

  • A health check script that pings it every three seconds and restarts it as needed.
  • A volume named db-data that it uses for its relational tables, so its data is persistent across application restarts.
  • One network connection to the backnet network, where it exposes two ports. We’ll cover what expose means below.

This service does not have a connection to frontnet.

services:  
  db:  
    image: mariadb:10-focal  
    restart: always  
    healthcheck:  
      test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 --password="$$(cat /run/secrets/db-password)" --silent']  
      interval: 3s  
      retries: 5  
      start_period: 30s  
    secrets:  
      - db-password  
    volumes:  
      - db-data:/var/lib/mysql  
    networks:  
      - backnet
    environment:  
      - MYSQL_DATABASE=example  
      - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db-password  
    expose:  
      - 3306  
      - 33060

The backend service is a Python app Compose builds on startup from files in the backend subdirectory. It has connections to both networks and is forwarding port 8000 to the host network.

 backend:  
    build:  
      context: backend  
      target: builder  
    restart: always  
    secrets:  
      - db-password  
    ports:  
      - 8000:8000  
    networks:  
      - backnet  
      - frontnet  
    depends_on:  
      db:  
        condition: service_healthy

Proxy is an Nginx container Compose builds on startup from the proxy subdirectory. It’s forwarding port 80. It has an Nginx configuration that forwards HTTP requests on port 80 to port 8000 on the backend service.

 proxy:  
    build: proxy  
    restart: always  
    ports:  
      - 80:80  
    depends_on:   
      - backend  
    networks:  
      - frontnet  

This application uses the two custom networks to isolate the network traffic between the database and web application from the host network and load balancer. But, it could be better. Let’s see why, and how to fix it.

Let’s run this application and see how the networking looks at runtime.

The configuration is ready to run on any host with Docker installed. Move into the awesome-compose/nginx-flask-mysql directory and run docker compose up -d.

~/src/awesome-compose/nginx-flask-mysql$ docker compose up -d
[+] Running 5/5
 ⠿ Network nginx-flask-mysql_backnet      Created                                         0.1s
 ⠿ Network nginx-flask-mysql_frontnet     Created                                         0.1s
 ⠿ Container nginx-flask-mysql-db-1       Healthy                                         4.2s
 ⠿ Container nginx-flask-mysql-backend-1  Star...                                         4.6s
 ⠿ Container nginx-flask-mysql-proxy-1    Starte...                                       5.0s

Docker ps tells us a lot about the networking in this application:

~/src/awesome-compose/nginx-flask-mysql$ docker ps
CONTAINER ID   IMAGE                       COMMAND                  CREATED              STATUS                        PORTS                                       NAMES
36e7f372dc0e   nginx-flask-mysql-proxy     "nginx -g 'daemon of…"   About a minute ago   Up About a minute             0.0.0.0:80->80/tcp, :::80->80/tcp           nginx-flask-mysql-proxy
f729b4ef689d   nginx-flask-mysql-backend   "flask run"              About a minute ago   Up About a minute             0.0.0.0:8000->8000/tcp, :::8000->8000/tcp   nginx-flask-mysql-backend
75a8998e06da   mariadb:10-focal            "docker-entrypoint.s…"   About a minute ago   Up About a minute (healthy)   3306/tcp, 33060/tcp                         nginx-flask-mysql-db

We can drill down a bit further with docker inspect.

This command inspects a container and filters for the network settings.

Pipe the output to jq for pretty printing.

~/src/awesome-compose/nginx-flask-mysql$ docker inspect --format='{{json .NetworkSettings}}' nginx-flask-mysql-backend|jq
{
  "Bridge": "",
  "SandboxID": "81a9820e1345567596c17d037eaa134d8c5261814500a0fccf0dfb420eade98d",
  "HairpinMode": false,
  "LinkLocalIPv6Address": "",
  "LinkLocalIPv6PrefixLen": 0,
  "Ports": {
    "8000/tcp": null
  },
  "SandboxKey": "/var/run/docker/netns/81a9820e1345",
  "SecondaryIPAddresses": null,
  "SecondaryIPv6Addresses": null,
  "EndpointID": "",
  "Gateway": "",
  "GlobalIPv6Address": "",
  "GlobalIPv6PrefixLen": 0,
  "IPAddress": "",
  "IPPrefixLen": 0,
  "IPv6Gateway": "",
  "MacAddress": "",
  "Networks": {
    "nginx-flask-mysql_backnet": {
      "IPAMConfig": null,
      "Links": null,
      "Aliases": [
        "nginx-flask-mysql-backend",
        "backend",
        "26b36187c2ea"
      ],
      "NetworkID": "80bd0d20df08faa1ed4d91f387cae1cf5d6c33484498d9b898949ceca552518c",
      "EndpointID": "7853c7d411188b204298c0e3f3f508951d13723428c98a32fec7f886fbb13863",
      "Gateway": "172.24.0.1",
      "IPAddress": "172.24.0.3",
      "IPPrefixLen": 16,
      "IPv6Gateway": "",
      "GlobalIPv6Address": "",
      "GlobalIPv6PrefixLen": 0,
      "MacAddress": "02:42:ac:18:00:03",
      "DriverOpts": null
    },
    "nginx-flask-mysql_frontnet": {
      "IPAMConfig": null,
      "Links": null,
      "Aliases": [
        "nginx-flask-mysql-backend",
        "backend",
        "26b36187c2ea"
      ],
      "NetworkID": "cc921202ac9b83ef425bc9a28eeca22809d9c45e4215b5a09f932517de004e3c",
      "EndpointID": "4515cceb3f63f35b64760306ce1d4ed257b16da81e72851c7fd85fe48ecf4237",
      "Gateway": "172.25.0.1",
      "IPAddress": "172.25.0.2",
      "IPPrefixLen": 16,
      "IPv6Gateway": "",
      "GlobalIPv6Address": "",
      "GlobalIPv6PrefixLen": 0,
      "MacAddress": "02:42:ac:19:00:02",
      "DriverOpts": null
    }
  }
}

The Networks section shows that backend is connected to two networks.

If you look back to docker ps, you can see that the proxy and backend services are reachable via 0.0.0.0:80 and 0.0.0.0:8000 respectively. Db, however, is not forwarding its two ports.

We can confirm that port 80 is reachable with a web browser:

port80.png

So is port 8000:

port8000.png

That doesn’t make sense. Why bother running a proxy if users can reach the application with a direct connection?

Let’s fix that. Open the compose file and edit the service definition for backend. Change the ports keyword to expose and remove the port mappings.

the ports keyword to expose and remove the port mappings.
 backend:
    build:
      context: backend
      target: builder
    restart: always
    secrets:
      - db-password
    expose:
      - 8000
    networks:
      - backnet
      - frontnet
    depends_on:
      db:
        condition: service_healthy

Run docker compose up -d again.

~/src/awesome-compose/nginx-flask-mysql$ docker compose up -d
[+] Running 3/3
 ⠿ Container nginx-flask-mysql-db-1       Healthy                                           1.8s
 ⠿ Container nginx-flask-mysql-backend-1  Started                                           2.4s
 ⠿ Container nginx-flask-mysql-proxy-1    Started                                           2.1s

Now, docker ps tells a different story.

~/src/awesome-compose/nginx-flask-mysql$ docker ps 
CONTAINER ID   IMAGE                       COMMAND                  CREATED          STATUS                    PORTS                               NAMES
3623f1799e0d   nginx-flask-mysql-proxy     "nginx -g 'daemon of…"   6 minutes ago    Up 6 minutes              0.0.0.0:80->80/tcp, :::80->80/tcp   nginx-flask-mysql-proxy-1
02f065db02d1   nginx-flask-mysql-backend   "flask run"              6 minutes ago    Up 6 minutes              8000/tcp                           nginx-flask-mysql-backend-1
75a8998e06da   mariadb:10-focal            "docker-entrypoint.s…"   16 minutes ago   Up 16 minutes (healthy)   3306/tcp, 33060/tcp                 nginx-flask-mysql-db-1

You can verify this by pointing a browser at 127.0.0.1:8000 again.

In this post, we discussed how Docker Compose networking works, and how it makes it easy to create distributed applications. Then, we used a sample application to demonstrate how port forwarding and multiple networks operate in a Docker Compose environment.

Architect has tools to make building and deploying containerized applications easier than Docker Compose. We even have tools for importing Compose configuration into our continuous deployment environment. Get started today!

Learn more about Docker

If you want to learn more about Docker and containerization in general, check out some of our other outstanding posts:

Also, if you have any questions or comments, leave a comment below or hit us up on Twitter!