I built a SpringBoot application which writes and reads files from a runtimeDir which can be set from the environment: -DruntimeDir=/Users/user/app/runtime
On startup, the application wants to setup directories inside the runtime dir through a CommandLineRunner
@Value("${runtimeDir}/download")
private String downloadDirectory;
Files.createDirectories(downloadDirectory);
With the spring-boot-maven-plugin I built the app into an image through the bundled buildpacks. The container is setup through docker compose:
volumes:
app_data:
driver: local
driver_opts:
type: none
device: /home/user/runtime
o: bind
app:
image: 'app:0.0.1-SNAPSHOT'
ports:
- '8080:8080'
environment:
- RUNTIMEDIR=/workspace/app
volumes:
- app_data:/workspace/app
The App crashes with the following log statement:
java.lang.IllegalStateException: Failed to execute CommandLineRunner
at org.springframework.boot.SpringApplication.lambda$callRunner$6(SpringApplication.java:797)
at org.springframework.util.function.ThrowingConsumer.accept(ThrowingConsumer.java:66)
at org.springframework.util.function.ThrowingConsumer$1.accept(ThrowingConsumer.java:88)
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:798)
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:789)
at org.springframework.boot.SpringApplication.lambda$callRunners$3(SpringApplication.java:774)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(Unknown Source)
at java.base/java.util.stream.SortedOps$SizedRefSortingSink.end(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.copyInto(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(Unknown Source)
at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(Unknown Source)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.evaluate(Unknown Source)
at java.base/java.util.stream.ReferencePipeline.forEach(Unknown Source)
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:774)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:341)
at org.springframework.boot.builder.SpringApplicationBuilder.run(SpringApplicationBuilder.java:149)
at ars.App.main(App.java:35)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
at org.springframework.boot.loader.launch.Launcher.launch(Launcher.java:91)
at org.springframework.boot.loader.launch.Launcher.launch(Launcher.java:53)
at org.springframework.boot.loader.launch.JarLauncher.main(JarLauncher.java:58)
Caused by: java.nio.file.AccessDeniedException: /workspace/app/download
at java.base/sun.nio.fs.UnixException.translateToIOException(Unknown Source)
at java.base/sun.nio.fs.UnixException.rethrowAsIOException(Unknown Source)
at java.base/sun.nio.fs.UnixException.rethrowAsIOException(Unknown Source)
at java.base/sun.nio.fs.UnixFileSystemProvider.createDirectory(Unknown Source)
at java.base/java.nio.file.Files.createDirectory(Unknown Source)
at java.base/java.nio.file.Files.createAndCheckIsDirectory(Unknown Source)
at java.base/java.nio.file.Files.createDirectories(Unknown Source)
at ars.monitoring.ArsConsistencyCheck.createIfNotExists(ArsConsistencyCheck.java:151)
at ars.monitoring.ArsConsistencyCheck.run(ArsConsistencyCheck.java:54)
at org.springframework.boot.SpringApplication.lambda$callRunner$5(SpringApplication.java:790)
at org.springframework.util.function.ThrowingConsumer$1.acceptWithException(ThrowingConsumer.java:83)
at org.springframework.util.function.ThrowingConsumer.accept(ThrowingConsumer.java:60)
I tried already different things (different paths outside and inside the container, trying to set different users to own the paths), but cannot get my head around the root problem.
This post helped a little bit, but does not clarify my problem for me: Create a directory in a spring boot application built with cloud native buildpack to run on kubernetes
In different places, I read, that inside the buildpack image, the app runs as a user with groupt cnb and may only access /layers
and /workspace
. Therefore I set the runtime-var to /workspace/app
.
Then I mount a docker volume to this directory. The docker volume is set to bind to a dir inside the hostsystems user which starts the container. So the host path is owned by user:mygroup
Where exactly is my access permission problem now?
Is it inside the container/image?
Is it the user which created/runs the container which differs from the "1000:cnb" user of the container.
Reading from the directory is no problem on the other hand. Just writing into it.
Do I need the same users on the host and inside the container?
Is my docker volume setup wrong? I also tried simple binds with -v /home/user/app:/workspace/app
I had a fundamental lack of understanding of the permission system in docker mounts. As Daniel Mikusa also explained, the key point are the UID and GID of the users inside the container and the hostsystems mounted volume - or rather the path and its permissions in the hostsystem. With this knowledge I also understand and find more topics to this problem 👍
In my case I overcome the first issues with setting a custom user in the compose file:
app:
image: 'app:0.0.1-SNAPSHOT'
ports:
- '8080:8080'
user: "${UID}:${GID}" <-- the important part in my case. Will be filled from env-file.
environment:
- RUNTIMEDIR=/workspace/app
volumes:
- app_data:/workspace/app
Next change would be to move away from the inner workspace folder to not get into any conflicts on this behalf.
For a production deployment, it is necessary to know which user ids are needed in the setup on the hostsystem or through the docker option to overwrite the user used inside the container.