Externalising Spring Boot properties when deploying to Docker

163.9k Views Asked by At

In my Spring Boot app I want to externalise the properties to run in a Docker container. When first deployed, the properties that are currently in my-server/src/main/resources/application.yml are loaded and used by the application as expected. All works fine.

However, my problem is that I need these properties to be updatable as needed, so I need access to the application.yml file once on the Docker container. But at this point, it's not included in the build/docker/ directory before running the buildDocker task, so won't be copied over or accessible after first deployment.

So, what I have tried is to copy the Yaml file into the docker/ build directory, copy it to an accessible directory (/opt/meanwhileinhell/myapp/conf), and use the spring.config.location property to pass a location of the config to the Jar in my Dockerfile:

ENTRYPOINT  ["java",\
...
"-jar", "/app.jar",\
"--spring.config.location=classpath:${configDirectory}"]

Looking at the Command running on the Docker container I can see that this is as expected:

/app.jar --spring.config.location=classpath:/opt/meanwhileinhell/myapp/conf]

However, when I update a property in this file and restart the Docker container, it isn't picking up the changes. File permissions are:

-rw-r--r-- 1 root root  618 Sep  5 13:59 application.yml

The documentation states:

When custom config locations are configured, they are used in addition to the default locations. Custom locations are searched before the default locations.

I can't seem to figure out what I'm doing wrong or misinterpreting, but probably more importantly, is this the correct way to externalise the config for this type of Docker scenario?

7

There are 7 best solutions below

6
On BEST ANSWER

DOCKER IMAGE CONFIGURATION

If you look to the way Spring recommends to launch a Spring Boot powered docker container, that's what you find:

FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

That means your image extends openjdk and your container has its own environment. If you're doing like that, it would be enough to declare what you want to override as environment properties and Spring Boot will fetch them, since environment variables take precedence over the yml files.

Environment variables can be passed in your docker command too, to launch the container with your desired configuration. If you want to set some limit for the JVM memory, see the link below.


DOCKER COMPOSE SAMPLE

Here you have an example of how I launch a simple app environment with docker compose. As you see, I declare the spring.datasource.url property here as an environment variable, so it overrides whatever you've got in your application.yml file.

version: '2'
services:
    myapp:
        image: mycompany/myapp:1.0.0
        container_name: myapp
        depends_on:
        - mysql
        environment:
            - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/myapp?useUnicode=true&characterEncoding=utf8&useSSL=false
        ports:
            - 8080:8080

    mysql:
        image: mysql:5.7.19
        container_name: mysql
        volumes:
            - /home/docker/volumes/myapp/mysql/:/var/lib/mysql/
        environment:
            - MYSQL_USER=root
            - MYSQL_ALLOW_EMPTY_PASSWORD=yes
            - MYSQL_DATABASE=myapp
        command: mysqld --lower_case_table_names=1 --skip-ssl --character_set_server=utf8

See also:

0
On

Your approach is definitely a viable solution, however it is not recommended, since it makes your image not portable between different production and dev environments. Containers should be immutable and all environment configuration should be externalized.

For spring boot, there is very powerful project that allows you to externalize configuration. Its called Spring Cloud Config. The config server allows you to save your environment specific configuration in a git repository and serve the configuration to applications that need it. You basically just save the same application.yml in git and point the config server to the repository location.

Following this approach you can define multiple configuration files for different environments and keep your docker container immutable.

1
On

A variation on Xtreme Biker's answer, this time for deployment of a Spring boot war into a dockerized TomCat

I recommend including a nominal application.yml in your app, but use Docker environment variables to override any individual keys which need environment-specific variation.

The reason I recommend this approach (using Docker environment variables) is:

  • your docker image can use exactly the same artefact as you might use for local development
  • using volume-mounts is painful; you need to find somewhere for them to live on your docker host — which turns that host into a snowflake
  • using docker secrets is painful; image or application layer need to be changed to explicitly lookup secrets from the filesystem

Spring Boot's Externalized Configuration docs explain two ways to supply environment via command-line:

  • UN*X env vars (i.e. SPRING_DATASOURCE_USERNAME=helloworld)
  • Java options (i.e. -Dspring.datasource.username=helloworld)

