DEV Community

Cover image for Configuration of Services
Kostas Kalafatis
Kostas Kalafatis

Posted on

Configuration of Services

In cloud-native applications, we typically use environment variables to manage configuration settings. These variables offer a flexible way to adjust settings without modifying the source code. They are stored on Linux-based systems and can be accessed by applications to control their behavior.

For example, suppose your application relies on a LOG_LEVEL environment variable to determine the level of detail in its logs. If you change this variable from INFO to DEBUG and restart the application, you'll start seeing more detailed logs, which can help you diagnose issues more effectively.

This approach is also handy for deploying the same application across different environments, such as staging, testing, and production. You can simply adjust the environment variables for each environment to meet its specific needs.

Similarly, when using Docker Compose to manage containers, you configure services by setting environment variables for each container. This keeps your configurations consistent and easy to manage across different environments.

There are three methods of defining environment variables in Docker Compose arranged from the most stable, with the least frequent changes, to the most dynamic, with the most frequent changes:

  1. Using the Compose file
  2. Using shell environment variables
  3. Using the environment file

If the environment variables you need for your containers don’t change often, it's a good idea to keep them in your docker-compose.yaml file. For sensitive information like passwords, it’s safer to set these variables directly in your shell environment before running the docker-compose command. However, if you have a lot of variables or if they differ between your testing, staging, and production environments, it's more manageable to put them in .env files and then reference those in your docker-compose.yaml.

In the services section of your docker-compose.yaml file, you can specify environment variables for each service. For instance, you can set environment variables like LOG_LEVEL and METRICS_PORT for the server service like this:

services:
    server:
        image: nginx-custom
        environment:
            - LOG_LEVEL=debug
            - METRICS_PORT=8445
Enter fullscreen mode Exit fullscreen mode

If the environment variables are not defined directly in the docker-compose.yaml file, Docker Compose can still retrieve their values from the shell environment. For example, if you want the HOSTNAME environment variable for the server service to be set from the shell, you can ensure it's available by exporting it in your shell before running Docker Compose. Here's how it might look:

services:
    server:
        image: nginx-custom
        environment:
            - HOSTNAME
Enter fullscreen mode Exit fullscreen mode

And exporting the HOSTNAME variable to the shell:

export HOSTNAME=myhost
docker-compose up
Enter fullscreen mode Exit fullscreen mode

If the shell running the docker-compose command doesn't have a value set for the HOSTNAME environment variable, the container will start with HOSTNAME as an empty environment variable.

You can also store environment variables in .env files and configure them in your docker-compose.yaml files. For example, you might have a database.env file that looks like this:

DB_HOST=localhost
DB_PORT=5432
DB_USER=myuser
DB_PASSWORD=mypassword
Enter fullscreen mode Exit fullscreen mode

In the docker-compose.yaml file, you can specify the .env file for each service using the env_file field. Here's how you would configure it for a service:

services:
    my-service:
        image: my-image
        env_file:
            - ./database.env
Enter fullscreen mode Exit fullscreen mode

When Docker Compose sets up the server service, it will apply all the environment variables specified in the database.env file to the container. In the following example, you'll get hands-on experience configuring an application using all three methods for setting environment variables in Docker Compose.

Configuring Services with Docker Compose

In this example, you'll set up a Docker Compose application using various methods for configuring environment variables. You’ll start by defining two environment variables in a file named print.env. Next, you'll add one environment variable directly in the docker-compose.yaml file and set another one from the Terminal on the fly. By the end, you’ll see how these four environment variables from different sources are combined and used in your container.

Create a folder named configuration-server and navigate into it using the cd command

mkdir configuration-server
cd configuration-server
Enter fullscreen mode Exit fullscreen mode

Create an .env file with the name display.env and the following content, using your favorite code editor. I am going to use VSCode:

VAR_FROM_ENV_FILE_1=HELLO
VAR_FROM_ENV_FILE_2=WORLD
Enter fullscreen mode Exit fullscreen mode

