├── .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 |
--------------------------------------------------------------------------------