Spring Boot 3.2.5
The explicitly configured usernames and passwords are not used when using the Docker Compose support:
spring:
datasource:
hikari:
username: example_rw
password: example_rw
liquibase:
user: example_ow
password: example_ow
they should not be overwritten by the one configured in compose.yaml
:
services:
db:
environment:
POSTGRES_USER: sa
POSTGRES_PASSWORD: sa
Logs
$ ./gradlew bootRun
...
liquibase.database : Connected to sa@jdbc:postgresql://127.0.0.1:5432/example?ApplicationName=docker-compose-datasource-test
...
com.zaxxer.hikari.HikariConfig : jdbcUrl.........................jdbc:postgresql://127.0.0.1:5432/example?ApplicationName=docker-compose-datasource-test
...
com.zaxxer.hikari.HikariConfig : schema.........................."example"
...
com.zaxxer.hikari.HikariConfig : username........................"sa"
$ docker compose logs db -f
... POSTGRES_DB from environment is created with 'sa' - correct
db-1 | 2024-05-16 09:25:45.782 UTC [47] LOG: connection received: host=[local]
db-1 | 2024-05-16 09:25:45.783 UTC [47] LOG: connection authorized: user=sa database=postgres application_name=psql
db-1 | 2024-05-16 09:25:45.785 UTC [47] LOG: statement: CREATE DATABASE "example" ;
... init scripts use 'sa' - correct
db-1 | /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/001-create-users-and-database.sh
db-1 | 2024-05-16 09:25:45.832 UTC [50] LOG: connection received: host=[local]
db-1 | 2024-05-16 09:25:45.832 UTC [50] LOG: connection authorized: user=sa database=example application_name=psql
db-1 | 2024-05-16 09:25:45.842 UTC [50] LOG: statement: REVOKE ALL PRIVILEGES ON DATABASE postgres FROM PUBLIC;
... liquibase uses 'sa' - should be 'example_ow'
db-1 | 2024-05-16 09:25:48.670 UTC [65] LOG: connection received: host=192.168.65.1 port=40715
db-1 | 2024-05-16 09:25:48.751 UTC [65] LOG: connection authenticated: identity="sa" method=scram-sha-256 (/var/lib/postgresql/data/pg_hba.conf:128)
db-1 | 2024-05-16 09:25:48.751 UTC [65] LOG: connection authorized: user=sa database=example
...
db-1 | 2024-05-16 09:25:49.509 UTC [65] LOG: execute <unnamed>: CREATE TABLE example.databasechangeloglock (ID INTEGER NOT NULL, LOCKED BOOLEAN NOT NULL, LOCKGRANTED TIMESTAMP WITHOUT TIME ZONE, LOCKEDBY VARCHAR(255), CONSTRAINT databasechangeloglock_pkey PRIMARY KEY (ID))
... hikari uses 'sa' - should be 'example_rw'
db-1 | 2024-05-16 09:26:38.544 UTC [32] LOG: connection received: host=192.168.65.1 port=40792
db-1 | 2024-05-16 09:26:38.559 UTC [32] LOG: connection authenticated: identity="sa" method=scram-sha-256 (/var/lib/postgresql/data/pg_hba.conf:128)
db-1 | 2024-05-16 09:26:38.559 UTC [32] LOG: connection authorized: user=sa database=example
db-1 | 2024-05-16 09:26:38.562 UTC [32] LOG: execute <unnamed>: SET extra_float_digits = 3
db-1 | 2024-05-16 09:26:38.563 UTC [32] LOG: execute <unnamed>: SET application_name = 'docker-compose-datasource-test'
db-1 | 2024-05-16 09:26:38.564 UTC [32] LOG: execute <unnamed>: SET SESSION search_path TO 'example'
Setup
application.yaml
spring:
application:
name: docker-compose-datasource-test
datasource:
hikari:
schema: example
username: example_rw
password: example_rw
liquibase:
default-schema: example
user: example_ow
password: example_ow
logging:
level:
com:
zaxxer:
hikari:
HikariConfig: DEBUG
liquibase:
database: DEBUG
pattern:
console: '%c : %m%n'
compose.yaml
services:
db:
image: postgres:16.3-alpine3.19
restart: always
ports:
- '5432:5432'
command: ["postgres", "-c", "log_statement=all", "-c", "log_connections=true"]
environment:
POSTGRES_USER: sa
POSTGRES_PASSWORD: sa
POSTGRES_DB: example
volumes:
- ./docker/db/init/001-create-users-and-database.sh:/docker-entrypoint-initdb.d/001-create-users-and-database.sh
- ./docker/db/init/002-create-schema.sh:/docker-entrypoint-initdb.d/002-create-schema.sh
labels:
org.springframework.boot.jdbc.parameters: 'ApplicationName=docker-compose-datasource-test'
docker/db/init/001-create-users-and-database.sh
#!/usr/bin/env bash
set -Eeu -o pipefail -o posix
readonly example_admin_pw='example_admin'
readonly example_ow_pw='example_ow'
readonly example_rw_pw='example_rw'
readonly example_ro_pw='example_ro'
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
REVOKE ALL PRIVILEGES ON DATABASE postgres FROM PUBLIC;
GRANT ALL PRIVILEGES ON DATABASE postgres TO $POSTGRES_USER;
CREATE USER example_admin WITH LOGIN REPLICATION PASSWORD '$example_admin_pw';
CREATE USER example_ow WITH LOGIN PASSWORD '$example_ow_pw';
CREATE USER example_rw WITH LOGIN PASSWORD '$example_rw_pw';
CREATE USER example_ro WITH LOGIN PASSWORD '$example_ro_pw';
CREATE DATABASE tmp;
\c tmp
DROP DATABASE IF EXISTS example;
CREATE DATABASE example WITH OWNER example_admin TEMPLATE template0
ENCODING UTF8 LC_COLLATE 'de_DE.UTF8' LC_CTYPE 'de_DE.UTF8';
\c example
DROP DATABASE IF EXISTS tmp;
DROP SCHEMA IF EXISTS public;
REVOKE ALL ON DATABASE example FROM PUBLIC;
GRANT ALL ON DATABASE example TO $POSTGRES_USER;
GRANT ALL ON DATABASE example TO example_admin;
GRANT CONNECT,TEMPORARY ON DATABASE example TO example_ow;
GRANT CONNECT,TEMPORARY ON DATABASE example TO example_rw;
GRANT CONNECT ON DATABASE example TO example_ro;
EOSQL
docker/db/init/002-create-schema.sh
#!/usr/bin/env bash
set -Eeu -o pipefail -o posix
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE SCHEMA IF NOT EXISTS example AUTHORIZATION example_ow;
REVOKE ALL ON SCHEMA example FROM PUBLIC;
GRANT ALL ON SCHEMA example TO $POSTGRES_USER;
GRANT ALL ON SCHEMA example TO example_admin;
GRANT ALL ON SCHEMA example TO example_ow;
ALTER ROLE example_ow IN DATABASE example SET search_path = 'example';
GRANT pg_read_all_data, pg_write_all_data TO example_rw;
ALTER ROLE example_rw IN DATABASE example SET search_path = 'example';
GRANT pg_read_all_data TO example_ro;
ALTER ROLE example_ro IN DATABASE example SET search_path = 'example';
EOSQL
src/main/resources/db/changelog/db.changelog-master.yaml
databaseChangeLog:
- changeSet:
id: INIT-1-1
logicalFilePath: INIT-1
author: sdavids
changes:
- tagDatabase:
tag: INIT-1
I have not verified but I suspect that all spring.datasource.*.username
and spring.datasource.*.password
properties are affected.
Comment From: wilkinsona
When you're using the Docker Compose support, the auto-configured DataSource
is created using the service connection details that comes from any SQL database service. These details are the JDBC URL, the username, and the password. They intentionally take precedence over the JDBC URL, username, and password configured in application.yaml
as those details won't, typically, allow a connection to the Docker Compose-managed database to be established.
Can you please describe what you're trying to achieve here? I think I might be able to reverse engineer it from the scripts and configuration that you have shared, but a description directly from you will be considerably more accurate.
Comment From: sdavids
The setup above in words:
Postgres instance; admin super user 'sa'
Each bounded context gets its own database and super user; database 'example`, super user 'example_admin'.
Each bounded context creates one or more schemas in its database and uses a dedicated admin user for each schema—all created database objects will be owned by the admin user; schema 'example`, admin user 'example_ow'
Two additional users: One with read-only permissions and one with read-write (but not create) permissions; user 'example_ro' and user 'example_rw'.
Each bounded context will be provisioned by Liquibase with the corresponding admin user; admin user 'example_ow'.
The bounded context's application uses the read-write user; user 'example_rw'.
Other stuff (Reporting/QA/etc.) uses the read-only user; user 'example_ro'.
The basic premise is: The development setup should be as close as possible to production.
As it is now, everything is done with the super user 'sa'—therefore no permission checks are performed because 'sa' has all permissions.
Therefore, if you forget to setup permissions correctly in the Liquibase migration scripts the local development setup will work.
Once you deploy to production it might not work because you set the permissions incorrectly (or forgot to set them up altogether), i.e. missing GRANT
statements.
TL;DR
By using the super user one cannot test if the database roles and permissions are set up correctly.
Comment From: sdavids
At first, I tried replicating our setup with Using Testcontainers at Development Time but unfortunately the Testcontainers Postgres support does not allow multiple init scripts running against different databases:
https://github.com/testcontainers/testcontainers-java/issues/8634
Comment From: wilkinsona
Thanks for the additional details. While cumbersome, I think you can achieve what you want with a custom connection details factory:
package com.example;
import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails;
import org.springframework.boot.docker.compose.core.RunningService;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
import org.springframework.boot.docker.compose.service.connection.jdbc.JdbcUrlBuilder;
import org.springframework.core.Ordered;
class CustomUsernameAndPasswordPostgresJdbcDockerComposeConnectionDetailsFactory
extends DockerComposeConnectionDetailsFactory<JdbcConnectionDetails> implements Ordered {
protected CustomUsernameAndPasswordPostgresJdbcDockerComposeConnectionDetailsFactory() {
super("postgres");
}
@Override
protected JdbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
return new PostgresJdbcDockerComposeConnectionDetails(source.getRunningService());
}
static class PostgresJdbcDockerComposeConnectionDetails extends DockerComposeConnectionDetails
implements JdbcConnectionDetails {
private static final JdbcUrlBuilder jdbcUrlBuilder = new JdbcUrlBuilder("postgresql", 5432);
private final String jdbcUrl;
PostgresJdbcDockerComposeConnectionDetails(RunningService service) {
super(service);
this.jdbcUrl = jdbcUrlBuilder.build(service, "example");
}
@Override
public String getUsername() {
return "example_rw";
}
@Override
public String getPassword() {
return "example_rw";
}
@Override
public String getJdbcUrl() {
return this.jdbcUrl;
}
}
@Override
public int getOrder() {
return 0;
}
}
Registered in META-INF/spring.factories
under the org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory
key, this should give you complete control of mapping from the running Docker Compose service to the JDBC connection details. You could do similar for Liquibase by implementing another factory that produces LiquibaseConnectionDetails
.
Note that ConnectionDetailsFactory
implementations don't have access to the Environment
so you'd have to either hardcode the username and password or provide them via some other means for now at least.
Comment From: sdavids
I will try it later today, thanks …
Comment From: sdavids
I tried your suggestion above:
java.lang.IllegalStateException: Duplicate connection details supplied for org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails
at org.springframework.util.Assert.state(Assert.java:97) ~[spring-core-6.1.6.jar:6.1.6]
at org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactories.getConnectionDetails(ConnectionDetailsFactories.java:84) ~[spring-boot-autoconfigure-3.2.5.jar:3.2.5]
at org.springframework.boot.docker.compose.service.connection.DockerComposeServiceConnectionsApplicationListener.registerConnectionDetails(DockerComposeServiceConnectionsApplicationListener.java:68) ~[spring-boot-docker-compose-3.2.5.jar:3.2.5]
https://github.com/spring-projects/spring-boot/blob/95145b23ec31c2b2a797434ef29fae7170b0a5b4/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactories.java#L75-L92
src/main/java/com.example.CustomPostgresJdbcDockerComposeConnectionDetailsFactory
package com.example;
import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails;
import org.springframework.boot.docker.compose.core.RunningService;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
import org.springframework.boot.docker.compose.service.connection.jdbc.JdbcUrlBuilder;
import org.springframework.core.Ordered;
class CustomPostgresJdbcDockerComposeConnectionDetailsFactory
extends DockerComposeConnectionDetailsFactory<JdbcConnectionDetails> implements Ordered {
CustomPostgresJdbcDockerComposeConnectionDetailsFactory() {
super("postgres");
}
@Override
public int getOrder() {
return 0;
}
@Override
protected JdbcConnectionDetails getDockerComposeConnectionDetails(
DockerComposeConnectionSource source) {
return new PostgresJdbcDockerComposeConnectionDetails(source.getRunningService());
}
static class PostgresJdbcDockerComposeConnectionDetails
extends DockerComposeConnectionDetailsFactory.DockerComposeConnectionDetails
implements JdbcConnectionDetails {
private final String jdbcUrl;
PostgresJdbcDockerComposeConnectionDetails(RunningService service) {
super(service);
this.jdbcUrl = new JdbcUrlBuilder("postgresql", 5432).build(service, "example");
}
@Override
public String getUsername() {
return "example_rw";
}
@Override
public String getPassword() {
return "example_rw";
}
@Override
public String getJdbcUrl() {
return this.jdbcUrl;
}
}
}
src/main/resources/META-INF/spring.factories
org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\
com.example.CustomPostgresJdbcDockerComposeConnectionDetailsFactory
The current registration algorithm does not support Ordered
.
Comment From: wilkinsona
Sorry, I'd incorrectly recalled that the first factory would win, hence it implementing Ordered
.
You can get PostgresJdbcDockerComposeConnectionDetailsFactory
to ignore your db
service by hiding the fact that it's Postgres. This will then allow the custom factory to take control. First, add a org.springframework.boot.service-connection
label to the service with a value that's anything other than postgres
, say custom-postgres
. Then update the constructor of CustomPostgresJdbcDockerComposeConnectionDetailsFactory
to pass that value to its super constructor:
protected CustomUsernameAndPasswordPostgresJdbcDockerComposeConnectionDetailsFactory() {
super("custom-postgres");
}
This should ensure that only the custom factory is used for your db
service.
Comment From: sdavids
src/main/java/com/example/CustomDockerComposeConnectionDetailsFactory.java
package com.example;
import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails;
import org.springframework.boot.docker.compose.core.RunningService;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
import org.springframework.boot.docker.compose.service.connection.jdbc.JdbcUrlBuilder;
class CustomDockerComposeConnectionDetailsFactory
extends DockerComposeConnectionDetailsFactory<JdbcConnectionDetails> {
CustomDockerComposeConnectionDetailsFactory() {
super("custom-postgres");
}
@Override
protected JdbcConnectionDetails getDockerComposeConnectionDetails(
DockerComposeConnectionSource source) {
return new CustomDockerComposeConnectionDetails(source.getRunningService());
}
static class CustomDockerComposeConnectionDetails extends DockerComposeConnectionDetails
implements JdbcConnectionDetails {
private final String jdbcUrl;
CustomDockerComposeConnectionDetails(RunningService service) {
super(service);
this.jdbcUrl = new JdbcUrlBuilder("postgresql", 5432).build(service, "example");
}
@Override
public String getUsername() {
return "example_rw";
}
@Override
public String getPassword() {
return "example_rw";
}
@Override
public String getJdbcUrl() {
return jdbcUrl;
}
}
}
src/main/resources/META-INF/spring.factories
org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\
com.example.CustomDockerComposeConnectionDetailsFactory
compose.yaml
services:
db:
...
labels:
org.springframework.boot.service-connection: 'custom-postgres'
The setup above works.
But once Liquibase is in the mix we are back at square one.
src/main/java/com/example/CustomLiquibaseConnectionDetailsFactory.java
package com.example;
import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails;
import org.springframework.boot.autoconfigure.liquibase.LiquibaseConnectionDetails;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory;
class CustomLiquibaseConnectionDetailsFactory
implements ConnectionDetailsFactory<JdbcConnectionDetails, LiquibaseConnectionDetails> {
CustomLiquibaseConnectionDetailsFactory() {}
@Override
public LiquibaseConnectionDetails getConnectionDetails(JdbcConnectionDetails input) {
return new MyLiquibaseConnectionDetails(input);
}
static class MyLiquibaseConnectionDetails implements LiquibaseConnectionDetails {
private final String jdbcUrl;
private final String driverClassName;
public MyLiquibaseConnectionDetails(JdbcConnectionDetails input) {
jdbcUrl = input.getJdbcUrl();
driverClassName = input.getDriverClassName();
}
@Override
public String getUsername() {
return "example_ow";
}
@Override
public String getPassword() {
return "example_ow";
}
@Override
public String getJdbcUrl() {
return jdbcUrl;
}
@Override
public String getDriverClassName() {
return driverClassName;
}
}
}
src/main/resources/META-INF/spring.factories
org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\
com.example.CustomDockerComposeConnectionDetailsFactory,\
com.example.CustomLiquibaseConnectionDetailsFactory
⇓
java.lang.IllegalStateException: Duplicate connection details supplied for org.springframework.boot.autoconfigure.liquibase.LiquibaseConnectionDetails
at org.springframework.util.Assert.state(Assert.java:97) ~[spring-core-6.1.6.jar:6.1.6]
at org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactories.getConnectionDetails(ConnectionDetailsFactories.java:84) ~[spring-boot-autoconfigure-3.2.5.jar:3.2.5]
I also tried:
src/main/java/com/example/CustomLiquibaseConnectionDetailsFactory2.java
package com.example;
import org.springframework.boot.autoconfigure.liquibase.LiquibaseConnectionDetails;
import org.springframework.boot.docker.compose.core.RunningService;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
import org.springframework.boot.docker.compose.service.connection.jdbc.JdbcUrlBuilder;
class CustomLiquibaseConnectionDetailsFactory2
extends DockerComposeConnectionDetailsFactory<LiquibaseConnectionDetails> {
CustomLiquibaseConnectionDetailsFactory2() {
super("custom-postgres");
}
@Override
protected LiquibaseConnectionDetails getDockerComposeConnectionDetails(
DockerComposeConnectionSource source) {
return new CustomLiquibaseConnectionDetails(source.getRunningService());
}
private static class CustomLiquibaseConnectionDetails implements LiquibaseConnectionDetails {
private final String jdbcUrl;
CustomLiquibaseConnectionDetails(RunningService service) {
this.jdbcUrl = new JdbcUrlBuilder("postgresql", 5432).build(service, "example");
}
@Override
public String getUsername() {
return "example_ow";
}
@Override
public String getPassword() {
return "example_ow";
}
@Override
public String getJdbcUrl() {
return jdbcUrl;
}
}
}
src/main/resources/META-INF/spring.factories
org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\
com.example.CustomDockerComposeConnectionDetailsFactory,\
com.example.CustomLiquibaseConnectionDetailsFactory2
src/main/resources/application.yaml
spring:
main:
allow-bean-definition-overriding: true
The application starts.
The result
map contains both custom entries
https://github.com/spring-projects/spring-boot/blob/95145b23ec31c2b2a797434ef29fae7170b0a5b4/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactories.java#L91
result = {LinkedHashMap@4873} size = 2
{Class@4893} "interface org.springframework.boot.autoconfigure.liquibase.LiquibaseConnectionDetails" -> {CustomLiquibaseConnectionDetailsFactory2$CustomLiquibaseConnectionDetails@4906}
{Class@4898} "interface org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails" -> {CustomDockerComposeConnectionDetailsFactory$CustomDockerComposeConnectionDetails@4907}
Unfortunately, the bean injected into the liquibase
bean is not our custom one but the default one:
https://github.com/spring-projects/spring-boot/blob/df578d56160af186627d7b75d2f5fa6293eb8684/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java#L99-L101
connectionDetails = {JdbcAdaptingLiquibaseConnectionDetailsFactory$1@6852}
Implementing Ordered
did not help either.
Comment From: sdavids
Should I author a PR adding Ordered
support?
https://github.com/spring-projects/spring-boot/blob/95145b23ec31c2b2a797434ef29fae7170b0a5b4/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactories.java#L83-L85
Basically if previous != null
then check for Ordered
and use the one with the lowest priority—if tied throw "duplicate" exception
Comment From: wilkinsona
Thanks for the offer. That's definitely room for improvement here but I'm not yet sure what we should do. Ideally, you wouldn't have to mess around for connection details factories at all to do what you want. We'll discuss it as team and try to figure out what we want to do here.
Comment From: wilkinsona
This won't help with the Liquibase side of things, but for Postgres things can be improved slightly by having the custom JdbcConnectionDetails
implement EnvironmentAware
. It can then retrieve properties from the environment for the username and password:
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public String getUsername() {
return this.environment.getProperty("spring.datasource.username");
}
@Override
public String getPassword() {
return this.environment.getProperty("spring.datasource.password");
}
Comment From: wilkinsona
I've experimented a bit in this branch. With no need for a custom connection details factory, it lets you have a compose file like this:
services:
database:
image: postgres:16.3-alpine3.19'
ports:
- '5432'
environment:
- 'POSTGRES_USER=sa'
- 'POSTGRES_PASSWORD=sa'
- 'POSTGRES_DB=mydatabase'
labels:
# $$ is required to disable Docker Compose's own interpolation
# https://docs.docker.com/compose/compose-file/12-interpolation/
- org.springframework.boot.jdbc.username=$${spring.datasource.username}
- org.springframework.boot.jdbc.password=$${spring.datasource.password}
This is very much an experiment and, with this particular approach, I dislike how broad the changes would be to expand support across other service types or even just across the other factories for JdbcConnectionDetails
. Putting that aside for now, I quite like the end result from an external perspective. I haven't yet looked at the Liquibase side of things.
Comment From: sdavids
Just one question about this approach:
Where/how do you specify the desired spring profile?
One could have fine-grained profiles containing only the credentials and then run the application with several profiles.
So instead of -Dspring.profiles.active=dev
you might have -Dspring.profiles.active=dev,jpa_dev,liquibase_dev,kafka_dev,something_else_dev
.
Comment From: wilkinsona
You'd specify the profiles as you normally would when starting the app. As usual, the profiles would influence the application properties and YAML files that are loaded into the Spring environment. The placeholders in the label values are then resolved against the environment.
Comment From: sdavids
Two alternative ideas:
Label org.springframework.boot.service.use-environment-configuration
or something similarly named.
If true
the Docker Compose support will use the appropriate config from the Spring environment for the annotated service.
It would not be as explicit though.
Label org.springframework.boot.service.configuration-precedence
with values like compose-only
, spring-only
, compose-first
, spring-first
.
Comment From: wilkinsona
Thanks for suggestion.
I'm not sure that Spring Boot's Docker Compose support should be that tightly coupled to the configuration properties that are defined in spring-boot-autoconfigure
. Just for the case of a DataSource's username, it would require the Docker Compose support to know about spring.datasource.username
, spring.datasource.hikari.username
, spring.datasource.dbcp2.username
, etc.
There's also the possibility that the JdbcConnectionDetails
bean has been defined using some other properties. The Docker Compose support would then have no way of knowing what properties to use when overriding it with the JDBC URL from the compose-managed container.
While it may make the compose YAML slightly more verbose, I think it will be better to configure the use of properties explicitly rather than trying to do it automatically. That also allows people who want to hardcode the custom credentials rather than using their application's properties to achieve their goal too.
Comment From: serandel
Spring Boot 3.3.4
I could workaround Liquibase with a custom AutoConfiguration.
By default, by getting the ConnectionDetails
with a custom factory, as seen in this thread, the application used the values from the application property files instead of creating one with the superuser credentials from the Docker container env variables.
But then, LiquibaseAutoConfiguration
would get a JdbcAdaptingLiquibaseConnectionDetailsFactory.LiquibaseConnectionDetails
, that took my custom JdbcConnectionDetails
and just wrapped them to use in Liquibase
as well.
As I wanted a different Liquibase
user, I did this...
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
example.CustomLiquibaseAutoConfiguration
src/main/kotlin/example/CustomLiquibaseAutoConfiguration.kt
@AutoConfiguration(before = [LiquibaseAutoConfiguration::class])
@ConditionalOnClass(
LiquibaseAutoConfiguration::class
)
class CustomLiquibaseAutoConfiguration {
@Bean
@Primary
fun customLiquibaseConnectionDetails(): LiquibaseConnectionDetails = CustomLiquibaseConnectionDetails()
private class CustomLiquibaseConnectionDetails :
LiquibaseConnectionDetails, EnvironmentAware {
private lateinit var environment: Environment
override fun setEnvironment(environment: Environment) {
this.environment = environment
}
override fun getUsername() = environment.getProperty("spring.liquibase.user")
override fun getPassword() = environment.getProperty("spring.liquibase.password")
override fun getJdbcUrl() = environment.getProperty("spring.datasource.url")
}
}
Now the Liquibase
auto configuration gets these connection details and I can enjoy different DB users and passwords for the Postgres superuser, my app main datasource and Liquibase.
Comment From: zartc
I too have encountered a similar problem when attempting to include spring-boot-docker-compose in my application.
I don't understand why, as stated by wilkinsona, spring-boot-docker-compose intentionally prefer to use database connection details comming from the SQL database service in the docker-compose.yml file, instead of those configured in the application.yml file!
Because of that, I had a lot of pain making Liquibase operates on my database of choice and my schema of choice instead of the default "postgres
/public
" (I had to avoid setting the POSTGRES_DB
environment variable to prevent Postgres from creating the database itself before my scripts have had a chance to run).
I seems to me that the database connection details coming from the SQL database service in the docker-compose.yml are somewhat limited in scope, because 1) it can only provide one username/password pair (for the admin), not multiple users/roles, and 2) because there is no way to change the schema name to operate on (always default to public
).
This implementation choice is not the wisest one IMHO, because 1) if I wanted to have this behavior (i.e. always work as administrator on the default public
schema), I could simply remove the data source configuration from the application.yml file and rely on the one created in memory by spring-boot-docker-compose and 2) because I naturally expect SpringBoot to use and respect the configurations carefully crafted in the application.yml file. If I've taken the time and effort to configure different datasources for the app and for Liquibase, it's obviously because I expect the application to respect these configurations.
I like spring-boot-docker-compose's feature, which consists of starting my infrastructure services, if needed, before starting the application, but I don't like that it overrides/ignore my configurations.
Due to this, spring-boot-docker-compose is more of a pain than a gain, for me at the moment. I can easily create a taskfile or a justfile or a makefile to start my services without pain. And the Dynamic port mapping isn't such a significant issue that it justifies ignoring the whole datasource (and liquibase) configurations in the application.yml file.
regards.