DEV Community

Cover image for Use Kool to run Multiple Docker Applications at the same time in your Local Development Environment
Fabrício José Souza for kool.dev

Posted on • Updated on • Originally published at blog.kool.dev

Use Kool to run Multiple Docker Applications at the same time in your Local Development Environment

More and more, we find ourselves working with Docker containers in the context of microservice architectures comprising multiple, loosely-coupled applications and services, and/or projects made up of two or more distinct apps running side-by-side talking to each other via APIs. As developers who work on these types of projects know all too well, setting up your local development environment to run multiple Docker applications at the same time can be a real hassle. Developing each app on its own is a breeze using Kool and Docker Compose. However, when you need to run multiple apps at the same time, configuring the routing and intercommunication between different containers can be challenging.

In this tutorial, we'll show you how to set up your local environment to run more than one Docker application at the same time using Kool and a Caddy web server. No doubt, this is just one of several ways to solve for this use case, but we think it's an approach that works really well for most projects.

Requirements

Before you start, if you haven't done so already, you need to install Docker and Kool CLI. It also wouldn't hurt to quickly get up-to-speed with the Kool CLI commands.

kool is a CLI tool that makes local development with Docker super easy. Kool CLI will level up your development workflow, and help you and your team improve the way you develop and deploy cloud native applications. Make sure you're always using the latest version by running kool self-update.

It always starts off simple

You have a single application with its Docker Compose environment. Let's call it App 1.

$ mkdir -p ~/multiple-apps/app-1
$ cd ~/multiple-apps/app-1
Enter fullscreen mode Exit fullscreen mode

Create the two files we need:

# App 1
# ~/multiple-apps/app-1/docker-compose.yml
services:
  app:
    image: kooldev/php:8.0-nginx
    ports:
      - 80:80
    volumes:
      - .:/app/public
Enter fullscreen mode Exit fullscreen mode
# App 1
# ~/multiple-apps/app-1/index.php
<?php
    echo "Welcome to App 1!\n";
Enter fullscreen mode Exit fullscreen mode

With these two files, you can now get App 1 up and running using kool start, and check the status of its service container using kool status:

$ kool start
Creating network "app-1_default" with the default driver
Creating app-1_app_1 ... done

$ kool status
+---------+---------+---------------------------------------------+---------------+
| SERVICE | RUNNING | PORTS                                       | STATE         |
+---------+---------+---------------------------------------------+---------------+
| app     | Running | 0.0.0.0:80->80/tcp, :::80->80/tcp, 9000/tcp | Up 4 seconds  |
+---------+---------+---------------------------------------------+---------------+
[done] Fetching services status

$ curl localhost
Welcome to App 1!
Enter fullscreen mode Exit fullscreen mode

Awesome! Your app service container is running, kool status shows that port 80 is mapped from your host to the container, and curl localhost successfully returns the output of App 1.

But then it starts getting tricky

As your project evolves over time, you need to add a second application called App 2, which runs alongside App 1. In other words, to work on the project, you need to run both apps at the same time.

Let's quickly set up App 2.

$ mkdir -p ~/multiple-apps/app-2
$ cd ~/multiple-apps/app-2
Enter fullscreen mode Exit fullscreen mode

Once again, let's create the two files we need (inside the app-2 directory):

# App 2
# ~/multiple-apps/app-2/docker-compose.yml
services:
  app:
    image: kooldev/php:8.0-nginx
    ports:
      - 80:80
    volumes:
      - .:/app/public
Enter fullscreen mode Exit fullscreen mode
# App 2
# ~/multiple-apps/app-2/index.php
<?php
    echo "Welcome to App 2!\n";
Enter fullscreen mode Exit fullscreen mode

This time, when you try to get App 2 up and running (using kool start), you run into a problem.

$ kool start
Creating network "app-2_default" with the default driver
Creating app-2_app_1 ...
Creating app-2_app_1 ... error

ERROR: for app-2_app_1  Cannot start service app: driver failed programming external connectivity on endpoint app-2_app_1 (24719704f55491122a18f051d3f1e789b6afc3f34ccf7bfe3d7eac510117ef42):
  Bind for 0.0.0.0:80 failed: port is already allocated

ERROR: for app  Cannot start service app: driver failed programming external connectivity on endpoint app-2_app_1 (24719704f55491122a18f051d3f1e789b6afc3f34ccf7bfe3d7eac510117ef42):
  Bind for 0.0.0.0:80 failed: port is already allocated
ERROR: Encountered errors while bringing up the project.
Enter fullscreen mode Exit fullscreen mode

As per the error message, you have a port conflict (Bind for 0.0.0.0:80 failed: port is already allocated). You cannot have two different containers bound to the same port on your host.

