brandLogo

Tech Notes.

By Danish

Docker Compose

Let's first see what issue we face and what docker compose is there to help us with

It is going to be a bit long story but bear with me please, it'll be worth it, I promise.

Consider this scenario of this microservices app. The app architecture consists of two microservices:

Accounts Microservice (accounts-ms):

  • Provides monetary insights.
  • Reads fees.json to fetch class-wise fees.
  • Fetches student details from the Students Microservice.
  • Combines the data to calculate the total fee collected for a class.

Students Microservice (students-ms):

  • Handles CRUD operations for student data.
  • Reads from and writes to students.json.

App structure


Cool, now we know the app

We'll have the docker file the usual way. But let's now try to run the app fully functional.

Our scenarios

Let's consider the scenarios first.

  1. App should store the data in same volume so overall app data remains at one place
  2. Apps should be able to communicate with each other, hence should be in the same network
  3. We need to pass the port numbers for the apps to start on
  4. The accounts-ms needs to know the path of students-ms in order to communticate. Environment variable is the way to go, so we can update the url without a need for redeployment
  5. account-ms depends on students-ms, so each time we start account-ms we have to start students-ms as well

Let's start our app now
Navigate to school-app in ubuntu terminal. Now reflect to our requrements and start executing our commands

  • create the images :
    • docker build -t students-ms:v1 /students-ms
    • docker build -t accounts-ms:v1 /accounts-ms
  • Before creating running the containers, we need to create the volume.
    • docker volume create school-app
  • Now we can start our containers
    • Let's run the students-ms first
      • docker run --name students-ms -p 8181:8181 -v app/school-app/students-ms students-ms:v1
    • Now, account-ms' turn. We'll have to consider all the requirements and then add flags accordingly
      • Since we are running a docker conatiner, this will be the base as ususal docker run
      • we need map container and host's port, -p 8080:8080
      • we need to pass the volume name to which the container will map to -v app/school-app/accounts-ms
      • we need to pass the url of students-ms in environment variable so -e TUDENTS_MS_URL=http://students-ms:8181
      • pass the image repo and tag or id accounts-ms:v1
    • In order for these microservices to communicate, they need to be in the same netork
      • docker network create school-app
      • docker network connect school-app students-ms accounts-ms

With these steps our app should be up and running

Now let's assume we have to bring the services down. Image that now we have new features added in the app, we'll have to get rid of the existing containers and create some new one. We'll have to repeat the entrie process again, except for the volume and network creation part but then commands will be additional

  • Stop the existing containers
    • docker stop students-ms accounts-ms
  • Remove the containers
    • docker rm students-ms accounts-ms
  • Remove the existing images
    • docker rmi students-ms:v1 accounts-ms:v1

Thereafter we'll have to repeat the commands mentioned earlier to bring our app to running state.

Do mind this is our dummy app with just two ms. In a production application, there would be much more stuff, much more environment variables, additional arguments and customiztion

The problem that docker compose is solving

As of now, we are well aware of what is the process of getting the app up and running. We have these many commands just for two dummy services,
but on prodcution we can have 100s of micorservices communicating with each other and each one having n number of environment variables, arguments and etc

Doing these things manually is hectic, cumbersome and very much error-prone.

Here comes docker compose, our saviour 💪🏼

Docker compose provides us a very neat way to write down the specifications of our services and then manage them effectively.

docker compose provides us a way to easily manage our applications. We have to learn the template it provides and write a yml file accordingly. Thereafter, docker compose in one command can perform the following task for us

  • look for existing images, if not found, pull them, if doesn't exist on remote build it locally
  • create volume if not found
  • create network if doesn't exist
  • run our containers with written args and environment variables
  • attach our containers to networks
  • in our case, since accounts-ms relies on students-ms, we must start students-ms while running accounts-ms, docker compose can do that as well

The template for docker compose is this roughly

services:
  service-name:
    image: image-name:tag # optional - our image name and tag
    container_name: container-name # optional our container name
    build:
      context: ./path-to-docker-file-directory
    dockerfile: name-of-docker-file
    ports:
      - "host-port:container-port" # Map host port to container port
    volumes:
      - host-path:container-path # Mount a volume
    environment:
      - ENV_VAR_NAME=value # Set environment variables
    depends_on:
      - another-service-name # Specify dependencies (optional)
    network: 
      - networks-a # the networks our ms should be a part of
      - network-b

  another-service-name:
    build:
      context: ./path-to-dockerfile # Path to Dockerfile
      dockerfile: Dockerfile # Optional: Specify Dockerfile name
    ports:
      - "host-port:container-port"
    networks:
      - my-network # Custom network (optional)

volumes:
  volume-name: # Declare named volumes

networks:
  my-network: # Declare custom networks
    driver: bridge
We now have a basic tempate knowladge

Let's go ahead ad write the docker compose file for our app. Remember the conditions we discussed earlier.

services:
  accounts-ms:
    image: accounts-ms:v1 # repo name and tag, since we are doing it on local else we'll have to attach username as well
    container_name: accounts-ms
    ports:
      - "8080:8080" # maps host machines port 8181 to container's port 8181
    build:
      - context: ./accounts-ms # that's where our app's docker file is
    volumes:
      - school-app-data:/app/data/accounts-ms # we don't want to loose data or data inconsistency in multiple instances of our ms
    networks:
      - school-app # because we need our ms to be able to communicate with each other
    environment:
      - STUDENTS_MS_URL=http://students-ms:8181
    depends_on:
      - students-ms # Accounts-ms implicitly depends on students-ms for student data and results.

  students-ms:
    image: students-ms:v1 # repo name and tag, since we are doing it on local else we'll have to attach username as well
    container_name: students-ms
    ports:
      - "8181:8181" # maps host machines port 8181 to container's port 8181
    build:
      - context: ./students-ms # that's where our app's docker file is
    volumes:
      - school-app-data:/app/data/students-ms # we don't want to loose data or data inconsistency in multiple instances of our ms
    networks:
      - school-app # because we need our ms to be able to communicate with each other

volumes:
  school-app-data: # we have to tell docker about our volume

networks:
  school-app: # we need to tell it the network
    driver: bridge

Checkout the full version of this file here

Once done with this file, we can simply run the command docker compose up and it'll bring up the services and setup networks and volumes as required

docker compose up by default looks for docker-compose.yml file in the directory, but this can be customsied and we can have any name for the file and run it with -f flag like docker compose -f /some_path/abc.yml up

There are few ways we can run the compose command

  • docker compose up it'll bring up all the services mentioned
  • docker compose up <service-name> this will run container for the particular service and the one it depends on
  • docker compose up --build it'll build the image locally prior and then use that to run the containers
  • docker compose up --no-cache it'll build the images and not use any layer or existing cache data
There is much, much more to it, but as full stack developers, we are good. Let devops guys take care of the rest

Explore more at official doc : https://docs.docker.com/compose/