I prefer Java options, because they express an explicit intent: "this is intended for the following Java process, and only for that Java process".

Finally: I would use TomCat's CATALINA_OPTS as the mechanism for passing those Java options. Documentation from catalina.sh:

(Optional) Java runtime options used when the "start", "run" or "debug" command is executed. Include here and not in JAVA_OPTS all options, that should only be used by Tomcat itself, not by the stop process, the version command etc. Examples are heap size, GC logging, JMX ports etc.

Because CATALINA_OPTS is an easier route than making your Docker image responsible for creating a setenv.sh and passing the appropriate Docker env declarations into it.


Build your .war artefact like so:

./gradlew war

We expect a .war artefact to be output by Gradle to build/libs/api-0.0.1-SNAPSHOT.war.

Use such a Dockerfile:

FROM tomcat:8.5.16-jre8-alpine

EXPOSE 8080

COPY build/libs/api-0.0.1-SNAPSHOT.war /usr/local/tomcat/webapps/v1.war

CMD ["catalina.sh", "run"]

Build your Docker image like so:

docker build . --tag=my-api

Pass CATALINA_OPTS to your container like so:

docker run -it \
-p 8080:8080 \
-e CATALINA_OPTS="\
-Dspring.datasource.url='jdbc:mysql://mydatabase.stackoverflow.com:3306' \
-Dspring.datasource.username=myuser \
" \
my-api

And a docker-compose variant looks like this:

version: '3.2'
services:
  web:
    image: my-api
    ports:
      - "8080:8080"
    environment:
      - >
        CATALINA_OPTS=
        -Dspring.datasource.url='jdbc:mysql://mydatabase.stackoverflow.com:3306'
        -Dspring.datasource.username=myuser
0
On

So I managed to get it working. Rather than passing the classpath to the directory in my DockerFile:

"--spring.config.location=classpath:${configDirectory}"]

I instead tried passing the full location of the file:

 "--spring.config.location=file:${configDirectory}/application.yml"]

This now updates upon restart of the Docker container.

1
On

Personally I'd use Spring Cloud Config Server instead of trying to set up properties files all over the place.

tl;dr it allows you to hold properties in git (which allows version control, branching etc) at a per environment/profile level in a centralised location, which are then served up by REST. Spring Boot has full support for it; in effect it's just another property source that ends up in your Environment.

https://spring.io/guides/gs/centralized-configuration/

1
On

I personally would consider two options:

  1. Using an environment variable per config

    app:
      image: my-app:latest
      ports:
        - "8080:8080"
      environment:
         SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/table
    
  2. Using SPRING_APPLICATION_JSON

    app:
      image: my-app:latest
      ports:
        - "8080:8080"
      environment:
        SPRING_APPLICATION_JSON: '{
          "spring.datasource.url": "jdbc:mysql://db:3306/table",
        }'
    
0
On

Set spring.config.additional-location as ENTRYPOINT (ending with /) on Dockerfile and mount the volume to the host folder where application.yml is

Dockerfile

RUN mkdir /opt/meanwhileinhell/myapp/conf
  (...)
ENTRYPOINT ["java", "-Dspring.config.additional-location=/opt/meanwhileinhell/myapp/conf/", "-jar", "/opt/meanwhileinhell/myapp/app.jar"]

When we want to specify a directory location then we must make sure that the value of spring.config.location ends with / (for example spring.config.location=classpath:/config/) and that the configuration file name is the default. https://springframework.guru/spring-external-configuration-data/

To change on restarts need that application.yml will be located on the host machine, referenced it with a volume

docker-compose.yml

...
volumes:
     - my-server/src/main/resources:/opt/meanwhileinhell/myapp/conf
...

Other alternative is as environment on docker-compose.yml: ... environment: -spring.config.additional-location=/opt/meanwhileinhell/myapp/conf/ volumes: - my-server/src/main/resources:/opt/meanwhileinhell/myapp/conf ...

For Spring < 2.x use spring.config.location instead of spring.config.additional-location