Before you continue, let's stop your App 2 and App 1 containers:

$ cd ~/multiple-apps/app-2 # you should already be here
$ kool stop

$ cd ~/multiple-apps/app-1
$ kool stop
Enter fullscreen mode Exit fullscreen mode

Not so fast!

To fix this error, your first impulse is probably to use different ports for each service. For example, you can run App 1 on localhost:8081 and App 2 on localhost:8082. However, you'll quickly realize this solution isn't viable because it's not flexible enough, and doesn't provide intercommunication between applications over a shared Docker network.

Proxy to the rescue

The proxy design pattern provides a much better solution for running multiple Docker applications at the same time in your local development environment.

Add a global network to Docker Compose

First, you need to improve the Docker Compose environments used by each of your apps. Using one of the many best practices built into the Docker configurations included with Kool Presets, let's create a shared network between Docker containers.

By default, all containers in a docker-compose.yml file will share the same virtual network. This means two different applications will not have a channel of communication. For this reason, Kool Presets will usually have two networks for each container: kool_local and kool_global.

  • kool_local is a local network that's only available to the group of containers from that docker-compose.yml file. It's the same as the default network (if we didn't specify the network ourselves).
  • kool_global is a global network created outside the scope of any particular docker-compose.yml file. It's available system-wide, and any containers running on the host can join it.

Let's create an external kool_global network inside each Docker Compose environment, and add each app to it.

# App 1
# ~/multiple-apps/app-1/docker-compose.yml
services:
  app:
    image: kooldev/php:8.0-nginx
    expose:
      - 80
    volumes:
      - .:/app/public
    networks:
      kool_global:
        aliases:
          - app-1

networks:
  kool_global:
    external: true
Enter fullscreen mode Exit fullscreen mode
# App 2
# ~/multiple-apps/app-2/docker-compose.yml
services:
  app:
    image: kooldev/php:8.0-nginx
    expose:
      - 80
    volumes:
      - .:/app/public
    networks:
      kool_global:
        aliases:
          - app-2

networks:
  kool_global:
    external: true
Enter fullscreen mode Exit fullscreen mode

Notice that we replaced the ports configuration with expose. You don't want these app containers bound to the host anymore, in order to avoid a conflict. Instead, you want to bind a single container to the host, which proxies each request internally over the kool_global network to the correct service container using its network alias.

The aliases key on the container network works like a domain name that resolves to that container's address when used within the same network. This is a great way to normalize names for service containers that need to talk to each other.

Set up the proxy

As mentioned earlier, we're going to use a Caddy web server as our reverse proxy. It's our first choice because it has a simple configuration interface and a rich feature set. Traefik or Nginx would work great too, so feel free to use what you like best when you implement this solution in a real project.

Let's start by creating a Caddyfile configuration file in a new proxy directory:

$ mkdir ~/multiple-apps/proxy
$ cd ~/multiple-apps/proxy
Enter fullscreen mode Exit fullscreen mode
# ~/multiple-apps/proxy/Caddyfile
{
  auto_https off
}

http://a.localhost {
  reverse_proxy / http://app-1
}

http://b.localhost {
  reverse_proxy / http://app-2
}
Enter fullscreen mode Exit fullscreen mode

Notice that we use the app-1 and app-2 container network aliases to point to each destination, based on the incoming Host request (a.localhost vs. b.localhost). You should also add these local domains to your /etc/hosts file: echo "127.0.0.1 a.localhost b.localhost" | sudo tee -a /etc/hosts.

By default, Caddy tries to use HTTPS for all hosts. For this tutorial, we're disabling it. We'll cover local TLS usage in a future article.

Next, let's create a new docker-compose.yml for Caddy itself:

# ~/multiple-apps/proxy/docker-compose.yml
services:
  proxy:
    image: caddy:2-alpine
    ports:
      - 80:80
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
    networks:
      - kool_global

networks:
  kool_global:
    external: true
Enter fullscreen mode Exit fullscreen mode

And that's it. You're done configuring your proxy.

Now, all you need to do is spin up your App 1, App 2 and Proxy service containers (using kool start of course), and verify it works by sending a couple of test requests.

# Proxy
$ cd ~/multiple-apps/proxy # you should already be here
$ kool start
Creating proxy_proxy_1 ... done

# App 1
$ cd ~/multiple-apps/app-1
$ kool start

# App 2
$ cd ~/multiple-apps/app-2
$ kool start

$ curl -H "Host: a.localhost" http://localhost
Welcome to App 1!

$ curl -H "Host: b.localhost" http://localhost
Welcome to App 2!
Enter fullscreen mode Exit fullscreen mode

W00t! You now have both apps up and running at the same time. You can access them using different host names, and they can also communicate with each other.

