Java Socket Programming Docker Example
The connection between Docker containers
Nowadays a microservice architecture is a defacto a software development standard. Most microservices run on multiple containers such as Docker. Docker is a very useful tool while developing software. It allows keeping the development environment on PC as close as possible to a real environment. A very common development pattern is a container with the application written in Java, JavaScript, Python, etc. plus one more container with a database (MySql, OracleDB, MongoDB, etc). In this tutorial, I would like to show you how to configure the connection between Docker containers.
The tutorial is based on a simple Java Spring Boot application hosted on a container with a REST endpoint that returns information about customers and their purchase transactions. The application fetches the data from the MySql database hosted on another container. The complete source code can be found below:
This tutorial is about how to connect applications running in different Docker containers. To make it simple I will focus on 2 containers: one with Java Spring Boot application and another with a MySql DataBase instance. These examples are technology agnostic and the below concepts will work with any apps running on Docker containers.
To connect to a DB instance from a Java application the connection URL has to be specified that follows the format below:
jdbc:mysql://[host][:port]/[database]
The port by default is 3306 and I will keep the default one, the database name is customers and the host is a computer hosting a database. The host part will change depending on the case.
To spin up the container we can use the Docker command line or docker-compose. In my opinion and I think many people will agree with me, the docker-compose way is more convenient because this way does not require typing huge commands. In this tutorial, I will show you both ways.
The command to run MySql container:
$docker container run \
--name=mysql_db \
--restart=always \
-e MYSQL_DATABASE=customers \
-e MYSQL_ROOT_PASSWORD=password \
-p 3306:3306 mysql:8
The command to stop the container:
$docker container stop mysql_db
To run the container using docker-compose we need two things. First, a definition file in a yaml format, and second — execute the below command.
$docker-compose -f <docker-compose.yaml> up
To stop containers defined in the docker-compose.yaml run the command:
$docker-compose -f <docker-compose.yaml> down
We don't have to specify the -f option when the filename is docker-compose.yaml and it is located in the current directory.
The docker-compose file :
version: '3'
services:
db: # this is a service name
container_name: mysql_db # this is a container name
image: mysql:8
restart: always
ports:
- "3306:3306"
environment:
MYSQL_DATABASE: customers
MYSQL_ROOT_PASSWORD: password
After command execution the container is created with a MySql instance, the name of the container is mysql_db and port 3306 on the container is mapped to a port 3306 on the host (PC). This port mapping allows another application running on the PC to have access to this database on host port 3306. The difference between the docker command line and docker-compose is that the docker-compose creates a separate network and attaches a container to this network and the command line way will attach the container to the default Docker bridge network. This difference will be important, but I will explain why a bit later. Of course, it is possible to override this behavior, but to focus on the main point I will live as default.
The setup looks like this:
1. Running App from IDE
When IDE, for example, IntelliJ is running an app it is attached to the host network, the container with DB instance has a port mapped to localhost so in this case, an app can call the DB instance by localhost (127.0.0.1):
jdbc:mysql://localhost:3306/customers
or host IP address
jdbc:mysql://host-ip:3306/customers
To deploy the application using Docker, first an image has to be created. To create an image the below commands have to be executed from a root directory of a project
- Build and package Java application
$mvn clean install
2. Build Docker image with jar file from the previous command. The Dockerfile has to be present in a directory, more about Dockerfile you can find here.
$docker image build -t customer-app .
This will create a Docker image tagged as customer-app:latest, later on, this tag is used to refer to an image in docker commands and docker-compose files.
More about Docker commands you can find here
2. Running App on a Docker container using the Docker host network mode
When using host network mode the app container will share the network namespace with the host. It works similarly to running the application directly from IDE, so here URL will have the same format as before. It is worth mentioning that host network mode works only on Linux. More information you can find here.
The command:
$docker container run --network="host" customer-app:latest
The docker-compose.yaml file :
version: '3.8'
services:
customer-app:
image: customer-app:latest
network_mode: "host"
Note that here we don't have to specify a port mapping.
It is possible to run both containers in the docker network host mode. In this case, the setup looks like this:
We can run the MySql DB container in host mode and run the application from IDE as well. I won't explain this example any further because at this point I think you get an idea.
3. Running App on a Docker container using the Docker network
When containers are connected to the same network, they can "talk" to each other by using the service name or container name as a domain name instead of localhost. This is possible because the Docker network provides service discovery. On the other hand, using localhost to call another container won't work, because every container has its localhost. And let's say Java application within the container try to call localhost it will call localhost of the container and not the localhost of PC.
The service name and container name can be used interchangeably only when docker-compose is used. If you create a container via the command line there is no service name, so you can't call this container by its service name.
If containers are attached to a default Docker bridge network, then service discovery won't work. Remember at the beginning of this tutorial I mention the difference between launching a DB container via command and docker-compose file. This decision was made by the Docker team for the reason to keep backward compatibility. On the default bridge network, a host IP has to be used to call a container from another container (explained at the end of the tutorial) if a container has a port mapping. If a container does not have a port mapping then an IP of a container can be used to connect to the container within a docker network. It is bad practice to connect by container IP because each time a Docker runs a container it may assign a different IP. And this behavior can be overridden by assigning a static IP to the container, but it is too complicated for the tutorial case. To obtain container IP use inspect command.
The connection string can use the service name as a host, but only if a container was launched by docker-compose
jdbc:mysql://db:3306/customers
or container name as a host:
jdbc:mysql://mysql_db:3306/customers
To figure out the network name the container attached to we can execute the command below:
$docker container inspect --format '{{json .NetworkSettings.Networks}}' <container-name/id>
Then pass the name of a network to command as an argument to lunch another container. Note that from now on we have to override the connection string by passing the environment variable because localhost won't work anymore:
$docker container run \
-p 8080:8080 \
-e SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/customers\
--network="docker-compose_default"\
customer-app:latest
More about how to pass configuration to a container you can find here:
When using docker-compose file there can be 2 different situations:
The first situation is when 2 containers are defined in 2 different docker-compose files. Let's say we have a container with a DB instance already running, then in the docker-compose file for the Java app, we have to specify the network name of the DB container, and set this network as external, like in the definition file below:
version: '3.8'
services:
customer-app:
image: customer-app:latest
restart: on-failure
ports:
- "8080:8080"
networks:
- docker-compose_default
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/customersnetworks:
docker-compose_default:
external: true
The second situation is when 2 containers are defined in the same docker-compose file. Then docker-compose will automatically create a network and attach containers to it, like in the definition file below:
version: '3.8'
services:
db: # this is a service name
container_name: mysql_db # this is a container name
image: mysql:8
restart: always
ports:
- "3306:3306"
environment:
MYSQL_DATABASE: customers
MYSQL_ROOT_PASSWORD: password
networks:
- custommer-app-db-network customer-app:
image: customer-app:latest
restart: on-failure
ports:
- "8080:8080"
networks:
- custommer-app-db-network
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/customers
networks:
custommer-app-db-network:
The network definition is not required, in the case where the network is not specified docker-compose will create a default network and attach containers to it.
Just like a side note — different containers have a different start-up time, this can cause an error when one container depends on another. In our case, the Java application is dependent on DB and DB container takes a much longer time to start up. So while running the above file the Java application will throw errors until the DB container will be ready. The property restart: on-failure is set to true to tell Docker to start the container over and over until it runs correctly. Before version 3 of the docker-compose definition file, the property depends_on could solve this problem, but starting from version 3 it is no longer supported. The recommendation is to implement logic in the app to handle this kind of situation or use restart: on-failure.
We still can use a host IP to call another container if this container has port mappings.
When a container doesn't have a port mapping and it resides in a Docker network, then only containers in the same network can access it. In our example, we still have a port mapping, so this container can be accessed from inside of a network using host IP as hostname:
jdbc:mysql://<host-ip>:3306/customers
The docker-compose.yaml
version: '3.8'
services:
customer-app:
image: customer-app:latest
restart: on-failure
ports:
- "8080:8080"
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://<host-ip>:3306/customers
Conclusion
In this tutorial, I've explained with real use examples how the connection between containers works. Thank you for reading! If you have any questions or suggestions, please feel free to write me on my LinkedIn account.
Join FAUN: Website 💻| Podcast 🎙️| Twitter 🐦| Facebook 👥| Instagram 📷|Facebook Group 🗣️| Linkedin Group 💬| Slack 📱| Cloud Native News 📰| More .
If this post was helpful, please click the clap 👏 button below a few times to show your support for the author 👇
Source: https://faun.pub/the-connection-between-docker-containers-579854230ad
0 Response to "Java Socket Programming Docker Example"
Post a Comment