Create a docker-compose.yaml file similar to the following:

services:
    display:
        image: busybox
        command: sh -c 'sleep 5 && env'
        env_file:
            - display.env
        environment:
            - ENV_FROM_COMPOSE_FILE=HELLO
            - ENV_FROM_SHELL
Enter fullscreen mode Exit fullscreen mode

The docker-compose.yaml file defines a print service using the busybox image. It runs a command that sleeps for 5 seconds and then prints the environment variables. Environment variables are configured in three ways: from the print.env file, which contains key-value pairs; directly in the Compose file with ENV_FROM_COMPOSE_FILE=HELLO; and via shell environment variables with ENV_FROM_SHELL, which will be set at runtime if available.


Export the ENV_FROM_SHELL variable to the shell with the following command:

export ENV_FROM_SHELL=WORLD
Enter fullscreen mode Exit fullscreen mode

Use the docker-compose up command to start the app. The output should be similar to the following:

docker-compose up
error during connect: this error may indicate that the docker daemon is not running: Get "http://%2F%2F.%2Fpipe%2Fdocker_engine/v1.24/containers/json?all=1&filters=%7B%22label%22%3A%7B%22com.docker.compose.config-hash%22%3Atrue%2C%22com.docker.compose.project%3Dconfiguration-server%22%3Atrue%7D%7D": open //./pipe/docker_engine: The system cannot find the file specified.
Enter fullscreen mode Exit fullscreen mode

After this moment of embarrassment, you will remember to start the actual Docker daemon, and the output should be the following:

display Pulling
 ec562eabd705 Already exists
 display Pulled
 Network configuration-server_default  Creating
 Network configuration-server_default  Created
 Container configuration-server-display-1  Creating
 Container configuration-server-display-1  Created
Attaching to configuration-server-display-1
configuration-server-display-1  | HOSTNAME=c791b58e8a00
configuration-server-display-1  | SHLVL=1
configuration-server-display-1  | HOME=/root
configuration-server-display-1  | VAR_FROM_ENV_FILE_1=HELLO
configuration-server-display-1  | PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
configuration-server-display-1  | VAR_FROM_ENV_FILE_2=WORLD
configuration-server-display-1  | ENV_FROM_COMPOSE_FILE=HELLO
configuration-server-display-1  | ENV_FROM_SHELL=WORLD
configuration-server-display-1  | PWD=/
configuration-server-display-1 exited with code 0
Enter fullscreen mode Exit fullscreen mode

The output comes from the display container defined in the docker-compose.yaml file, which runs the env command to display its environment variables. The results show two variables from the display.env file: VAR_FROM_ENV_FILE_1 with the value HELLO, and VAR_FROM_ENV_FILE_2 with the value WORLD. Additionally, the ENV_FROM_COMPOSE_FILE variable defined directly in the docker-compose.yaml file has the value HELLO. Finally, the ENV_FROM_SHELL variable, set in the shell before running Docker Compose, appears with the value WORLD.

In this example, you set up a Docker Compose application using various methods for configuration, such as Docker Compose files, environment definition files, and shell-exported values. This flexibility allows you to deploy the same application across different platforms.

Since Docker Compose handles multi-container setups, it's essential to define how these containers depend on each other. The next section will cover how to manage these interdependencies within Docker Compose applications.

Service Dependency

Docker Compose is used to manage multi-container applications, which are defined in docker-compose.yaml files. Even though the containers operate as separate microservices, it's common to have them depend on each other.

For example, in a two-tier application with a PostgreSQL database and a Java backend, the backend needs the PostgreSQL service to be running before it can connect and function correctly. Docker Compose allows you to control the startup and shutdown order of these services, ensuring that dependencies are handled properly.

services:
  init:
    image: busybox
  pre:
    image: busybox
    depends_on:
      - init
  main:
    image: busybox
    depends_on:
      - pre
Enter fullscreen mode Exit fullscreen mode

In the above example, the main container depends on the pre container, and the pre container depends on the init container. Docker Compose will start the containers in the order of init, pre and main. Also, the containers will be stopped in reverse order: main, pre and then init.

In the following example, the order of containers will be used to fill a file and serve it with a web server.

Service Dependency with Docker Compose

In this exercise, you'll configure a Docker Compose application with four containers. The first three containers will run in sequence to generate a static file, which will then be served by the fourth container. This setup demonstrates how to manage and coordinate dependencies between services in Docker Compose.

Create a folder named service-dependency-example and navigate into it using the cd command

mkdir service-dependency-example
cd service-dependency-example
Enter fullscreen mode Exit fullscreen mode

Create your docker-compose.yaml similar to the following

# This is the main configuration file for Docker Compose
# It defines the services that are to be run, and how they should be configured.
# Each service is defined as a key-value pair, where the key is the name of the service,
# and the value is a dictionary of configuration options.
#
# The services section is where you define the services that you want to run.
# Each service is defined as a key-value pair, where the key is the name of the service,
# and the value is a dictionary of configuration options.
#
# The configuration options for a service can include things like the image to use,
# the command to run, the ports to expose, and the volumes to mount.
#
# In this file, we have defined four services:
# - clean: This service simply cleans out the static volume.
# - init: This service initializes the static volume with a simple HTML file.
# - pre: This service simply adds data to the HTML file
# - server: This service runs an Nginx server, which serves the content from the static volume.
#
# The volumes section is where you define the volumes that are to be used by the services.
# In this case, we have defined a single volume called "static", which is mounted by all three services.
services:
  clean:
    image: busybox
    command: rm -rf /data/*
    volumes:
      - static:/data
 
  init:
    image: busybox
    command: "sh -c 'echo Hello from init >> /data/hello.html'"
    volumes:
      - static:/data
    depends_on:
      - clean

  pre:
    image: busybox
    command: "sh -c 'echo Hello from pre >> /data/hello.html'"
    volumes:
      - static:/data
    depends_on:
      - init

  server:
    image: nginx
    volumes:
      - static:/usr/share/nginx/html
    ports:
      - "8080:80"
    depends_on:
      - pre

volumes:
  static:
Enter fullscreen mode Exit fullscreen mode

This Docker Compose file defines four services and a single volume named static. The volume is shared among all services. The clean service starts by removing the index.html file from the volume. The init service then creates and populates index.html. Afterward, the pre service appends an additional line to index.html. Finally, the server service serves the content from the static volume.


Start the application with the docker-compose up command. The output should look like the following:

Container service-dependency-clean-1  Created
 Container service-dependency-init-1  Recreate
 Container service-dependency-init-1  Recreated
 Container service-dependency-pre-1  Recreate
 Container service-dependency-pre-1  Recreated
 Container service-dependency-server-1  Recreate
 Container service-dependency-server-1  Recreated
Attaching to service-dependency-clean-1, service-dependency-init-1, service-dependency-pre-1, service-dependency-server-1
service-dependency-clean-1 exited with code 0
service-dependency-init-1 exited with code 0
service-dependency-pre-1 exited with code 0
service-dependency-server-1  | /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
service-dependency-server-1  | /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
service-dependency-server-1  | /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
service-dependency-server-1  | 10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
service-dependency-server-1  | 10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
service-dependency-server-1  | /docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
service-dependency-server-1  | /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
service-dependency-server-1  | /docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
service-dependency-server-1  | /docker-entrypoint.sh: Configuration complete; ready for start up
service-dependency-server-1  | 2024/08/13 06:53:43 [notice] 1#1: using the "epoll" event method
service-dependency-server-1  | 2024/08/13 06:53:43 [notice] 1#1: nginx/1.27.0
service-dependency-server-1  | 2024/08/13 06:53:43 [notice] 1#1: built by gcc 12.2.0 (Debian 12.2.0-14)
service-dependency-server-1  | 2024/08/13 06:53:43 [notice] 1#1: OS: Linux 5.10.102.1-microsoft-standard-WSL2
service-dependency-server-1  | 2024/08/13 06:53:43 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576
service-dependency-server-1  | 2024/08/13 06:53:43 [notice] 1#1: start worker processes
service-dependency-server-1  | 2024/08/13 06:53:43 [notice] 1#1: start worker process 29
service-dependency-server-1  | 2024/08/13 06:53:43 [notice] 1#1: start worker process 30
service-dependency-server-1  | 2024/08/13 06:53:43 [notice] 1#1: start worker process 31
service-dependency-server-1  | 2024/08/13 06:53:43 [notice] 1#1: start worker process 32
service-dependency-server-1  | 2024/08/13 06:53:43 [notice] 1#1: start worker process 33
service-dependency-server-1  | 2024/08/13 06:53:43 [notice] 1#1: start worker process 34
service-dependency-server-1  | 2024/08/13 06:53:43 [notice] 1#1: start worker process 35
service-dependency-server-1  | 2024/08/13 06:53:43 [notice] 1#1: start worker process 36
service-dependency-server-1  | 2024/08/13 06:53:43 [notice] 1#1: start worker process 37
service-dependency-server-1  | 2024/08/13 06:53:43 [notice] 1#1: start worker process 38
service-dependency-server-1  | 2024/08/13 06:53:43 [notice] 1#1: start worker process 39
service-dependency-server-1  | 2024/08/13 06:53:43 [notice] 1#1: start worker process 40
service-dependency-server-1  | 2024/08/13 06:53:43 [notice] 1#1: start worker process 41
service-dependency-server-1  | 2024/08/13 06:53:43 [notice] 1#1: start worker process 42
service-dependency-server-1  | 2024/08/13 06:53:43 [notice] 1#1: start worker process 43
service-dependency-server-1  | 2024/08/13 06:53:43 [notice] 1#1: start worker process 44
service-dependency-server-1  | 172.21.0.1 - - [13/Aug/2024:06:53:48 +0000] "GET / HTTP/1.1" 200 31 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" "-"
Enter fullscreen mode Exit fullscreen mode

The output shows that Docker Compose creates the containers in the order of clean, init, pre and server.


Open http://localhost:8080 in the browser:

Image description

The output shows that the clean, init and pre containers work in the expected order.


Return to your terminal and hit Ctrl + C to close the application gracefully. You will notice that in the logs, that the containers stop in the reverse order.

Gracefully stopping... (press Ctrl+C again to force)
Aborting on container exit...
 Container service-dependency-server-1  Stopping
 Container service-dependency-server-1  Stopped
 Container service-dependency-pre-1  Stopping
 Container service-dependency-pre-1  Stopped
 Container service-dependency-init-1  Stopping
 Container service-dependency-init-1  Stopped
 Container service-dependency-clean-1  Stopping
 Container service-dependency-clean-1  Stopped
Enter fullscreen mode Exit fullscreen mode

In this example, we set up a Docker Compose application with interdependent services to demonstrate how Docker Compose manages the startup and operation of containers in a specific order. This feature is crucial for building and orchestrating complex multi-container applications.

Summary

In this post, we explored the use of environment variables to manage configurations in cloud-native applications, specifically within Docker Compose. We demonstrated how to configure applications using environment variables defined in Docker Compose files, shell environments, and .env files, allowing for flexible and adaptable deployments across different environments like staging, testing, and production.

We also delved into the management of service dependencies in Docker Compose, showing how to control the order in which containers start and stop. We created a Docker Compose application with four interdependent services that worked together to generate and serve a static file, highlighting the importance of managing dependencies in complex multi-container applications.

And with that we covered how to compose environments using Docker Compose. In the next few posts we are going to delve into Docker Networking.

See you next Monday!

Top comments (2)

Collapse
 
oluwatobi_onasanya_aa0237 profile image
Oluwatobi Onasanya

This was a really good 15 part article on docker, This is just too good.

Collapse
 
kalkwst profile image
Kostas Kalafatis

There are more parts coming my friend!