To clean up your local environment, you'll need to move into each directory (app-1, app-2, and proxy) and run kool stop, and then remove the directories you created: rm -r ~/multiple-apps.

Next steps

Kool's core team is already working on a new set of commands to make the above steps seamless and transparent, so you don't have to worry about the details. We invite you to join the discussion and contribute.

If you like what we're doing, please show your support by starring us on GitHub!

Support the Kool open source project

Kool is open source and totally free to use. If you're interested in learning more about the project, please check out kool.dev. If you have questions, need support, or want to get involved, please join our Slack channel.

Top comments (5)

Collapse
 
andreidascalu profile image
Andrei Dascalu • Edited

I'm not quite sure I understand what Kool brings on top of Docker compose (which is already included within Docker binary).

Instead of specifying Kool global network as external I can specify my own whatever network. As long as it's named, it doesn't matter whether it is external or not, it will just get created by whichever app is up first and the next will join it.

Collapse
 
fabriciojs profile image
Fabrício José Souza

Hello Andrei! - What Kool brings to the table is a take on how dockerized local dev environments should work, trying to solve common usability issues along the way. We understand Docker as a great containers management tool, while we aim to make Kool a great Local environment management tool - leveraging Docker and containers for that, of course, but making the DX (developer experience) better for dev envs.

If you have a few minutes, please take a look at this previous article where we explanined more about the ideas behind Kool.

Now back to this use case here - yes, we could use a named network whithin docker-compose.yml so it doesn't need to be external per say. Kool uses an external network, which is created/managed by Kool CLI. The main advantage of this approach is to seamlessly also share it with possible one-off containers you may wanna run (using kool docker) - as we wanna leave it for kool to handle the networks, so the developer doesn't need to worry at this moment with the nuances of Docker Compose networking, but still achieve the desired/expected behaviour. As mentioned in the article, this is something that comes out of the box with the Presets we ship.

Collapse
 
andreidascalu profile image
Andrei Dascalu

Well, I did, but the previous article seems like the classic marketing/SEO article that provides a lot of unspecific claims. Like "It simplifies the way you use Docker in your local environment by removing the Docker "speed bumps" that slow you down" - the only benefit listed for Kool CLI, without providing examples.
Optimised images was my only meaningful takeaway, in that it's always good to have another choice. So far I've used Bitnami images as they have a long proven expertise in production optimised configs for lots of common software.
Other than that it's a lot of nice and unspecific things with respect to what exactly Kool brings on top of plain compose (beyond a little CLI sugar, though it's not clear whether it really warrants another tool that's a wrapper around a standard tool)

Thread Thread
 
fabriciojs profile image
Fabrício José Souza • Edited

Thank you for your input (and time!) looking at the previous article, @andreidascalu ! This kind of feedback is especially useful for improving how we present in the future.

Your point seems to focus on Kool CLI as a simple "replacement" for Docker/Docker Compose CLI tools - with the recognized CLI sugar. It's important to take a comprehensive look at what the Kool project offers:

  • Custom Docker images, as you mentioned. We aim to optimize for development and production usage.
  • Presets - pre-configured Docker environments for popular frameworks.
  • Scripts - kool.yml for standardization of tasks related to dev environment paired with kool run for running them.
  • Charts for K8S deployment.
  • Kool CLI - is the tool that brings it all together, and allows us to offer more features and the desired better DX through the CLI sugar!
    • kool create - to get a new app environment with one command (from our presets).
    • kool share - to create a live URL for your local environment.
    • Sugar: kool start/kool stop/kool status with more fluent interfaces for what an environment is, not an inert group of containers is.
    • Sugar: kool exec/kool run without the need to worry about docker TTY allocation whether running locally or in a CI environment.

It's important to make clear we don't intend to be a full replacement to docker compose by any means, we just recreate the necessary commands we find useful for a better developer experience. All the rest stays where it belongs and is always reachable - you can always keep using docker compose with an application using Kool Preset configs if you don't like the sugar!

Many developers should be familiar with the usual custom Bash scripts with helpers created for each dockerized project they have worked, and we hope - like us - they are not a big fan of that. Kool's standard UX/DX across projects we hope will be seen as valuable to others, just like it currently is to us! I can't say this enough - Docker is great for managing containers, but all the time new people realize something is missing, a gap for using it effectively for local development. We are in the missing to help with that!

Having said all this - we know we have a long road ahead, with many features to add - then perhaps at some point it could change your view as to kool CLI adoption - i.e more presets and images for different stacks (community actively working on this); kool cloud command for K8S deployment (soon); kool doctor for guaranteeing Docker environment setup is correct (soon).

Collapse
 
luciocalazaes profile image
Lucio Calazaes

Niceeee .. I'm beginner.. however it seems very simple to use.