Running Multiple Spring Boot Services With Docker Compose
In this post, we'll look at how Docker Compose makes it easier to configure and run multiple containers in your local environment.
Join the DZone community and get the full member experience.
Join For Freein this post, we'll look at how docker compose makes it easier to configure and run multiple containers in your local environment.
why docker compose?
first up, you don't need docker compose to run multiple containers. you can do this just fine by manually starting and stopping the containers yourself, as shown previously in this post . however, as the number of containers in your application grows, it becomes more cumbersome to manage each container manually.
docker compose simplifies things by allowing you to configure a multi-container application in a single yaml file. you can start and stop all containers in the application with a single command.
sample app code
i've created a sample app for this post which you can pull from github . it contains the following
-
2 spring boot applications
- bank account service — exposes a rest api for creating and reading bank simple account details
- config service — exposes a rest api with application configuration for the bank account service
- 2 dockerfiles — to define the container images for the above services
- a docker compose file defining the multi-container application
aside from the docker side of things, i won't go into any detail on the boot services. if you want more info you can check out this previous post .
bank account service dockerfile
we'll begin defining a docker image for the bank account service.
# maintainer brian hannaway
from openjdk:8-jre-alpine
workdir /app
# add wait script to the image - script pulled from https://github.com/ufoscout/docker-compose-wait/releases/download/2.7.3/wait /wait
copy /scripts/wait /app/
run chmod +x /app
run apk --no-cache add curl
copy /target/bank-account-service-0.0.1-snapshot.jar /app/
cmd ./wait && java -jar bank-account-service-0.0.1-snapshot.jar
from openjdk:8-jre-alpine
tells docker to use the openjdk base image.
workdir /app
tells docker to create a new working directory in the image called. all further commands will run from this directory.
copy /scripts/wait /app/
tells docker to copy the
wait
script from the
scripts
directory on the host to the
/app
directory in the image. i'll explain the purpose of the
wait
script in detail later.
run chmod +x /app
makes the contents of the
/app
directory executable
copy /target/bank-account-service-0.0.1-snapshot.jar /app/
copies the service jar from the target directory on the host to the
/app
directory in the image
cmd ./wait && java -jar bank-account-service-0.0.1-snapshot.jar
runs the
wait
script, followed by the bank account service. the service won't run until the
wait
script has finished.
config service dockerfile
next, we'll define the config service docker image. its a slightly simpler version of the image we created for the bank account service above. we'll simply create a working directory, copy in the service jar, and run it.
xxxxxxxxxx
from openjdk:8-jre-alpine
maintainer brian hannaway
workdir /app
copy /target/config-server-0.0.1-snapshot.jar /app/
entrypoint ["java", "-jar", "config-server-0.0.1-snapshot.jar"]
defining the docker compose file
now that we've defined dockerfiles for the bank account and config services, the next step to create a docker-compose file that describes how we'll use these images to run containers.
xxxxxxxxxx
version: "3"
services:
config-service:
image: config-service
container_name: config-service
networks:
- micro-service-network
ports:
- 8888:8888
bank-service:
image: bank-service
container_name: bank-service
networks:
- micro-service-network
ports:
- 8080:8080
environment:
wait_hosts: config-service:8888
networks:
micro-service-network:
version: "3"
tells docker that we're using version 3 of the docker-compose file format. at the time of writing version 3 is the latest and recommended version. the docker-compose format version you use will be dictated by the version of docker you're running. i'm running docker version 19.03.12 which means that i should be using version 3. if you want to check what version of docker-compose is compatible with your docker version, check out this
compatibility matrix
.
services definition
the
services
section defines the containers that make up your application. each service definition contains all the configuration required to start a container from an image. the information in each service definition is what you'd typically supply on the command line, running a container manually.
xxxxxxxxxx
config-service:
image: config-service
container_name: config-service
networks:
- micro-service-network
expose:
- "8888"
config service
the
config-service
section defines all the configuration docker needs to run the config-service container
image
tells compose which image to use to run the container.
container_name
is the name given to the container when it starts. if we don't specify a name, compose will derive one based on the name of the compose file and the image name. for example, if i omit the name attribute for the
config-service
and run
docker-compose up
, i can see that the containers derived name is
boot-microservices-docker-compose_config-service_1
.
generally, it's a good idea to give your containers a meaningful name. you'll see later that we need to reference
config-service
from the
bank-service
. we'll do this using the name specified in
container_name
.
networks
defines the networks that the
config-service
container will join when it starts. in this instance it will join
micro-service-network
, which we'll define later.
expose
lists the ports that are exposed on the container. the ports are exposed on either the default network or any network the container is attached to. the ports are not exposed to the host machine. to do this you'll need to use the
ports
attribute and supply the appropriate mappings.
bank service
the
bank-service
definition is very similar to what we've already defined, with the
image
,
container_name
,
networks
and
expose
attributes being similar to those defined for the
config-service
xxxxxxxxxx
image: bank-service
container_name: bank-service
networks:
- micro-service-network
expose:
- "8080"
environment:
wait_hosts: config-service:8888
the
environment
attribute is used to specify a list of environment variables for the container. in the
bank-service
we specify the environment variable
wait_hosts
and give it the value
config-service:8888
. in short, this is required to control container start up order and ensure that the
config-service
is up and running before the
bank-service
starts. i'll explain this in detail later.
networks definition
the
networks
section allows you define a network for your services. for our application we defined a network called
micro-service-network
. when you run
docker-compose up
, each container that starts will be added to the
micro-services-network
and will be visible to every other container in the application. containers can reference one another via their hostname, which is the same as the service name. so in our sample application, the
banks-service
can access the
config-service
as
config-service:8888.
if we don't explicitly define a
network
, docker will create one by default and add all services in the compose file to it.
running the application
running the
docker-compose up
command will
-
create a bridge network called
micro-service-network
-
start a container using the
config-service
image. the container will expose port 8888 onmicro-service-network
and will be accessible to other containers via host nameconfig-service
. -
start a container using the
bank-service
image. the container will expose port 8080 onmicro-service-network
and will be accessible to other containers via host namebank-service
.
it takes approximately 20 seconds for both the
bank-service
and
config-service
to start. if you run
docker container ls
you should see the two containers that have just been created.
service dependencies and startup order
it's common to have dependencies between containers, such that a container a requires container b to be running before container a can start. compose allows you to handle this scenario to a certain degree, by defining the startup order using the
depends_on
attribute. for example, the compose file below defines a
web
service and a
db
service, where
web
is dependent on
db
.
xxxxxxxxxx
version: '3'
services:
web:
image: mywebapp
depends_on:
- db
db:
image: postgres
in the above example, compose will start the containers in dependency order, so
db
will be started before
web
. although
depends_on
sets the order in which containers are started, it does not guarantee that postgres, inside the
db
container is fully operational before the
web
container starts.
we have a similar problem in our sample application because
bank-service
tries to call
config-service
on startup. if
config-service
isn't fully stood up and available to take requests on port 8888,
bank-service
will fail. using the
depends_on
attribute to start the
config-service
first, won't guarantee that the
config-service
is fully operational before
bank-service
calls it.
introducing docker-compose-wait
docker-compose-wait
is a great command line utility that solves the problem described above. when defining the
bank-service
earlier we made docker-compose-wait available to the image by copying the
wait
script into the
app
directory.
xxxxxxxxxx
# add wait script to the image - script pulled from https://github.com/ufoscout/docker-compose-wait/releases/download/2.7.3/wait /wait
copy /scripts/wait /app/
we then told docker to run the
wait
script along with the jar when starting the container.
cmd ./wait && java -jar bank-account-service-0.0.1-snapshot.jar
when we defined
bank-service
in the docker-compose file, we included a
wait_hosts
environment variable that referenced
config-service
on port 8888. when we run
docker-compose up
, the
wait
script pings
config-service
on port 8888. it will not allow
bank-service
container to start until
config-service
is up and running on port 8888.
we can see this in action in the log snippets below. the
wait
script checks if
config-service
is available on port 8888, initially reporting that it isn't available.
eventually,
config-service
bootstraps and is up and running on port 8888. the
wait
script then reports
host config-service:8888 is now available
and the
bank-service
container is started.
wrapping up
in this post, we looked at how docker-compose makes it easy to manage multiple containers in a simple single-node environment. this is particularly useful for development environments and automated test environments. if you want to manage multiple containers in a multi-node environment, then docker swarm is a better bet. we'll look at swarm in another post soon.
Published at DZone with permission of Brian Hannaway, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments