Manage Docker Containers with Docker Compose

Manage Docker Containers with Docker Compose

1 Comment

Last Updated on October 21, 2024 by jt

Docker Compose defines and runs multi-container Docker applications. It lets you create and start all services from a configuration file with a single docker-compose command.

In the previous posts on the Docker series, I discussed how to run Spring Boot on Docker,

In this post, I will discuss how to dockerize multiple containers that depend on each other. You’ll also see how these containers can be managed together with single commands.

Docker Compose Installation

We will start by installing Docker Compose on Ubuntu.

1. Run this command to download the current stable release of Docker Compose:

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

2. Apply executable permissions to the binary:

$ sudo chmod +x /usr/local/bin/docker-compose

3. Test the installation.

$ docker-compose --version

The figure shows the docker-compose version

Docker-Compose versionTo install Docker-Compose on Windows/Mac click here.

Docker Compose

I’ll create two containers:

    1. A simple user-service application that will expose one REST endpoint to manage user information.
    2. The second one is a MongoDB container that will store the information of users.

    The Application

    Add the spring-boot-starter-data-mongodb dependency in your pom.xml.

    The pom.xml file is this.

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.2.6.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <packaging>pom</packaging>
        <modelVersion>4.0.0</modelVersion>
    
        <artifactId>spring-boot-docker-compose</artifactId>
        <groupId>guru.springframework.dockercompose</groupId>
        <version>0.0.1-SNAPSHOT</version>
    
        <modules>
            <module>user-service</module>
        </modules>
    
        <properties>
            <java.version>11</java.version>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <maven.compiler.source>11</maven.compiler.source>
            <maven.compiler.target>11</maven.compiler.target>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-mongodb</artifactId>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <scope>provided</scope>
            </dependency>
    
            <dependency>
                <groupId>javax.xml.bind</groupId>
                <artifactId>jaxb-api</artifactId>
                <version>2.3.1</version>
            </dependency>
            <dependency>
                <groupId>com.sun.xml.bind</groupId>
                <artifactId>jaxb-core</artifactId>
                <version>2.3.0</version>
            </dependency>
            <dependency>
                <groupId>com.sun.xml.bind</groupId>
                <artifactId>jaxb-impl</artifactId>
                <version>2.3.2</version>
            </dependency>
            <dependency>
                <groupId>javax.activation</groupId>
                <artifactId>activation</artifactId>
                <version>1.1.1</version>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>

    The example REST service has a POST method in the controller. The code of the controller is this.

    UserController.java:

    package guru.springframework.dockercompose.controller;
    
    import guru.springframework.dockercompose.domain.User;
    import guru.springframework.dockercompose.exception.UserAlreadyExistsException;
    import guru.springframework.dockercompose.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.*;
    
    /**
    * RestController annotation is used to create
    * Restful web services using Spring MVC
    */
    @RestController
    
    /**
    * RequestMapping annotation maps
    * HTTP requests to handler methods
    */
    @RequestMapping(value = "/api/v1/")
    public class UserController {
      private UserService userService;
      ResponseEntity responseEntity;
    
      @Autowired
      public UserController(UserService userService) {
        this.userService = userService;
      }
    
      /**
      * PostMapping Annotation for mapping HTTP POST requests onto
      * specific handler methods.
      */
      @PostMapping("user")
      public ResponseEntity<?> saveUser(@RequestBody User user) throws UserAlreadyExistsException, Exception {
        User savedUser = userService.saveUser(user);
        responseEntity = new ResponseEntity(savedUser, HttpStatus.CREATED);
        return responseEntity;
      }
    }
    

    The preceding code has one method to save user details. It calls the saveUser() method of UserService.

    The Dockerfile

    The Dockerfile for the user-service is this.

    FROM openjdk:11
    ADD ./target/user-service-0.0.1-SNAPSHOT.jar /usr/src/user-service-0.0.1-SNAPSHOT.jar
    WORKDIR usr/src
    ENTRYPOINT ["java","-jar", "user-service-0.0.1-SNAPSHOT.jar"]

    The docker-compose File

    With Docker Compose, you use a YAML file, docker-compose.yml file to configure your application’s services.

    Create a docker-compose.yml to manage the MongoDB and user-service containers at the root of the project.

    docker-compose.yml

    version: '3'
    services:
      mongo:
        image: mongo:3.6.18-xenial
        container_name: mongo
        restart: always
        ports:
          - 27017:27017
        network_mode: host
        volumes:
          - $HOME/mongo:/data/db
        healthcheck:
          test: "exit 0"
    
      user-service:
        build: user-service/
        image: user-service
        depends_on:
          - mongo
        network_mode: "host"
        hostname: localhost
        restart: always
        ports:
          - 8082:8082
        healthcheck:
          test: "exit 0"

    In the preceding docker-compose.yml file:

    • Line 01 sets the version of the docker-compose syntax. In this example, the version is 3.
    • In order to run Mongo in a Docker container, we need a Mongo image. mongo service is used to create Mongo image. Line 04 uses the mongo:3.6.18-xenial image for mongo.
    • In Line 05, container_name specifies a custom container name.
    • restart: always in Line 06 configures for how to restart containers when they exit.
    • Line – Line 08 maps the host port to the container’s port. Mongo listens to the port 27017 of the Docker network that is different from the network of your computer, also known as the host network. On the other hand, the mongo client will try to connect with the port 27017 of the host network. So to map both the networks, ports is used.
    • Line 09 specifies the host network mode. In this mode, the container shares the host’s networking namespace.
    • volumes in Line 16 – Line 17 maps a directory of our local filesystem to the directory in docker where mongo writes data. It means that our host/local folder $HOME/mongo will be mounted as /data/db inside our running container.
    • healthcheck in Line 12 – Line 13 determines whether or not the containers for this service are health”.
    • In Line 16 build instructs Docker COmpose to build the Spring Boot image from the Dockerfile present in the current folder.
    • Finally depends-on in Line 18 – Line 19 specifies that the user-service container depends on the mongo container. As a result, Docker Compose will start the user-service container after the mongo container starts.

    Passing Environment Variable

    At times, you will need to pass environment variables to containers.

    One use case for Mongo is to create additional users, initialize collections, and indexes at build time.

    The trick is to use the MONGO_INITDB_DATABASE variable that allows you to specify the name of a database to be used for creating scripts in /docker-entrypoint-initdb.d/*.js .

    What this means is, if you supply the image with init scripts, it will run them against the $MONGO_INITDB_DATABASE database.

    The docker-compose code is this.

    version: '3'
    
    services:
        mongodb:
            image: mongo:latest
            container_name: mongodb
            restart: always
            environment:
                MONGO_INITDB_ROOT_USERNAME: <admin-user>
                MONGO_INITDB_ROOT_PASSWORD: <admin-password>
                MONGO_INITDB_DATABASE: <database to create>
            ports:
                - 27017:27017
            volumes:
                - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
    

    The code of mongo-init.js is this

    db.createUser(
            {
                user: "<user for database which shall be created>",
                pwd: "<password of user>",
                roles: [
                    {
                        role: "readWrite",
                        db: "<database to create>"
                    }
                ]
            }
    );
    

    Managing Containers

    Starting Containers

    We’ll start the services using the docker-compose up command.

    $ docker-compose up

    The figure shows the output.

    docker-compose upList the containers and check the status with this command:

    $ docker-compose ps

    This figure shows the currently running Docker containers.

    docker-compose ps

    Beside docker-compose up, there are two other commands that can confuse new developers: start and run.

    docker-compose up builds, (re)creates, starts, and attaches to containers for a service. Unless they are already running, this command also starts any linked services.

    On the other hand, docker-compose start starts the existing containers for a service. Therefore use start if you need to restart containers that were previously created, but were stopped. It never creates new containers.

    The docker-compose run command is for running “one-off” or “adhoc” tasks. You pass the service name you want to run and Docker Compose will only start the containers for services that the running service depends on.

  1. Typically, you will use the run command to run tests or perform an administrative task such as removing or adding data to a data volume container. Also, note that the run command acts like docker run -ti. It opens an interactive terminal to the container and returns an exit status matching the exit status of the process in the container.

    Viewing Logs

    To view the container logs, run this command.

    $ docker-compose logs

    The log output is this.

    docker-compose logs

    Now that the container is running, we’ll try accessing the RESTful service using Postman.
    We’ll access the endpoint on http://localhost:<port>/api/v1/user and insert a user.

    POSTMAN

    Testing the Application

    Next, I’ll verify if the user is saved in MongoDB. I’ll do this by entering the mongo container by attaching a bash to it.

  2. In my case the container name is mongo. So I’ll run this.
    $ docker exec -it mongo bash
    
    

    Mongo container bash

    By running the above command, as you can see the terminal changes to something like root@meghna-Lenovo-ideapad-Y700-15ISK:/# which means we’re inside the mongo docker container.

    After this, run the commands to enter the Mongo client prompt and query the inserted data.

    $ mongo$ 
    $ use user 
    $ db.User.find()

    The output of the query is this.
    Verify User data in mongo container

    Stopping Containers

    You can use the stop or down commands of Docker Compose to stop your containers. Although both the commands stop containers, they are different.

    The docker-compose stop command stops running containers, but it won’t remove them. Whereas, the docker-compose down command stops containers and also removes the stopped containers as well as any networks that were created.

    There is also a docker-compose kill command that you can use to force running containers to stop by sending a SIGKILL signal.

    Summary

    Docker Compose is widely adopted by the industry to run automated test suite as part of Continuous Deployment or Continuous Integration process. Compose offers a convenient way to create and destroy isolated testing environments for test suites in just a few commands, by defining the full environment in a Compose file.

    You will hear lots of comments about Docker Compose not being fit for production. But, this is only partially true.

    If you have a distributed production environment across multiple machines. Also, if your containers should be distributed across those machines dynamically then Docker Compose is not the right tool for you.

    You would need to use some orchestration tools, such as Docker Swarm and Kubernetes to achieve this.

    But you can still have a perfectly fine production environment without dynamically distributing containers across multiple interconnected machines. This is where you should go for Docker Compose. It makes handling container configuration or multiple interconnected containers easier.

    Overall, it’s a cool tool to run multiple isolated services on a single host.

The source code for this post can be found here on GitHub.

About SFG Contributor

Staff writer account for Spring Framework Guru

    You May Also Like

    One comment

    1. November 6, 2020 at 1:14 pm

      Thanks for the post John!

      From what I understand from the docker docs and by simply running your example, I think the ports part in the docker compose file is not necessary when using network_mode: host. Seems it simply exposes the ports to the host network.

      Reply

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    This site uses Akismet to reduce spam. Learn how your comment data is processed.