├── .gitignore ├── Jenkinsfile ├── README.adoc └── demo ├── .gitignore ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── Dockerfile ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── example │ │ └── demo │ │ └── DemoApplication.java └── resources │ └── application.properties └── test └── java └── com └── example └── demo └── DemoApplicationTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .#* 3 | *# 4 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent none 3 | 4 | triggers { 5 | pollSCM 'H/10 * * * *' 6 | } 7 | 8 | options { 9 | disableConcurrentBuilds() 10 | buildDiscarder(logRotator(numToKeepStr: '14')) 11 | } 12 | 13 | stages { 14 | stage("test: baseline (jdk8)") { 15 | agent { 16 | docker { 17 | image 'adoptopenjdk/openjdk8:latest' 18 | args '-v $HOME/.m2:/tmp/jenkins-home/.m2' 19 | } 20 | } 21 | options { timeout(time: 30, unit: 'MINUTES') } 22 | steps { 23 | sh 'test/run.sh' 24 | } 25 | } 26 | 27 | } 28 | 29 | post { 30 | changed { 31 | script { 32 | slackSend( 33 | color: (currentBuild.currentResult == 'SUCCESS') ? 'good' : 'danger', 34 | channel: '#sagan-content', 35 | message: "${currentBuild.fullDisplayName} - `${currentBuild.currentResult}`\n${env.BUILD_URL}") 36 | emailext( 37 | subject: "[${currentBuild.fullDisplayName}] ${currentBuild.currentResult}", 38 | mimeType: 'text/html', 39 | recipientProviders: [[$class: 'CulpritsRecipientProvider'], [$class: 'RequesterRecipientProvider']], 40 | body: "${currentBuild.fullDisplayName} is reported as ${currentBuild.currentResult}") 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | tags: [docker,containers] 3 | projects: [spring-boot] 4 | --- 5 | :toc: 6 | :icons: font 7 | :source-highlighter: prettify 8 | 9 | # This repository is no longer maintained. 10 | 11 | = Spring Boot in a Container 12 | 13 | Many people use containers to wrap their Spring Boot applications, and building containers is not a simple thing to do. This is a guide for developers of Spring Boot applications, and containers are not always a good abstraction for developers. They force you to learn about and think about low-level concerns. However, you may on occasion be called on to create or use a container, so it pays to understand the building blocks. In this guide, we aim to show you some of the choices you can make if you are faced with the prospect of needing to create your own container. 14 | 15 | We assume that you know how to create and build a basic Spring Boot application. If not, go to one of the https://spring.io/guides[Getting Started Guides] -- for example, the one on building a https://spring.io/guides/gs/rest-service/[REST Service]. Copy the code from there and practice with some of the ideas contained in this guide. 16 | 17 | NOTE: There is also a Getting Started Guide on https://spring.io/guides/gs/spring-boot-docker[Docker], which would also be a good starting point, but it does not cover the range of choices that we cover here or cover them in as much detail. 18 | 19 | == A Basic Dockerfile 20 | 21 | A Spring Boot application is easy to convert into an executable JAR file. All the https://spring.io/guides[Getting Started Guides] do this, and every application that you download from https://start.spring.io[Spring Initializr] has a build step to create an executable JAR. With Maven, you run `./mvnw install`, With Gradle, you run `./gradlew build`. A basic Dockerfile to run that JAR would then look like this, at the top level of your project: 22 | 23 | `Dockerfile` 24 | ==== 25 | [source] 26 | ---- 27 | FROM eclipse-temurin:17-jdk-alpine 28 | VOLUME /tmp 29 | ARG JAR_FILE 30 | COPY ${JAR_FILE} app.jar 31 | ENTRYPOINT ["java","-jar","/app.jar"] 32 | ---- 33 | ==== 34 | 35 | You could pass in the `JAR_FILE` as part of the `docker` command (it differs for Maven and Gradle). For Maven, the following command works: 36 | 37 | ==== 38 | [source,bash] 39 | ---- 40 | docker build --build-arg JAR_FILE=target/*.jar -t myorg/myapp . 41 | ---- 42 | ==== 43 | 44 | For Gradle, the following command works: 45 | 46 | ==== 47 | [source,bash] 48 | ---- 49 | docker build --build-arg JAR_FILE=build/libs/*.jar -t myorg/myapp . 50 | ---- 51 | ==== 52 | 53 | Once you have chosen a build system, you don't need the `ARG`. You can hard code the JAR location. For Maven, that would be as follows: 54 | 55 | `Dockerfile` 56 | ==== 57 | [source] 58 | ---- 59 | FROM eclipse-temurin:17-jdk-alpine 60 | VOLUME /tmp 61 | COPY target/*.jar app.jar 62 | ENTRYPOINT ["java","-jar","/app.jar"] 63 | ---- 64 | ==== 65 | 66 | Then we can build an image with the following command: 67 | 68 | ==== 69 | [source,bash] 70 | ---- 71 | docker build -t myorg/myapp . 72 | ---- 73 | ==== 74 | 75 | Then we can run it by running the following command: 76 | 77 | ==== 78 | [source,bash] 79 | ---- 80 | docker run -p 8080:8080 myorg/myapp 81 | ---- 82 | ==== 83 | 84 | The output resembles the following sample output: 85 | ==== 86 | [source,bash] 87 | ---- 88 | . ____ _ __ _ _ 89 | /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ 90 | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ 91 | \\/ ___)| |_)| | | | | || (_| | ) ) ) ) 92 | ' |____| .__|_| |_|_| |_\__, | / / / / 93 | =========|_|==============|___/=/_/_/_/ 94 | :: Spring Boot :: (v2.7.4) 95 | 96 | Nov 06, 2018 2:45:16 PM org.springframework.boot.StartupInfoLogger logStarting 97 | INFO: Starting Application v0.1.0 on b8469cdc9b87 with PID 1 (/app.jar started by root in /) 98 | Nov 06, 2018 2:45:16 PM org.springframework.boot.SpringApplication logStartupProfileInfo 99 | ... 100 | ---- 101 | ==== 102 | 103 | If you want to poke around inside the image, you can open a shell in it by running the following command (note that the base image does not have `bash`): 104 | 105 | ==== 106 | [source] 107 | ---- 108 | docker run -ti --entrypoint /bin/sh myorg/myapp 109 | ---- 110 | ==== 111 | 112 | The output is similar to the following sample output: 113 | 114 | ==== 115 | [source] 116 | ---- 117 | / # ls 118 | app.jar dev home media proc run srv tmp var 119 | bin etc lib mnt root sbin sys usr 120 | / # 121 | ---- 122 | ==== 123 | 124 | NOTE: The alpine base container we used in the example does not have `bash`, so this is an `ash` shell. It has some but not all of the features of `bash`. 125 | 126 | If you have a running container and you want to peek into it, you can do so by running `docker exec`: 127 | 128 | ==== 129 | [source,bash] 130 | ---- 131 | docker run --name myapp -ti --entrypoint /bin/sh myorg/myapp 132 | docker exec -ti myapp /bin/sh 133 | / # 134 | ---- 135 | ==== 136 | 137 | where `myapp` is the `--name` passed to the `docker run` command. If you did not use `--name`, docker assigns a mnemonic name, which you can get from the output of `docker ps`. You could also use the SHA identifier of the container instead of the name. The SHA identifier is also visible in the `docker ps` output. 138 | 139 | === The Entry Point 140 | 141 | The https://docs.docker.com/engine/reference/builder/#exec-form-entrypoint-example[exec form] of the Dockerfile `ENTRYPOINT` is used so that there is no shell wrapping the Java process. The advantage is that the java process responds to `KILL` signals sent to the container. In practice, that means (for instance) that, if you `docker run` your image locally, you can stop it with `CTRL-C`. If the command line gets a bit long, you can extract it out into a shell script and `COPY` it into the image before you run it. The following example shows how to do so: 142 | 143 | `Dockerfile` 144 | ==== 145 | [source] 146 | ---- 147 | FROM eclipse-temurin:17-jdk-alpine 148 | VOLUME /tmp 149 | COPY run.sh . 150 | COPY target/*.jar app.jar 151 | ENTRYPOINT ["run.sh"] 152 | ---- 153 | ==== 154 | 155 | Remember to use `exec java ...` to launch the java process (so that it can handle the `KILL` signals): 156 | 157 | `run.sh` 158 | ==== 159 | [source] 160 | ---- 161 | #!/bin/sh 162 | exec java -jar /app.jar 163 | ---- 164 | ==== 165 | 166 | Another interesting aspect of the entry point is whether or not you can inject environment variables into the Java process at runtime. For example, suppose you want to have the option to add Java command line options at runtime. You might try to do this: 167 | 168 | `Dockerfile` 169 | ==== 170 | [source] 171 | ---- 172 | FROM eclipse-temurin:17-jdk-alpine 173 | VOLUME /tmp 174 | ARG JAR_FILE=target/*.jar 175 | COPY ${JAR_FILE} app.jar 176 | ENTRYPOINT ["java","${JAVA_OPTS}","-jar","/app.jar"] 177 | ---- 178 | ==== 179 | 180 | Then you might try the following commands: 181 | 182 | ``` 183 | docker build -t myorg/myapp . 184 | docker run -p 9000:9000 -e JAVA_OPTS=-Dserver.port=9000 myorg/myapp 185 | ``` 186 | 187 | This fails because the `${}` substitution requires a shell. The exec form does not use a shell to launch the process, so the options are not applied. You can get around that by moving the entry point to a script (like the `run.sh` example shown earlier) or by explicitly creating a shell in the entry point. The following example shows how to create a shell in the entry point: 188 | 189 | `Dockerfile` 190 | ==== 191 | [source] 192 | ---- 193 | FROM eclipse-temurin:17-jdk-alpine 194 | VOLUME /tmp 195 | ARG JAR_FILE=target/*.jar 196 | COPY ${JAR_FILE} app.jar 197 | ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] 198 | ---- 199 | ==== 200 | 201 | You can then launch this app by running the following command: 202 | 203 | ==== 204 | [source,bash] 205 | ---- 206 | docker run -p 8080:8080 -e "JAVA_OPTS=-Ddebug -Xmx128m" myorg/myapp 207 | ---- 208 | ==== 209 | 210 | That command produces output similar to the following: 211 | 212 | ==== 213 | [source,bash] 214 | ---- 215 | . ____ _ __ _ _ 216 | /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ 217 | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ 218 | \\/ ___)| |_)| | | | | || (_| | ) ) ) ) 219 | ' |____| .__|_| |_|_| |_\__, | / / / / 220 | =========|_|==============|___/=/_/_/_/ 221 | :: Spring Boot :: (v2.7.4) 222 | ... 223 | 2019-10-29 09:12:12.169 DEBUG 1 --- [ main] ConditionEvaluationReportLoggingListener : 224 | 225 | 226 | ============================ 227 | CONDITIONS EVALUATION REPORT 228 | ============================ 229 | ... 230 | ---- 231 | ==== 232 | 233 | (The preceding output shows parts of the full `DEBUG` output that is generated with `-Ddebug` by Spring Boot.) 234 | 235 | Using an `ENTRYPOINT` with an explicit shell (as the preceding example does) means that you can pass environment variables into the Java command. So far, though, you cannot also provide command line arguments to the Spring Boot application. The following command does not run the application on port 9000: 236 | 237 | ==== 238 | [source,bash] 239 | ---- 240 | docker run -p 9000:9000 myorg/myapp --server.port=9000 241 | ---- 242 | ==== 243 | 244 | That command produces the following output, which shows the port as 8080 rather than 9000: 245 | 246 | ==== 247 | [source,bash] 248 | ---- 249 | . ____ _ __ _ _ 250 | /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ 251 | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ 252 | \\/ ___)| |_)| | | | | || (_| | ) ) ) ) 253 | ' |____| .__|_| |_|_| |_\__, | / / / / 254 | =========|_|==============|___/=/_/_/_/ 255 | :: Spring Boot :: (v2.7.4) 256 | ... 257 | 2019-10-29 09:20:19.718 INFO 1 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080 258 | ---- 259 | ==== 260 | 261 | It did not work because the docker command (the `--server.port=9000` part) is passed to the entry point (`sh`), not to the Java process that it launches. To fix that, you need to add the command line from the `CMD` to the `ENTRYPOINT`: 262 | 263 | `Dockerfile` 264 | ==== 265 | [source] 266 | ---- 267 | FROM eclipse-temurin:17-jdk-alpine 268 | VOLUME /tmp 269 | ARG JAR_FILE=target/*.jar 270 | COPY ${JAR_FILE} app.jar 271 | ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar ${0} ${@}"] 272 | ---- 273 | ==== 274 | 275 | Then you can run the same command and set the port to 9000: 276 | 277 | ==== 278 | [source,bash] 279 | ---- 280 | $ docker run -p 9000:9000 myorg/myapp --server.port=9000 281 | ---- 282 | ==== 283 | 284 | As the following output sampe shows, the port does get set to 9000: 285 | 286 | ==== 287 | [source,bash] 288 | ---- 289 | . ____ _ __ _ _ 290 | /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ 291 | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ 292 | \\/ ___)| |_)| | | | | || (_| | ) ) ) ) 293 | ' |____| .__|_| |_|_| |_\__, | / / / / 294 | =========|_|==============|___/=/_/_/_/ 295 | :: Spring Boot :: (v2.7.4) 296 | ... 297 | 2019-10-29 09:30:19.751 INFO 1 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 9000 298 | ---- 299 | ==== 300 | 301 | Note the use of `${0}` for the "`command`" (in this case the first program argument) and `${@}` for the "`command arguments`" (the rest of the program arguments). If you use a script for the entry point, then you do not need the `${0}` (that would be `/app/run.sh` in the earlier example). The following list shows the proper command in a script file: 302 | 303 | `run.sh` 304 | ==== 305 | [source] 306 | ---- 307 | #!/bin/sh 308 | exec java ${JAVA_OPTS} -jar /app.jar ${@} 309 | ---- 310 | ==== 311 | 312 | The docker configuration is very simple so far, and the generated image is not very efficient. The docker image has a single filesystem layer with the fat JAR in it, and every change we make to the application code changes that layer, which might be 10MB or more (even as much as 50MB for some applications). We can improve on that by splitting the JAR into multiple layers. 313 | 314 | === Smaller Images 315 | 316 | Notice that the base image in the earlier example is `eclipse-temurin:17-jdk-alpine`. The `alpine` images are smaller than the standard `eclipse-temurin` library images from https://hub.docker.com/_/eclipse-temurin/[Dockerhub]. You can also save about 20MB in the base image by using the `jre` label instead of `jdk`. Not all applications work with a JRE (as opposed to a JDK), but most do. Some organizations enforce a rule that every application has to work with a JRE because of the risk of misuse of some of the JDK features (such as compilation). 317 | 318 | Another trick that could get you a smaller image is to use https://openjdk.java.net/projects/jigsaw/quick-start#linker[JLink], which is bundled with OpenJDK 11 and above. JLink lets you build a custom JRE distribution from a subset of modules in the full JDK, so you do not need a JRE or JDK in the base image. In principle, this would get you a smaller total image size than using the official docker images. In practice a custom JRE in your own base image cannot be shared among other applications, since they would need different customizations. So you might have smaller images for all your applications, but they still take longer to start because they do not benefit from caching the JRE layer. 319 | 320 | That last point highlights a really important concern for image builders: the goal is not necessarily always going to be to build the smallest image possible. Smaller images are generally a good idea because they take less time to upload and download, but only if none of the layers in them are already cached. Image registries are quite sophisticated these days and you can easily lose the benefit of those features by trying to be clever with the image construction. If you use common base layers, the total size of an image is less of a concern, and it is likely to become even less of a concern as the registries and platforms evolve. Having said that, it is still important, and useful, to try to optimize the layers in our application image. However, the goals should always be to put the fastest changing stuff in the highest layers and to share as many of the large, lower layers as possible with other applications. 321 | 322 | [[a-better-dockerfile]] 323 | == A Better Dockerfile 324 | 325 | A Spring Boot fat JAR naturally has "`layers`" because of the way that the JAR itself is packaged. If we unpack it first, it is already divided into external and internal dependencies. To do this in one step in the docker build, we need to unpack the JAR first. The following commands (sticking with Maven, but the Gradle version is pretty similar) unpack a Spring Boot fat JAR: 326 | 327 | ==== 328 | [source,bash] 329 | ---- 330 | mkdir target/dependency 331 | (cd target/dependency; jar -xf ../*.jar) 332 | docker build -t myorg/myapp . 333 | ---- 334 | ==== 335 | 336 | Then we can use the following `Dockerfile` 337 | 338 | `Dockerfile` 339 | ==== 340 | [source] 341 | ---- 342 | FROM eclipse-temurin:17-jdk-alpine 343 | VOLUME /tmp 344 | ARG DEPENDENCY=target/dependency 345 | COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib 346 | COPY ${DEPENDENCY}/META-INF /app/META-INF 347 | COPY ${DEPENDENCY}/BOOT-INF/classes /app 348 | ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"] 349 | ---- 350 | ==== 351 | 352 | There are now three layers, with all the application resources in the later two layers. If the application dependencies do not change, the first layer (from `BOOT-INF/lib`) need not change, so the build is faster, and the startup of the container at runtime if also faster, as long as the base layers are already cached. 353 | 354 | NOTE: We used a hard-coded main application class: `hello.Application`. This is probably different for your application. You could parameterize it with another `ARG` if you wanted. You could also copy the Spring Boot fat `JarLauncher` into the image and use it to run the application. It would work and you would not need to specify the main class, but it would be a bit slower on startup. 355 | 356 | === Spring Boot Layer Index 357 | 358 | Starting with Spring Boot 2.3.0, a JAR file built with the Spring Boot Maven or Gradle plugin includes https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.container-images.layering[layer information] in the JAR file. 359 | This layer information separates parts of the application based on how likely they are to change between application builds. 360 | This can be used to make Docker image layers even more efficient. 361 | 362 | The layer information can be used to extract the JAR contents into a directory for each layer: 363 | 364 | ==== 365 | [source,bash] 366 | ---- 367 | mkdir target/extracted 368 | java -Djarmode=layertools -jar target/*.jar extract --destination target/extracted 369 | docker build -t myorg/myapp . 370 | ---- 371 | ==== 372 | 373 | Then we can use the following `Dockerfile`: 374 | 375 | `Dockerfile` 376 | ==== 377 | [source] 378 | ---- 379 | FROM eclipse-temurin:17-jdk-alpine 380 | VOLUME /tmp 381 | ARG EXTRACTED=/workspace/app/target/extracted 382 | COPY ${EXTRACTED}/dependencies/ ./ 383 | COPY ${EXTRACTED}/spring-boot-loader/ ./ 384 | COPY ${EXTRACTED}/snapshot-dependencies/ ./ 385 | COPY ${EXTRACTED}/application/ ./ 386 | ENTRYPOINT ["java","org.springframework.boot.loader.launch.JarLauncher"] 387 | ---- 388 | ==== 389 | 390 | NOTE: The Spring Boot fat `JarLauncher` is extracted from the JAR into the image, so it can be used to start the application without hard-coding the main application class. 391 | 392 | See the https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.container-images.building.dockerfiles[Spring Boot documentation] for more information on using the layering feature. 393 | 394 | == Tweaks 395 | 396 | If you want to start your application as quickly as possible (most people do), you might consider some tweaks: 397 | 398 | * Use the `spring-context-indexer` (https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-scanning-index[link to docs]). It is not going to add much for small applications, but every little helps. 399 | * Do not use https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#production-ready[actuators] if you can afford not to. 400 | * Use the latest versions of Spring Boot and Spring. 401 | * Fix the location of the 402 | https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config-application-property-files[Spring Boot config file(s)] 403 | with `spring.config.location` (by command line argument, System property, or other approach). 404 | 405 | Your application might not need a full CPU at runtime, but it does need multiple CPUs to start up as quickly as possible (at least two, four is better). If you do not mind a slower startup, you could throttle the CPUs down below four. If you are forced to start with less than four CPUs, it might help to set `-Dspring.backgroundpreinitializer.ignore=true`, since it prevents Spring Boot from creating a new thread that it probably cannot use (this works with Spring Boot 2.1.0 and above). 406 | 407 | == Multi-Stage Build 408 | 409 | The `Dockerfile` shown in <> assumed that the fat JAR was already built on the command line. You can also do that step in docker by using a multi-stage build and copying the result from one image to another. The following example does so by using Maven: 410 | 411 | `Dockerfile` 412 | ==== 413 | [source] 414 | ---- 415 | FROM eclipse-temurin:17-jdk-alpine as build 416 | WORKDIR /workspace/app 417 | 418 | COPY mvnw . 419 | COPY .mvn .mvn 420 | COPY pom.xml . 421 | COPY src src 422 | 423 | RUN ./mvnw install -DskipTests 424 | RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar) 425 | 426 | FROM eclipse-temurin:17-jdk-alpine 427 | VOLUME /tmp 428 | ARG DEPENDENCY=/workspace/app/target/dependency 429 | COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib 430 | COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF 431 | COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app 432 | ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"] 433 | ---- 434 | ==== 435 | 436 | The first image is labelled `build`, and it is used to run Maven, build the fat JAR, and unpack it. The unpacking could also be done by Maven or Gradle (this is the approach taken in the Getting Started Guide). There is not much difference, except that the build configuration would have to be edited and a plugin added. 437 | 438 | Notice that the source code has been split into four layers. The later layers contain the build configuration and the source code for the application, and the earlier layers contain the build system itself (the Maven wrapper). This is a small optimization, and it also means that we do not have to copy the `target` directory to a docker image, even a temporary one used for the build. 439 | 440 | Every build where the source code changes is slow because the Maven cache has to be re-created in the first `RUN` section. But you have a completely standalone build that anyone can run to get your application running as long as they have docker. That can be quite useful in some environments -- for example, where you need to share your code with people who do not know Java. 441 | 442 | === Experimental Features 443 | 444 | Docker 18.06 comes with some https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/experimental.md["`experimental`" features], including a way to cache build dependencies. To switch them on, you need a flag in the daemon (`dockerd`) and an environment variable when you run the client. Then you can add a "`magic`" first line to your `Dockerfile`: 445 | 446 | `Dockerfile` 447 | ==== 448 | [source] 449 | ---- 450 | # syntax=docker/dockerfile:experimental 451 | ---- 452 | ==== 453 | 454 | The `RUN` directive then accepts a new flag: `--mount`. The following listing shows a full example: 455 | 456 | `Dockerfile` 457 | ==== 458 | [source] 459 | ---- 460 | # syntax=docker/dockerfile:experimental 461 | FROM eclipse-temurin:17-jdk-alpine as build 462 | WORKDIR /workspace/app 463 | 464 | COPY mvnw . 465 | COPY .mvn .mvn 466 | COPY pom.xml . 467 | COPY src src 468 | 469 | RUN --mount=type=cache,target=/root/.m2 ./mvnw install -DskipTests 470 | RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar) 471 | 472 | FROM eclipse-temurin:17-jdk-alpine 473 | VOLUME /tmp 474 | ARG DEPENDENCY=/workspace/app/target/dependency 475 | COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib 476 | COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF 477 | COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app 478 | ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"] 479 | ---- 480 | ==== 481 | 482 | Then you can run it: 483 | 484 | ==== 485 | [source,bash] 486 | ---- 487 | DOCKER_BUILDKIT=1 docker build -t myorg/myapp . 488 | ---- 489 | ==== 490 | 491 | The following listing shows sample output: 492 | 493 | ==== 494 | [source,bash] 495 | ---- 496 | ... 497 | => /bin/sh -c ./mvnw install -DskipTests 5.7s 498 | => exporting to image 0.0s 499 | => => exporting layers 0.0s 500 | => => writing image sha256:3defa... 501 | => => naming to docker.io/myorg/myapp 502 | ---- 503 | ==== 504 | 505 | With the experimental features, you get different output on the console, but you can see that a Maven build now only takes a few seconds instead of minutes, provided the cache is warm. 506 | 507 | The Gradle version of this `Dockerfile` configuration is very similar: 508 | 509 | `Dockerfile` 510 | ==== 511 | [source] 512 | ---- 513 | # syntax=docker/dockerfile:experimental 514 | FROM eclipse-temurin:17-jdk-alpine AS build 515 | WORKDIR /workspace/app 516 | 517 | COPY . /workspace/app 518 | RUN --mount=type=cache,target=/root/.gradle ./gradlew clean build 519 | RUN mkdir -p build/dependency && (cd build/dependency; jar -xf ../libs/*-SNAPSHOT.jar) 520 | 521 | FROM eclipse-temurin:17-jdk-alpine 522 | VOLUME /tmp 523 | ARG DEPENDENCY=/workspace/app/build/dependency 524 | COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib 525 | COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF 526 | COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app 527 | ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"] 528 | ---- 529 | ==== 530 | 531 | NOTE: While these features are in the experimental phase, the options for switching buildkit on and off depend on the version of `docker` that you use. Check the documentation for the version you have (the example shown earlier is correct for `docker` 18.0.6). 532 | 533 | == Security Aspects 534 | 535 | Just as in classic VM deployments, processes should not be run with root permissions. Instead, the image should contain a non-root user that runs the application. 536 | 537 | In a `Dockerfile`, you can achieve this by adding another layer that adds a (system) user and group and setting it as the current user (instead of the default, root): 538 | 539 | `Dockerfile` 540 | ==== 541 | [source] 542 | ---- 543 | FROM eclipse-temurin:17-jdk-alpine 544 | 545 | RUN addgroup -S demo && adduser -S demo -G demo 546 | USER demo 547 | 548 | ... 549 | ---- 550 | ==== 551 | 552 | In case someone manages to break out of your application and run system commands inside the container, this precaution limits their capabilities (following the principle of least privilege). 553 | 554 | NOTE: Some of the further `Dockerfile` commands only work as root, so maybe you have to move the USER command further down (for example, if you plan to install more packages in the container, which works only as root). 555 | 556 | NOTE: For other approaches, not using a `Dockerfile` might be more amenable. For instance, in the buildpack approach described later, most implementations use a non-root user by default. 557 | 558 | Another consideration is that the full JDK is probably not needed by most applications at runtime, so we can safely switch to the JRE base image, once we have a multi-stage build. So, in the multi-stage build shown earlier we can use for the final, runnable image: 559 | 560 | `Dockerfile` 561 | ==== 562 | [source] 563 | ---- 564 | FROM eclipse-temurin:17-jre-alpine 565 | 566 | ... 567 | ---- 568 | ==== 569 | 570 | As mentioned earlier, this also saves some space in the image, which would be occupied by tools that are not needed at runtime. 571 | 572 | == Build Plugins 573 | 574 | If you do not want to call `docker` directly in your build, there is a rich set of plugins for Maven and Gradle that can do that work for you. Here are just a few. 575 | 576 | === Spring Boot Maven and Gradle Plugins 577 | 578 | You can use the Spring Boot build plugins for https://docs.spring.io/spring-boot/docs/current/maven-plugin/reference/htmlsingle/#build-image[Maven] and https://docs.spring.io/spring-boot/docs/current/gradle-plugin/reference/htmlsingle/#build-image[Gradle] to create container images. 579 | The plugins create an OCI image (the same format as one created by `docker build`) by using https://buildpacks.io/[Cloud Native Buildpacks]. 580 | You do not need a `Dockerfile`, but you do need a Docker daemon, either locally (which is what you use when you build with docker) or remotely through the `DOCKER_HOST` environment variable. 581 | The default builder is optimized for Spring Boot applications, and the image is layered efficiently as in the examples above. 582 | 583 | The following example works with Maven without changing the `pom.xml` file: 584 | 585 | ==== 586 | [source,bash] 587 | ---- 588 | ./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=myorg/myapp 589 | ---- 590 | ==== 591 | 592 | The following example works with Gradle, without changing the `build.gradle` file: 593 | 594 | ==== 595 | [source,bash] 596 | ---- 597 | ./gradlew bootBuildImage --imageName=myorg/myapp 598 | ---- 599 | ==== 600 | 601 | The first build might take a long time because it has to download some container images and the JDK, but subsequent builds should be fast. 602 | 603 | Then you can run the image, as the following listing shows (with output): 604 | 605 | ==== 606 | [source,bash] 607 | ---- 608 | docker run -p 8080:8080 -t myorg/myapp 609 | Setting Active Processor Count to 6 610 | Calculating JVM memory based on 14673596K available memory 611 | Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -Xmx14278122K -XX:MaxMetaspaceSize=88273K -XX:ReservedCodeCacheSize=240M -Xss1M (Total Memory: 14673596K, Thread Count: 50, Loaded Class Count: 13171, Headroom: 0%) 612 | Adding 129 container CA certificates to JVM truststore 613 | Spring Cloud Bindings Enabled 614 | Picked up JAVA_TOOL_OPTIONS: -Djava.security.properties=/layers/paketo-buildpacks_bellsoft-liberica/java-security-properties/java-security.properties -agentpath:/layers/paketo-buildpacks_bellsoft-liberica/jvmkill/jvmkill-1.16.0-RELEASE.so=printHeapHistogram=1 -XX:ActiveProcessorCount=6 -XX:MaxDirectMemorySize=10M -Xmx14278122K -XX:MaxMetaspaceSize=88273K -XX:ReservedCodeCacheSize=240M -Xss1M -Dorg.springframework.cloud.bindings.boot.enable=true 615 | .... 616 | 2015-03-31 13:25:48.035 INFO 1 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http) 617 | 2015-03-31 13:25:48.037 INFO 1 --- [ main] hello.Application 618 | ---- 619 | ==== 620 | 621 | You can see the application start up as normal. 622 | You might also notice that the JVM memory requirements were computed and set as command line options inside the container. 623 | This is the same memory calculation that has been in use in Cloud Foundry build packs for many years. 624 | It represents significant research into the best choices for a range of JVM applications, including but not limited to Spring Boot applications, and the results are usually much better than the default setting from the JVM. 625 | You can customize the command line options and override the memory calculator by setting environment variables as shown in the https://paketo.io/docs/howto/java/[Paketo buildpacks documentation]. 626 | 627 | 628 | === Spotify Maven Plugin 629 | 630 | The https://github.com/spotify/dockerfile-maven[Spotify Maven Plugin] is a popular choice. It requires you to write a `Dockerfile` and then runs `docker` for you, just as if you were doing it on the command line. There are some configuration options for the docker image tag and other stuff, but it keeps the docker knowledge in your application concentrated in a `Dockerfile`, which many people like. 631 | 632 | For really basic usage, it will work out of the box with no extra configuration: 633 | 634 | ==== 635 | [source,bash] 636 | ---- 637 | mvn com.spotify:dockerfile-maven-plugin:build 638 | ... 639 | [INFO] Building Docker context /home/dsyer/dev/demo/workspace/myapp 640 | [INFO] 641 | [INFO] Image will be built without a name 642 | [INFO] 643 | ... 644 | [INFO] BUILD SUCCESS 645 | [INFO] ------------------------------------------------------------------------ 646 | [INFO] Total time: 7.630 s 647 | [INFO] Finished at: 2018-11-06T16:03:16+00:00 648 | [INFO] Final Memory: 26M/595M 649 | [INFO] ------------------------------------------------------------------------ 650 | ---- 651 | ==== 652 | 653 | That builds an anonymous docker image. We can tag it with `docker` on the command line now or use Maven configuration to set it as the `repository`. The following example works without changing the `pom.xml` file: 654 | 655 | ==== 656 | [source,bash] 657 | ---- 658 | $ mvn com.spotify:dockerfile-maven-plugin:build -Ddockerfile.repository=myorg/myapp 659 | ---- 660 | ==== 661 | 662 | Alternatively, you change the `pom.xml` file: 663 | 664 | `pom.xml` 665 | ==== 666 | [source,xml] 667 | ---- 668 | 669 | 670 | 671 | com.spotify 672 | dockerfile-maven-plugin 673 | 1.4.8 674 | 675 | myorg/${project.artifactId} 676 | 677 | 678 | 679 | 680 | ---- 681 | ==== 682 | 683 | === Palantir Gradle Plugin 684 | 685 | The https://github.com/palantir/gradle-docker[Palantir Gradle Plugin] works with a `Dockerfile` and can also generate a `Dockerfile` for you. Then it runs `docker` as if you were running it on the command line. 686 | 687 | First you need to import the plugin into your `build.gradle`: 688 | 689 | `build.gradle` 690 | ==== 691 | [source,groovy] 692 | ---- 693 | buildscript { 694 | ... 695 | dependencies { 696 | ... 697 | classpath('gradle.plugin.com.palantir.gradle.docker:gradle-docker:0.13.0') 698 | } 699 | } 700 | ---- 701 | ==== 702 | 703 | Then, finally, you can apply the plugin and call its task: 704 | 705 | `build.gradle` 706 | ==== 707 | [source,groovy] 708 | ---- 709 | apply plugin: 'com.palantir.docker' 710 | 711 | group = 'myorg' 712 | 713 | bootJar { 714 | baseName = 'myapp' 715 | version = '0.1.0' 716 | } 717 | 718 | task unpack(type: Copy) { 719 | dependsOn bootJar 720 | from(zipTree(tasks.bootJar.outputs.files.singleFile)) 721 | into("build/dependency") 722 | } 723 | docker { 724 | name "${project.group}/${bootJar.baseName}" 725 | copySpec.from(tasks.unpack.outputs).into("dependency") 726 | buildArgs(['DEPENDENCY': "dependency"]) 727 | } 728 | ---- 729 | ==== 730 | 731 | In this example, we have chosen to unpack the Spring Boot fat JAR in a specific location in the `build` directory, which is the root for the docker build. Then the multi-layer (not multi-stage) `Dockerfile` shown earlier works. 732 | 733 | === Jib Maven and Gradle Plugins 734 | 735 | Google has an open source tool called https://github.com/GoogleContainerTools/jib[Jib] that is relatively new but quite interesting for a number of reasons. Probably the most interesting thing is that you do not need docker to run it. Jib builds the image by using the same standard output as you get from `docker build` but does not use `docker` unless you ask it to, so it works in environments where docker is not installed (common in build servers). You also do not need a `Dockerfile` (it would be ignored anyway) or anything in your `pom.xml` to get an image built in Maven (Gradle would require you to at least install the plugin in `build.gradle`). 736 | 737 | Another interesting feature of Jib is that it is opinionated about layers, and it optimizes them in a slightly different way than the multi-layer `Dockerfile` created above. As in the fat JAR, Jib separates local application resources from dependencies, but it goes a step further and also puts snapshot dependencies into a separate layer, since they are more likely to change. There are configuration options for customizing the layout further. 738 | 739 | The following example works with Maven without changing the `pom.xml`: 740 | 741 | ==== 742 | [source,bash] 743 | ---- 744 | $ mvn com.google.cloud.tools:jib-maven-plugin:build -Dimage=myorg/myapp 745 | ---- 746 | ==== 747 | 748 | To run that command, you need to have permission to push to Dockerhub under the `myorg` repository prefix. If you have authenticated with `docker` on the command line, that works from your local `~/.docker` configuration. You can also set up a Maven "`server`" authentication in your `~/.m2/settings.xml` (the `id` of the repository is significant): 749 | 750 | `settings.xml` 751 | ==== 752 | [source] 753 | ---- 754 | 755 | registry.hub.docker.com 756 | myorg 757 | ... 758 | 759 | ---- 760 | ==== 761 | 762 | There are other options -- for example, you can build locally against a docker daemon (like running `docker` on the command line), using the `dockerBuild` goal instead of `build`. Other container registries are also supported. For each one, you need to set up local authentication through Docker or Maven settings. 763 | 764 | The gradle plugin has similar features, once you have it in your `build.gradle`:. 765 | 766 | `build.gradle` 767 | ==== 768 | [source,groovy] 769 | ---- 770 | plugins { 771 | ... 772 | id 'com.google.cloud.tools.jib' version '1.8.0' 773 | } 774 | ---- 775 | ==== 776 | 777 | Then you can build an image by running the following command: 778 | 779 | ==== 780 | [source,bash] 781 | ---- 782 | ./gradlew jib --image=myorg/myapp 783 | ---- 784 | ==== 785 | 786 | As with the Maven build, if you have authenticated with `docker` on the command line, the image push authenticates from your local `~/.docker` configuration. 787 | 788 | == Continuous Integration 789 | 790 | Automation (or should be) is part of every application lifecycle these days. The tools that people use to do the automation tend to be quite good at invoking the build system from the source code. So if that gets you a docker image, and the environment in the build agents is sufficiently aligned with developer's own environment, that might be good enough. Authenticating to the docker registry is likely to be the biggest challenge, but there are features in all the automation tools to help with that. 791 | 792 | However, sometimes it is better to leave container creation completely to an automation layer, in which case the user's code might not need to be polluted. Container creation is tricky, and developers sometimes need not really care about it. If the user code is cleaner, there is more chance that a different tool can "`do the right thing`" (applying security fixes, optimizing caches, and so on). There are multiple options for automation, and they all come with some features related to containers these days. We are going to look at a couple. 793 | 794 | === Concourse 795 | 796 | https://concourse-ci.org[Concourse] is a pipeline-based automation platform that you can use for CI and CD. It is used inside VMware, and the main authors of the project work there. Everything in Concourse is stateless and runs in a container, except the CLI. Since running containers is the main order of business for the automation pipelines, creating containers is well supported. The https://github.com/concourse/docker-image-resource[Docker Image Resource] is responsible for keeping the output state of your build up to date, if it is a container image. 797 | 798 | The following example pipeline builds a docker image for the sample shown earlier, assuming it is in github at `myorg/myapp`, has a `Dockerfile` at the root, and has a build task declaration in `src/main/ci/build.yml`: 799 | 800 | ==== 801 | [source] 802 | ---- 803 | resources: 804 | - name: myapp 805 | type: git 806 | source: 807 | uri: https://github.com/myorg/myapp.git 808 | - name: myapp-image 809 | type: docker-image 810 | source: 811 | email: {{docker-hub-email}} 812 | username: {{docker-hub-username}} 813 | password: {{docker-hub-password}} 814 | repository: myorg/myapp 815 | 816 | jobs: 817 | - name: main 818 | plan: 819 | - task: build 820 | file: myapp/src/main/ci/build.yml 821 | - put: myapp-image 822 | params: 823 | build: myapp 824 | ---- 825 | ==== 826 | 827 | The structure of a pipeline is very declarative: You define "`resources`" (input, output, or both), and "`jobs`" (which use and apply actions to resources). If any of the input resources changes, a new build is triggered. If any of the output resources changes during a job, it is updated. 828 | 829 | The pipeline could be defined in a different place than the application source code. Also, for a generic build setup, the task declarations can be centralized or externalized as well. This allows some separation of concerns between development and automation, which suits some software development organizations. 830 | 831 | === Jenkins 832 | 833 | https://jenkins.io[Jenkins] is another popular automation server. It has a huge range of features, but one that is the closest to the other automation samples here is the https://jenkins.io/doc/book/pipeline/docker/[pipeline] feature. The following `Jenkinsfile` builds a Spring Boot project with Maven and then uses a `Dockerfile` to build an image and push it to a repository: 834 | 835 | `Jenkinsfile` 836 | ==== 837 | [source] 838 | ---- 839 | node { 840 | checkout scm 841 | sh './mvnw -B -DskipTests clean package' 842 | docker.build("myorg/myapp").push() 843 | } 844 | ---- 845 | ==== 846 | 847 | For a (realistic) docker repository that needs authentication in the build server, you can add credentials to the `docker` object by using `docker.withCredentials(...)`. 848 | 849 | == Buildpacks 850 | 851 | NOTE: The Spring Boot Maven and Gradle plugins use buildpacks in exactly the same way that the `pack` CLI does in the following examples. 852 | The resulting images are identical, given the same inputs. 853 | 854 | https://www.cloudfoundry.org/[Cloud Foundry] has used containers internally for many years now, and part of the technology used to transform user code into containers is Build Packs, an idea originally borrowed from https://www.heroku.com/[Heroku]. The current generation of buildpacks (v2) generates generic binary output that is assembled into a container by the platform. The https://buildpacks.io/[new generation of buildpacks] (v3) is a collaboration between Heroku and other companies (including VMware), and it builds container images directly and explicitly. This is interesting for developers and operators. Developers do not need to care much about the details of how to build a container, but they can easily create one if they need to. Buildpacks also have lots of features for caching build results and dependencies. Often, a buildpack runs much more quickly than a native Docker build. Operators can scan the containers to audit their contents and transform them to patch them for security updates. Also, you can run the buildpacks locally (for example, on a developer machine or in a CI service) or in a platform like Cloud Foundry. 855 | 856 | The output from a buildpack lifecycle is a container image, but you do not need a `Dockerfile`. The filesystem layers in the output image are controlled by the buildpack. Typically, many optimizations are made without the developer having to know or care about them. There is also an https://en.wikipedia.org/wiki/Application_binary_interface[Application Binary Interface] between the lower level layers (such as the base image containing the operating system) and the upper layers (containing middleware and language specific dependencies). This makes it possible for a platform, such as Cloud Foundry, to patch lower layers if there are security updates without affecting the integrity and functionality of the application. 857 | 858 | To give you an idea of the features of a buildpack, the following example (shown with its output) uses the https://buildpacks.io/docs/tools/pack/[Pack CLI] from the command line (it would work with the sample application we have been using in this guide -- no need for a `Dockerfile` or any special build configuration): 859 | 860 | ==== 861 | [source,bash] 862 | ---- 863 | pack build myorg/myapp --builder=paketobuildpacks/builder:base --path=. 864 | base: Pulling from paketobuildpacks/builder 865 | Digest: sha256:4fae5e2abab118ca9a37bf94ab42aa17fef7c306296b0364f5a0e176702ab5cb 866 | Status: Image is up to date for paketobuildpacks/builder:base 867 | base-cnb: Pulling from paketobuildpacks/run 868 | Digest: sha256:a285e73bc3697bc58c228b22938bc81e9b11700e087fd9d44da5f42f14861812 869 | Status: Image is up to date for paketobuildpacks/run:base-cnb 870 | ===> DETECTING 871 | 7 of 18 buildpacks participating 872 | paketo-buildpacks/ca-certificates 2.3.2 873 | paketo-buildpacks/bellsoft-liberica 8.2.0 874 | paketo-buildpacks/maven 5.3.2 875 | paketo-buildpacks/executable-jar 5.1.2 876 | paketo-buildpacks/apache-tomcat 5.6.1 877 | paketo-buildpacks/dist-zip 4.1.2 878 | paketo-buildpacks/spring-boot 4.4.2 879 | ===> ANALYZING 880 | Previous image with name "myorg/myapp" not found 881 | ===> RESTORING 882 | ===> BUILDING 883 | 884 | Paketo CA Certificates Buildpack 2.3.2 885 | https://github.com/paketo-buildpacks/ca-certificates 886 | Launch Helper: Contributing to layer 887 | Creating /layers/paketo-buildpacks_ca-certificates/helper/exec.d/ca-certificates-helper 888 | 889 | Paketo BellSoft Liberica Buildpack 8.2.0 890 | https://github.com/paketo-buildpacks/bellsoft-liberica 891 | Build Configuration: 892 | $BP_JVM_VERSION 11 the Java version 893 | Launch Configuration: 894 | $BPL_JVM_HEAD_ROOM 0 the headroom in memory calculation 895 | $BPL_JVM_LOADED_CLASS_COUNT 35% of classes the number of loaded classes in memory calculation 896 | $BPL_JVM_THREAD_COUNT 250 the number of threads in memory calculation 897 | $JAVA_TOOL_OPTIONS the JVM launch flags 898 | BellSoft Liberica JDK 11.0.12: Contributing to layer 899 | Downloading from https://github.com/bell-sw/Liberica/releases/download/11.0.12+7/bellsoft-jdk11.0.12+7-linux-amd64.tar.gz 900 | Verifying checksum 901 | Expanding to /layers/paketo-buildpacks_bellsoft-liberica/jdk 902 | Adding 129 container CA certificates to JVM truststore 903 | Writing env.build/JAVA_HOME.override 904 | Writing env.build/JDK_HOME.override 905 | BellSoft Liberica JRE 11.0.12: Contributing to layer 906 | Downloading from https://github.com/bell-sw/Liberica/releases/download/11.0.12+7/bellsoft-jre11.0.12+7-linux-amd64.tar.gz 907 | Verifying checksum 908 | Expanding to /layers/paketo-buildpacks_bellsoft-liberica/jre 909 | Adding 129 container CA certificates to JVM truststore 910 | Writing env.launch/BPI_APPLICATION_PATH.default 911 | Writing env.launch/BPI_JVM_CACERTS.default 912 | Writing env.launch/BPI_JVM_CLASS_COUNT.default 913 | Writing env.launch/BPI_JVM_SECURITY_PROVIDERS.default 914 | Writing env.launch/JAVA_HOME.default 915 | Writing env.launch/MALLOC_ARENA_MAX.default 916 | Launch Helper: Contributing to layer 917 | Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/active-processor-count 918 | Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/java-opts 919 | Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/link-local-dns 920 | Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/memory-calculator 921 | Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/openssl-certificate-loader 922 | Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/security-providers-configurer 923 | Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/security-providers-classpath-9 924 | JVMKill Agent 1.16.0: Contributing to layer 925 | Downloading from https://github.com/cloudfoundry/jvmkill/releases/download/v1.16.0.RELEASE/jvmkill-1.16.0-RELEASE.so 926 | Verifying checksum 927 | Copying to /layers/paketo-buildpacks_bellsoft-liberica/jvmkill 928 | Writing env.launch/JAVA_TOOL_OPTIONS.append 929 | Writing env.launch/JAVA_TOOL_OPTIONS.delim 930 | Java Security Properties: Contributing to layer 931 | Writing env.launch/JAVA_SECURITY_PROPERTIES.default 932 | Writing env.launch/JAVA_TOOL_OPTIONS.append 933 | Writing env.launch/JAVA_TOOL_OPTIONS.delim 934 | 935 | Paketo Maven Buildpack 5.3.2 936 | https://github.com/paketo-buildpacks/maven 937 | Build Configuration: 938 | $BP_MAVEN_BUILD_ARGUMENTS -Dmaven.test.skip=true package the arguments to pass to Maven 939 | $BP_MAVEN_BUILT_ARTIFACT target/*.[jw]ar the built application artifact explicitly. Supersedes $BP_MAVEN_BUILT_MODULE 940 | $BP_MAVEN_BUILT_MODULE the module to find application artifact in 941 | Creating cache directory /home/cnb/.m2 942 | Compiled Application: Contributing to layer 943 | Executing mvnw --batch-mode -Dmaven.test.skip=true package 944 | 945 | [ ... Maven build output ... ] 946 | 947 | [INFO] ------------------------------------------------------------------------ 948 | [INFO] BUILD SUCCESS 949 | [INFO] ------------------------------------------------------------------------ 950 | [INFO] Total time: 53.474 s 951 | [INFO] Finished at: 2021-07-23T20:10:28Z 952 | [INFO] ------------------------------------------------------------------------ 953 | Removing source code 954 | 955 | Paketo Executable JAR Buildpack 5.1.2 956 | https://github.com/paketo-buildpacks/executable-jar 957 | Class Path: Contributing to layer 958 | Writing env/CLASSPATH.delim 959 | Writing env/CLASSPATH.prepend 960 | Process types: 961 | executable-jar: java org.springframework.boot.loader.JarLauncher (direct) 962 | task: java org.springframework.boot.loader.JarLauncher (direct) 963 | web: java org.springframework.boot.loader.JarLauncher (direct) 964 | 965 | Paketo Spring Boot Buildpack 4.4.2 966 | https://github.com/paketo-buildpacks/spring-boot 967 | Creating slices from layers index 968 | dependencies 969 | spring-boot-loader 970 | snapshot-dependencies 971 | application 972 | Launch Helper: Contributing to layer 973 | Creating /layers/paketo-buildpacks_spring-boot/helper/exec.d/spring-cloud-bindings 974 | Spring Cloud Bindings 1.7.1: Contributing to layer 975 | Downloading from https://repo.spring.io/release/org/springframework/cloud/spring-cloud-bindings/1.7.1/spring-cloud-bindings-1.7.1.jar 976 | Verifying checksum 977 | Copying to /layers/paketo-buildpacks_spring-boot/spring-cloud-bindings 978 | Web Application Type: Contributing to layer 979 | Reactive web application detected 980 | Writing env.launch/BPL_JVM_THREAD_COUNT.default 981 | 4 application slices 982 | Image labels: 983 | org.opencontainers.image.title 984 | org.opencontainers.image.version 985 | org.springframework.boot.version 986 | ===> EXPORTING 987 | Adding layer 'paketo-buildpacks/ca-certificates:helper' 988 | Adding layer 'paketo-buildpacks/bellsoft-liberica:helper' 989 | Adding layer 'paketo-buildpacks/bellsoft-liberica:java-security-properties' 990 | Adding layer 'paketo-buildpacks/bellsoft-liberica:jre' 991 | Adding layer 'paketo-buildpacks/bellsoft-liberica:jvmkill' 992 | Adding layer 'paketo-buildpacks/executable-jar:classpath' 993 | Adding layer 'paketo-buildpacks/spring-boot:helper' 994 | Adding layer 'paketo-buildpacks/spring-boot:spring-cloud-bindings' 995 | Adding layer 'paketo-buildpacks/spring-boot:web-application-type' 996 | Adding 5/5 app layer(s) 997 | Adding layer 'launcher' 998 | Adding layer 'config' 999 | Adding layer 'process-types' 1000 | Adding label 'io.buildpacks.lifecycle.metadata' 1001 | Adding label 'io.buildpacks.build.metadata' 1002 | Adding label 'io.buildpacks.project.metadata' 1003 | Adding label 'org.opencontainers.image.title' 1004 | Adding label 'org.opencontainers.image.version' 1005 | Adding label 'org.springframework.boot.version' 1006 | Setting default process type 'web' 1007 | Saving myorg/myapp... 1008 | *** Images (ed1f92885df0): 1009 | myorg/myapp 1010 | Adding cache layer 'paketo-buildpacks/bellsoft-liberica:jdk' 1011 | Adding cache layer 'paketo-buildpacks/maven:application' 1012 | Adding cache layer 'paketo-buildpacks/maven:cache' 1013 | Successfully built image 'myorg/myapp' 1014 | ---- 1015 | ==== 1016 | 1017 | The `--builder` is a Docker image that runs the buildpack lifecycle. Typically, it would be a shared resource for all developers or all developers on a single platform. You can set the default builder on the command line (creates a file in `~/.pack`) and then omit that flag from subsequent builds. 1018 | 1019 | NOTE: The `paketobuildpacks/builder:base` builder also knows how to build an image from an executable JAR file, so you can build using Maven first and then point the `--path` to the JAR file for the same result. 1020 | 1021 | == Knative 1022 | 1023 | Another new project in the container and platform space is https://cloud.google.com/knative/[Knative]. If you are not familiar with it, you can think of it as a building block for building a serverless platform. It is built on https://kubernetes.io[Kubernetes], so, ultimately, it consumes container images and turns them into applications or "`services`" on the platform. One of the main features it has, though, is the ability to consume source code and build the container for you, making it more developer- and operator-friendly. https://github.com/knative/build[Knative Build] is the component that does this and is itself a flexible platform for transforming user code into containers -- you can do it in pretty much any way you like. Some templates are provided with common patterns (such as Maven and Gradle builds) and multi-stage docker builds using https://github.com/GoogleContainerTools/kaniko[Kaniko]. There is also a template that uses https://github.com/knative/build-templates/tree/master/buildpacks[Buildpacks], which is interesting for us, since buildpacks have always had good support for Spring Boot. 1024 | 1025 | == Closing 1026 | 1027 | This guide has presented a lot of options for building container images for Spring Boot applications. All of them are completely valid choices, and it is now up to you to decide which one you need. Your first question should be "`Do I really need to build a container image?`" If the answer is "`yes,`" then your choices are likely to be driven by efficiency, cacheability, and by separation of concerns. Do you want to insulate developers from needing to know too much about how container images are created? Do you want to make developers responsible for updating images when operating system and middleware vulnerabilities need to be patched? Or maybe developers need complete control over the whole process and they have all the tools and knowledge they need. 1028 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | -------------------------------------------------------------------------------- /demo/.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | import java.io.File; 21 | import java.io.FileInputStream; 22 | import java.io.FileOutputStream; 23 | import java.io.IOException; 24 | import java.net.URL; 25 | import java.nio.channels.Channels; 26 | import java.nio.channels.ReadableByteChannel; 27 | import java.util.Properties; 28 | 29 | public class MavenWrapperDownloader { 30 | 31 | /** 32 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 33 | */ 34 | private static final String DEFAULT_DOWNLOAD_URL = 35 | "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; 36 | 37 | /** 38 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 39 | * use instead of the default one. 40 | */ 41 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 42 | ".mvn/wrapper/maven-wrapper.properties"; 43 | 44 | /** 45 | * Path where the maven-wrapper.jar will be saved to. 46 | */ 47 | private static final String MAVEN_WRAPPER_JAR_PATH = 48 | ".mvn/wrapper/maven-wrapper.jar"; 49 | 50 | /** 51 | * Name of the property which should be used to override the default download url for the wrapper. 52 | */ 53 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 54 | 55 | public static void main(String args[]) { 56 | System.out.println("- Downloader started"); 57 | File baseDirectory = new File(args[0]); 58 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 59 | 60 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 61 | // wrapperUrl parameter. 62 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 63 | String url = DEFAULT_DOWNLOAD_URL; 64 | if(mavenWrapperPropertyFile.exists()) { 65 | FileInputStream mavenWrapperPropertyFileInputStream = null; 66 | try { 67 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 68 | Properties mavenWrapperProperties = new Properties(); 69 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 70 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 71 | } catch (IOException e) { 72 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 73 | } finally { 74 | try { 75 | if(mavenWrapperPropertyFileInputStream != null) { 76 | mavenWrapperPropertyFileInputStream.close(); 77 | } 78 | } catch (IOException e) { 79 | // Ignore ... 80 | } 81 | } 82 | } 83 | System.out.println("- Downloading from: : " + url); 84 | 85 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 86 | if(!outputFile.getParentFile().exists()) { 87 | if(!outputFile.getParentFile().mkdirs()) { 88 | System.out.println( 89 | "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 90 | } 91 | } 92 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 93 | try { 94 | downloadFileFromURL(url, outputFile); 95 | System.out.println("Done"); 96 | System.exit(0); 97 | } catch (Throwable e) { 98 | System.out.println("- Error downloading"); 99 | e.printStackTrace(); 100 | System.exit(1); 101 | } 102 | } 103 | 104 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 105 | URL website = new URL(urlString); 106 | ReadableByteChannel rbc; 107 | rbc = Channels.newChannel(website.openStream()); 108 | FileOutputStream fos = new FileOutputStream(destination); 109 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 110 | fos.close(); 111 | rbc.close(); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /demo/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-attic/top-spring-boot-docker/8a5cde1dcb0d40854bc07742bba5be915fa77a15/demo/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /demo/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.0/apache-maven-3.6.0-bin.zip 2 | -------------------------------------------------------------------------------- /demo/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:experimental 2 | FROM eclipse-temurin:17-jdk-alpine as build 3 | WORKDIR /workspace/app 4 | 5 | COPY mvnw . 6 | COPY .mvn .mvn 7 | COPY pom.xml . 8 | COPY src src 9 | RUN --mount=type=cache,target=/root/.m2 ./mvnw install -DskipTests 10 | 11 | ARG JAR_FILE=target/*.jar 12 | COPY ${JAR_FILE} target/application.jar 13 | RUN java -Djarmode=layertools -jar target/application.jar extract --destination target/extracted 14 | 15 | FROM eclipse-temurin:17-jdk-alpine 16 | RUN addgroup -S demo && adduser -S demo -G demo 17 | VOLUME /tmp 18 | USER demo 19 | ARG EXTRACTED=/workspace/app/target/extracted 20 | WORKDIR application 21 | COPY --from=build ${EXTRACTED}/dependencies/ ./ 22 | COPY --from=build ${EXTRACTED}/spring-boot-loader/ ./ 23 | COPY --from=build ${EXTRACTED}/snapshot-dependencies/ ./ 24 | COPY --from=build ${EXTRACTED}/application/ ./ 25 | ENTRYPOINT ["java","-noverify","-XX:TieredStopAtLevel=1","-Dspring.main.lazy-initialization=true","org.springframework.boot.loader.JarLauncher"] 26 | -------------------------------------------------------------------------------- /demo/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | # TODO classpath? 118 | fi 119 | 120 | if [ -z "$JAVA_HOME" ]; then 121 | javaExecutable="`which javac`" 122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 123 | # readlink(1) is not available as standard on Solaris 10. 124 | readLink=`which readlink` 125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 126 | if $darwin ; then 127 | javaHome="`dirname \"$javaExecutable\"`" 128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 129 | else 130 | javaExecutable="`readlink -f \"$javaExecutable\"`" 131 | fi 132 | javaHome="`dirname \"$javaExecutable\"`" 133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 134 | JAVA_HOME="$javaHome" 135 | export JAVA_HOME 136 | fi 137 | fi 138 | fi 139 | 140 | if [ -z "$JAVACMD" ] ; then 141 | if [ -n "$JAVA_HOME" ] ; then 142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 143 | # IBM's JDK on AIX uses strange locations for the executables 144 | JAVACMD="$JAVA_HOME/jre/sh/java" 145 | else 146 | JAVACMD="$JAVA_HOME/bin/java" 147 | fi 148 | else 149 | JAVACMD="`which java`" 150 | fi 151 | fi 152 | 153 | if [ ! -x "$JAVACMD" ] ; then 154 | echo "Error: JAVA_HOME is not defined correctly." >&2 155 | echo " We cannot execute $JAVACMD" >&2 156 | exit 1 157 | fi 158 | 159 | if [ -z "$JAVA_HOME" ] ; then 160 | echo "Warning: JAVA_HOME environment variable is not set." 161 | fi 162 | 163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 164 | 165 | # traverses directory structure from process work directory to filesystem root 166 | # first directory with .mvn subdirectory is considered project base directory 167 | find_maven_basedir() { 168 | 169 | if [ -z "$1" ] 170 | then 171 | echo "Path not specified to find_maven_basedir" 172 | return 1 173 | fi 174 | 175 | basedir="$1" 176 | wdir="$1" 177 | while [ "$wdir" != '/' ] ; do 178 | if [ -d "$wdir"/.mvn ] ; then 179 | basedir=$wdir 180 | break 181 | fi 182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 183 | if [ -d "${wdir}" ]; then 184 | wdir=`cd "$wdir/.."; pwd` 185 | fi 186 | # end of workaround 187 | done 188 | echo "${basedir}" 189 | } 190 | 191 | # concatenates all lines of a file 192 | concat_lines() { 193 | if [ -f "$1" ]; then 194 | echo "$(tr -s '\n' ' ' < "$1")" 195 | fi 196 | } 197 | 198 | BASE_DIR=`find_maven_basedir "$(pwd)"` 199 | if [ -z "$BASE_DIR" ]; then 200 | exit 1; 201 | fi 202 | 203 | ########################################################################################## 204 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 205 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 206 | ########################################################################################## 207 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 208 | if [ "$MVNW_VERBOSE" = true ]; then 209 | echo "Found .mvn/wrapper/maven-wrapper.jar" 210 | fi 211 | else 212 | if [ "$MVNW_VERBOSE" = true ]; then 213 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 214 | fi 215 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" 216 | while IFS="=" read key value; do 217 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 218 | esac 219 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 220 | if [ "$MVNW_VERBOSE" = true ]; then 221 | echo "Downloading from: $jarUrl" 222 | fi 223 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 224 | 225 | if command -v wget > /dev/null; then 226 | if [ "$MVNW_VERBOSE" = true ]; then 227 | echo "Found wget ... using wget" 228 | fi 229 | wget "$jarUrl" -O "$wrapperJarPath" 230 | elif command -v curl > /dev/null; then 231 | if [ "$MVNW_VERBOSE" = true ]; then 232 | echo "Found curl ... using curl" 233 | fi 234 | curl -o "$wrapperJarPath" "$jarUrl" 235 | else 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Falling back to using Java to download" 238 | fi 239 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 240 | if [ -e "$javaClass" ]; then 241 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 242 | if [ "$MVNW_VERBOSE" = true ]; then 243 | echo " - Compiling MavenWrapperDownloader.java ..." 244 | fi 245 | # Compiling the Java class 246 | ("$JAVA_HOME/bin/javac" "$javaClass") 247 | fi 248 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 249 | # Running the downloader 250 | if [ "$MVNW_VERBOSE" = true ]; then 251 | echo " - Running MavenWrapperDownloader.java ..." 252 | fi 253 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 254 | fi 255 | fi 256 | fi 257 | fi 258 | ########################################################################################## 259 | # End of extension 260 | ########################################################################################## 261 | 262 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 263 | if [ "$MVNW_VERBOSE" = true ]; then 264 | echo $MAVEN_PROJECTBASEDIR 265 | fi 266 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 267 | 268 | # For Cygwin, switch paths to Windows format before running java 269 | if $cygwin; then 270 | [ -n "$M2_HOME" ] && 271 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 272 | [ -n "$JAVA_HOME" ] && 273 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 274 | [ -n "$CLASSPATH" ] && 275 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 276 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 277 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 278 | fi 279 | 280 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 281 | 282 | exec "$JAVACMD" \ 283 | $MAVEN_OPTS \ 284 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 285 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 286 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 287 | -------------------------------------------------------------------------------- /demo/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" 124 | FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( 125 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | echo Found %WRAPPER_JAR% 132 | ) else ( 133 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 134 | echo Downloading from: %DOWNLOAD_URL% 135 | powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" 136 | echo Finished downloading %WRAPPER_JAR% 137 | ) 138 | @REM End of extension 139 | 140 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 141 | if ERRORLEVEL 1 goto error 142 | goto end 143 | 144 | :error 145 | set ERROR_CODE=1 146 | 147 | :end 148 | @endlocal & set ERROR_CODE=%ERROR_CODE% 149 | 150 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 151 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 152 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 153 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 154 | :skipRcPost 155 | 156 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 157 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 158 | 159 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 160 | 161 | exit /B %ERROR_CODE% 162 | -------------------------------------------------------------------------------- /demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.7.4 9 | 10 | 11 | com.example 12 | docker-demo 13 | 0.0.1-SNAPSHOT 14 | docker-demo 15 | Demo project for Spring Boot 16 | 17 | 18 | 17 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-webflux 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-actuator 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-test 34 | test 35 | 36 | 37 | io.projectreactor 38 | reactor-test 39 | test 40 | 41 | 42 | 43 | 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-maven-plugin 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /demo/src/main/java/com/example/demo/DemoApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.demo; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | @SpringBootApplication 9 | @RestController 10 | public class DemoApplication { 11 | 12 | @GetMapping("/") 13 | public String home() { 14 | return "Hello World!"; 15 | } 16 | 17 | public static void main(String[] args) { 18 | SpringApplication.run(DemoApplication.class, args); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /demo/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /demo/src/test/java/com/example/demo/DemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example.demo; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | @SpringBootTest 8 | public class DemoApplicationTests { 9 | 10 | @Test 11 | public void contextLoads() { 12 | } 13 | 14 | } 15 | --------------------------------------------------------------------------------