├── .env ├── .github ├── dependabot.yml └── workflows │ ├── docker-image.yml │ └── maven.yml ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── hooks └── pre-commit ├── images └── spring-boot-admin-dashboard.png ├── justfile ├── mvnw ├── mvnw.cmd ├── pom.xml ├── spotbugs-exclude.xml ├── spotbugs-include.xml ├── src ├── main │ ├── java │ │ └── com │ │ │ └── miguno │ │ │ └── javadockerbuild │ │ │ ├── App.java │ │ │ ├── controllers │ │ │ ├── RootController.java │ │ │ └── WelcomeController.java │ │ │ ├── models │ │ │ └── Welcome.java │ │ │ └── security │ │ │ ├── AppSecurityConfiguration.java │ │ │ └── CustomCsrfFilter.java │ └── resources │ │ ├── application.properties │ │ └── templates │ │ └── .keep └── test │ └── java │ └── com │ └── miguno │ └── javadockerbuild │ ├── SmokeTest.java │ └── controllers │ ├── WelcomeControllerIT.java │ └── WelcomeControllerTest.java └── tools ├── create_image.sh └── start_container.sh /.env: -------------------------------------------------------------------------------- 1 | # .env 2 | # 3 | # This file pre-defines environment variables and is read by the `*.sh` scripts 4 | # and by `just`. 5 | # 6 | # If needed, you can override the variables defined here when invoking `just` 7 | # by manually setting the variable in the shell environment. For example, the 8 | # command below sets `DOCKER_IMAGE_NAME` to "foo/bar": 9 | # 10 | # $ DOCKER_IMAGE_NAME="foo/bar" just ... 11 | # 12 | 13 | ### App-related settings 14 | # APP_PORT must match `server.port` setting in `application.properties`. 15 | APP_PORT=8123 16 | 17 | ### Docker-related settings 18 | DOCKER_IMAGE_NAME="miguno/java-docker-build-tutorial" 19 | DOCKER_IMAGE_TAG="latest" 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem 9 | - package-ecosystem: "maven" # See documentation for possible values 10 | directory: "/" # Location of package manifests 11 | schedule: 12 | interval: "daily" 13 | ignore: 14 | # Ignore Maven APIs/SPIs. 15 | - dependency-name: org.apache.maven:* 16 | - package-ecosystem: "github-actions" 17 | directory: "/" 18 | schedule: 19 | interval: "daily" 20 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | # To cancel a currently running workflow from the same PR, branch, or tag 10 | # when a new workflow is triggered. 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | 17 | build: 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | - name: Build the Docker image 26 | run: docker buildx build . --file Dockerfile --tag miguno/java-docker-build-tutorial:$(date +%s) 27 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Java CI with Maven 10 | 11 | on: 12 | push: 13 | branches: ["main"] 14 | pull_request: 15 | branches: ["main"] 16 | 17 | # To cancel a currently running workflow from the same PR, branch, or tag 18 | # when a new workflow is triggered. 19 | concurrency: 20 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | build: 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | 32 | - name: Set up JDK 23 33 | uses: actions/setup-java@v4 # https://github.com/actions/setup-java 34 | with: 35 | java-version: '23' 36 | distribution: 'temurin' 37 | cache: maven 38 | 39 | - name: Print Java version 40 | run: java -version 41 | 42 | - name: Verify and Package with Maven 43 | run: ./mvnw --batch-mode --file pom.xml verify package 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dependency-reduced-pom.xml 2 | infer-out/ 3 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "java.configuration.updateBuildConfiguration": "interactive" 3 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # We use a multi-stage build setup. 4 | # (https://docs.docker.com/build/building/multi-stage/) 5 | 6 | ############################################################################### 7 | # Stage 1 of 2 (to create a "build" image) # 8 | ############################################################################### 9 | # https://hub.docker.com/_/eclipse-temurin 10 | FROM eclipse-temurin:23-jdk-alpine AS builder 11 | 12 | # Smoke test to verify if java is available. 13 | RUN java -version 14 | 15 | ### Build a downsized JRE 16 | # Required for jlink's `--strip-debug` option. 17 | RUN apk add --no-cache binutils 18 | RUN jlink \ 19 | --verbose \ 20 | --add-modules ALL-MODULE-PATH \ 21 | --compress=2 \ 22 | --no-header-files \ 23 | --no-man-pages \ 24 | --strip-debug \ 25 | --output /minimal-jre 26 | 27 | # Build and package the app. 28 | COPY . /usr/src/myapp/ 29 | WORKDIR /usr/src/myapp/ 30 | RUN ./mvnw package 31 | 32 | ############################################################################### 33 | # Stage 2 of 2 (to create a downsized "container executable", ~161MB) # 34 | ############################################################################### 35 | # https://hub.docker.com/_/alpine 36 | FROM alpine:latest 37 | ENV JAVA_HOME=/jre 38 | ENV PATH="${JAVA_HOME}/bin:${PATH}" 39 | RUN apk --no-cache add ca-certificates 40 | 41 | # Add app user. 42 | ARG USER_NAME="appuser" 43 | ARG USER_ID="1000" 44 | ARG GROUP_NAME="apps" 45 | ARG GROUP_ID="1000" 46 | RUN addgroup --gid $GROUP_ID $GROUP_NAME && \ 47 | adduser --no-create-home --disabled-password --ingroup $GROUP_NAME --uid $USER_ID $USER_NAME 48 | 49 | # Configure work directory. 50 | ARG APP_DIR=/app 51 | RUN mkdir $APP_DIR && \ 52 | chown -R $USER_NAME:$GROUP_NAME $APP_DIR 53 | WORKDIR $APP_DIR 54 | 55 | # Copy downsized JRE from builder image. 56 | COPY --from=builder /minimal-jre $JAVA_HOME 57 | 58 | # Copy packaged app from builder image. 59 | COPY --from=builder --chown=$USER_NAME:$GROUP_NAME /usr/src/myapp/target/app.jar ./app.jar 60 | 61 | # Run the application. 62 | USER $USER_NAME:$GROUP_NAME 63 | EXPOSE 8123 64 | ENTRYPOINT ["java", "-XX:+UseZGC", "-XX:+ZGenerational", "-jar", "./app.jar"] 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project Template: Create a Docker image for a Java application 2 | 3 | [![GitHub forks](https://img.shields.io/github/forks/miguno/java-docker-build-tutorial)](https://github.com/miguno/java-docker-build-tutorial/fork) 4 | [![Docker workflow status](https://github.com/miguno/java-docker-build-tutorial/actions/workflows/docker-image.yml/badge.svg)](https://github.com/miguno/java-docker-build-tutorial/actions/workflows/docker-image.yml) 5 | [![Maven workflow status](https://github.com/miguno/java-docker-build-tutorial/actions/workflows/maven.yml/badge.svg)](https://github.com/miguno/java-docker-build-tutorial/actions/workflows/maven.yml) 6 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 7 | 8 | A template project to create a Docker image for a Java application. 9 | The [example application](src/main/java/com/miguno/javadockerbuild/App.java) 10 | uses Spring Boot to expose an HTTP endpoint at 11 | [`/welcome`](http://localhost:8123/welcome). 12 | 13 | > [!TIP] 14 | > 15 | > **Golang developer?** Check out https://github.com/miguno/golang-docker-build-tutorial 16 | 17 | Features: 18 | 19 | - The Docker build uses a 20 | [multi-stage build setup](https://docs.docker.com/build/building/multi-stage/) 21 | including a downsized JRE (built inside Docker via `jlink`) 22 | to minimize the size of the generated Docker image, which is **161MB**. 23 | - Supports [Docker BuildKit](https://docs.docker.com/build/) 24 | - Java 23 (Eclipse Temurin) with the [generational ZGC garbage 25 | collector](https://docs.oracle.com/en/java/javase/21/gctuning/z-garbage-collector.html) 26 | - [JUnit 5](https://github.com/junit-team/junit5) for testing, 27 | [Jacoco](https://github.com/jacoco/jacoco) for code coverage, 28 | [SpotBugs](https://github.com/spotbugs/spotbugs) for static code analysis 29 | - Swagger UI and OpenAPI v3 integration via [springdoc](https://springdoc.org/) 30 | at endpoints [/swagger-ui.html](http://localhost:8123/swagger-ui.html) and 31 | and [/v3/api-docs](http://localhost:8123/v3/api-docs) 32 | - [Spring Actuator](https://docs.spring.io/spring-boot/reference/actuator/endpoints.html) 33 | at endpoint [/actuator](http://localhost:8123/actuator), e.g. for 34 | [healthchecks](http://localhost:8123/actuator/health) or [Prometheus 35 | metrics](http://localhost:8123/actuator/prometheus) 36 | - Maven for build management (see [pom.xml](pom.xml)), using 37 | [Maven Wrapper](https://github.com/apache/maven-wrapper) 38 | - [GitHub Actions workflows](https://github.com/miguno/java-docker-build-tutorial/actions) for 39 | [Maven](https://github.com/miguno/java-docker-build-tutorial/actions/workflows/maven.yml) 40 | and 41 | [Docker](https://github.com/miguno/java-docker-build-tutorial/actions/workflows/docker-image.yml) 42 | - Optionally, uses 43 | [just](https://github.com/casey/just) 44 | ![](https://img.shields.io/github/stars/casey/just) 45 | for running common commands conveniently, see [justfile](justfile). 46 | - Uses [.env](.env) as central configuration to set variables used by 47 | [justfile](justfile) and other helper scripts in this project. 48 | 49 | # Requirements 50 | 51 | Docker must be installed on your local machine. That's it. You do not need a 52 | Java JDK or Maven installed. 53 | 54 | # Usage and Demo 55 | 56 | **Step 1:** Create the Docker image according to [Dockerfile](Dockerfile). 57 | This step uses Maven to build, test, and package the Java application according 58 | to [pom.xml](pom.xml). The resulting image is 161MB in size, of which 44MB are 59 | the underlying `alpine` image. 60 | 61 | ```shell 62 | # ***Creating an image may take a few minutes!*** 63 | $ docker build --platform linux/x86_64/v8 -t miguno/java-docker-build-tutorial:latest . 64 | 65 | # You can also build with the new BuildKit. 66 | # https://docs.docker.com/build/ 67 | $ docker buildx build --platform linux/x86_64/v8 -t miguno/java-docker-build-tutorial:latest . 68 | ``` 69 | 70 | Optionally, you can check the size of the generated Docker image: 71 | 72 | ```shell 73 | $ docker images miguno/java-docker-build-tutorial 74 | REPOSITORY TAG IMAGE ID CREATED SIZE 75 | miguno/java-docker-build-tutorial latest bd64d898a04e 2 minutes ago 131MB 76 | ``` 77 | 78 | **Step 2:** Start a container for the Docker image. 79 | 80 | ```shell 81 | $ docker run -p 8123:8123 miguno/java-docker-build-tutorial:latest 82 | ``` 83 | 84 |
85 | Example output (click to expand) 86 | 87 | ``` 88 | Running container from docker image ... 89 | Starting container for image 'miguno/java-docker-build-tutorial:latest', exposing port 8123/tcp 90 | - Run 'curl http://localhost:8123/welcome' to send a test request to the containerized app. 91 | - Enter Ctrl-C to stop the container. 92 | 93 | . ____ _ __ _ _ 94 | /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ 95 | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ 96 | \\/ ___)| |_)| | | | | || (_| | ) ) ) ) 97 | ' |____| .__|_| |_|_| |_\__, | / / / / 98 | =========|_|==============|___/=/_/_/_/ 99 | 100 | :: Spring Boot :: (v3.3.3) 101 | 102 | 2024-08-26T15:45:08.859Z INFO 1 --- [main] com.miguno.javadockerbuild.App : Starting App v1.0.0-SNAPSHOT using Java 22.0.2 with PID 1 (/app/app.jar started by appuser in /app) 103 | 2024-08-26T15:45:08.868Z INFO 1 --- [main] com.miguno.javadockerbuild.App : No active profile set, falling back to 1 default profile: "default" 104 | 2024-08-26T15:45:10.930Z INFO 1 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8123 (http) 105 | 2024-08-26T15:45:10.950Z INFO 1 --- [main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 106 | 2024-08-26T15:45:10.951Z INFO 1 --- [main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.28] 107 | 2024-08-26T15:45:10.991Z INFO 1 --- [main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 108 | 2024-08-26T15:45:10.992Z INFO 1 --- [main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2004 ms 109 | 2024-08-26T15:45:12.452Z INFO 1 --- [main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 1 endpoint beneath base path '/actuator' 110 | 2024-08-26T15:45:12.562Z INFO 1 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8123 (http) with context path '/' 111 | 2024-08-26T15:45:12.597Z INFO 1 --- [main] com.miguno.javadockerbuild.App : Started App in 5.0 seconds (process running for 6.246) 112 | ``` 113 | 114 |
115 | 116 | **Step 3:** Open another terminal and access the example API endpoint of the 117 | running container. 118 | 119 | ```shell 120 | $ curl http://localhost:8123/welcome 121 | {"welcome":"Hello, World!"} 122 | ``` 123 | 124 | # Local usage without Docker 125 | 126 | You can also build, test, package, and run the Java application locally 127 | (without Docker) if you have JDK 22+ installed. You do not need to have Maven 128 | installed, because this repository contains the 129 | [Maven Wrapper](https://github.com/apache/maven-wrapper) `mvnw` (use `mvnw.cmd` 130 | on Windows). 131 | 132 | ```shell 133 | # Build, test, package the application locally. 134 | $ ./mvnw clean verify package 135 | 136 | # Run the application locally. 137 | $ ./mvnw spring-boot:run -Dspring-boot.run.jvmArguments="-XX:+UseZGC -XX:+ZGenerational" 138 | 139 | # Alternatively, run the application locally via its jar file. 140 | $ java -XX:+UseZGC -XX:+ZGenerational -jar target/app.jar 141 | ``` 142 | 143 | # Appendix 144 | 145 | ## Hot reloading during development 146 | 147 | This project uses 148 | [spring-boot-devtools](https://docs.spring.io/spring-boot/reference/using/devtools.html) 149 | for fast, automatic application 150 | [restarts](https://docs.spring.io/spring-boot/reference/using/devtools.html#using.devtools.restart) 151 | after code changes. 152 | 153 | - Restarts will be triggered whenever files in the classpath changed, e.g., 154 | after you ran `./mvnw compile` or after you re-built the project in your IDE. 155 | - This feature works both when running the application inside an IDE like 156 | IntelliJ IDEA as well as when running the application in a terminal with 157 | `./mvnw spring-boot:run`. 158 | - Be patient. After a file changed, it may take a few seconds for the refresh 159 | to happen. 160 | 161 | In IntelliJ IDEA, you can also enable automatic project builds for even more 162 | convenience, using the following settings. Then, whenever you modify a source 163 | file, IDEA will automatically rebuild the project in the background and thus 164 | trigger an automatic restart: 165 | 166 | - `Settings` > `Build, Execution, Deployment` > `Compiler`: 167 | [X] Build project automatically 168 | - `Settings` > `Advanced Settings`: 169 | [X] Allow auto-make to start even if developed application is currently running 170 | 171 | **Restart vs. Reload:** If you want true 172 | [hot reloads](https://docs.spring.io/spring-boot/reference/using/devtools.html#using.devtools.restart.restart-vs-reload) 173 | that are even faster than automatic restarts, look at tools like 174 | [JRebel](https://jrebel.com/software/jrebel/). 175 | 176 | ## Usage with just 177 | 178 | If you have [just](https://github.com/casey/just) installed, you can run the 179 | commands above more conveniently as per this project's [justfile](justfile): 180 | 181 | ```shell 182 | $ just 183 | Available recipes: 184 | [benchmarking] 185 | benchmark-plow # benchmark the app's HTTP endpoint with plow (requires https://github.com/six-ddc/plow) 186 | benchmark-wrk # benchmark the app's HTTP endpoint with wrk (requires https://github.com/wg/wrk) 187 | 188 | [development] 189 | analyze # perform static code analysis 190 | build # alias for 'compile' 191 | clean # clean (remove) the build artifacts 192 | compile # compile the project 193 | coverage # create coverage report 194 | dependencies # list dependency tree of this project 195 | docs # generate Java documentation 196 | format # format sources 197 | format-check # check formatting of sources (without modifying) 198 | infer # static code analysis with infer (requires https://github.com/facebook/infer) 199 | outdated # list outdated dependencies 200 | outdated-plugins # list outdated maven plugins 201 | package # package the app to create an uber jar 202 | send-request-to-app # send request to the app's HTTP endpoint (requires running app) 203 | site # generate site incl. reports for spotbugs, dependencies, javadocs, licenses 204 | spotbugs # static code analysis with spotbugs 205 | start # start the app 206 | start-jar # start the app via its packaged jar (requires 'package' step) 207 | test # run unit tests 208 | verify # run unit and integration tests, coverage check, static code analysis 209 | 210 | [docker] 211 | docker-image-create # create a docker image (requires Docker) 212 | docker-image-run # run the docker image (requires Docker) 213 | docker-image-size # size of the docker image (requires Docker) 214 | 215 | [maven] 216 | maven-active-profiles # list active profiles 217 | maven-all-profiles # list all profiles 218 | maven-help # show help of maven-help-plugin 219 | maven-lifecycles # show maven lifecycles like 'clean', 'compile' 220 | maven-pom # print effective pom.xml 221 | maven-system # print platform details like system properties, env variables 222 | mvnw-upgrade # upgrade maven wrapper 223 | 224 | [project-agnostic] 225 | default # print available targets 226 | evaluate # evaluate and print all just variables 227 | system-info # print system information such as OS and architecture 228 | ``` 229 | 230 | Example: 231 | 232 | ```shell 233 | $ just docker-image-create 234 | ``` 235 | 236 | # References 237 | 238 | - [How to reduce Java Docker image size](https://blog.monosoul.dev/2022/04/25/reduce-java-docker-image-size/) 239 | (with `jlink`) 240 | - [Creating your own runtime using jlink](https://adoptium.net/blog/2021/10/jlink-to-produce-own-runtime/) 241 | - [Using Jlink in Dockerfiles instead of a JRE](https://adoptium.net/blog/2021/08/using-jlink-in-dockerfiles/) 242 | -------------------------------------------------------------------------------- /hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run spotless checks on staged files only. 4 | # This is faster than running spotless on all files. 5 | ./mvnw -q spotless:check -DspotlessFiles="$(git diff --staged --name-only | grep ".java$" | sed 's/^/.*/' | paste -sd ',' -)" 6 | 7 | declare errcode=$? 8 | if [ $errcode -ne 0 ]; then 9 | echo 10 | echo "Run \`./mvnw spotless:apply\` to automatically fix these format violations." 11 | exit $errcode 12 | fi 13 | -------------------------------------------------------------------------------- /images/spring-boot-admin-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miguno/java-docker-build-tutorial/d884d121ae5d6eb7ebfcab38263bac6fd4065e4c/images/spring-boot-admin-dashboard.png -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | # This justfile requires https://github.com/casey/just 2 | 3 | # Load environment variables from `.env` file. 4 | set dotenv-load 5 | # Fail the script if the env file is not found. 6 | set dotenv-required 7 | 8 | project_dir := justfile_directory() 9 | build_dir := project_dir + "/target" 10 | app_uber_jar := build_dir + "/app.jar" 11 | 12 | # print available targets 13 | [group("project-agnostic")] 14 | default: 15 | @just --list --justfile {{justfile()}} 16 | 17 | # evaluate and print all just variables 18 | [group("project-agnostic")] 19 | evaluate: 20 | @just --evaluate 21 | 22 | # print system information such as OS and architecture 23 | [group("project-agnostic")] 24 | system-info: 25 | @echo "architecture: {{arch()}}" 26 | @echo "os: {{os()}}" 27 | @echo "os family: {{os_family()}}" 28 | 29 | # perform static code analysis 30 | [group("development")] 31 | analyze: 32 | #!/usr/bin/env bash 33 | echo "Running static code analysis with spotbugs" 34 | just spotbugs 35 | if command -v infer &>/dev/null; then 36 | echo "Running static code analysis with infer" 37 | just infer 38 | fi 39 | 40 | # benchmark the app's HTTP endpoint with plow (requires https://github.com/six-ddc/plow) 41 | [group("benchmarking")] 42 | benchmark-plow: 43 | @echo plow -c 100 --duration=30s http://localhost:${APP_PORT}/welcome 44 | @plow -c 100 --duration=30s http://localhost:${APP_PORT}/welcome 45 | 46 | # benchmark the app's HTTP endpoint with wrk (requires https://github.com/wg/wrk) 47 | [group("benchmarking")] 48 | benchmark-wrk: 49 | @echo wrk -t 10 -c 100 --latency --duration 30 http://localhost:${APP_PORT}/welcome 50 | @wrk -t 10 -c 100 --latency --duration 30 http://localhost:${APP_PORT}/welcome 51 | 52 | # alias for 'compile' 53 | [group("development")] 54 | build: compile 55 | 56 | # clean (remove) the build artifacts 57 | [group("development")] 58 | clean: 59 | @./mvnw clean 60 | 61 | # compile the project 62 | [group("development")] 63 | compile: 64 | @./mvnw compile 65 | 66 | # create coverage report 67 | [group("development")] 68 | coverage: verify 69 | @./mvnw jacoco:report && \ 70 | echo "Coverage report is available under {{build_dir}}/site/jacoco/" 71 | 72 | # list dependency tree of this project 73 | [group("development")] 74 | dependencies: 75 | @./mvnw dependency:tree 76 | 77 | # create a docker image (requires Docker) 78 | [group("docker")] 79 | docker-image-create: 80 | @echo "Creating a docker image ..." 81 | @./tools/create_image.sh 82 | 83 | # size of the docker image (requires Docker) 84 | [group("docker")] 85 | docker-image-size: 86 | @docker images $DOCKER_IMAGE_NAME 87 | 88 | # run the docker image (requires Docker) 89 | [group("docker")] 90 | docker-image-run: 91 | @echo "Running container from docker image ..." 92 | @./tools/start_container.sh 93 | 94 | # generate Java documentation 95 | [group("development")] 96 | docs: 97 | @./mvnw javadoc:javadoc 98 | 99 | # format sources 100 | [group("development")] 101 | format: 102 | @./mvnw spotless:apply 103 | 104 | # check formatting of sources (without modifying) 105 | [group("development")] 106 | format-check: 107 | @./mvnw spotless:check 108 | 109 | # static code analysis with infer (requires https://github.com/facebook/infer) 110 | [group("development")] 111 | infer: 112 | @infer run -- ./mvnw clean compile 113 | 114 | # list active profiles 115 | [group("maven")] 116 | maven-active-profiles: 117 | @./mvnw help:active-profiles 118 | 119 | # list all profiles 120 | [group("maven")] 121 | maven-all-profiles: 122 | @./mvnw help:all-profiles 123 | 124 | # show maven lifecycles like 'clean', 'compile' 125 | [group("maven")] 126 | maven-lifecycles: 127 | @echo "See https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html#Lifecycle_Reference" 128 | 129 | # print effective pom.xml 130 | [group("maven")] 131 | maven-pom: 132 | @./mvnw help:effective-pom 133 | 134 | # show help of maven-help-plugin 135 | [group("maven")] 136 | maven-help: 137 | @./mvnw help:help 138 | 139 | # print platform details like system properties, env variables 140 | [group("maven")] 141 | maven-system: 142 | @./mvnw help:system 143 | 144 | # upgrade maven wrapper 145 | [group("maven")] 146 | mvnw-upgrade: 147 | @./mvnw wrapper:wrapper 148 | 149 | # list outdated dependencies 150 | [group("development")] 151 | outdated: 152 | @./mvnw versions:display-dependency-updates 153 | 154 | # list outdated maven plugins 155 | [group("development")] 156 | outdated-plugins: 157 | @./mvnw versions:display-plugin-updates 158 | 159 | # package the app to create an uber jar 160 | [group("development")] 161 | package: 162 | @./mvnw verify package 163 | 164 | # send request to the app's HTTP endpoint (requires running app) 165 | [group("development")] 166 | send-request-to-app: 167 | @echo curl http://localhost:${APP_PORT}/welcome 168 | @curl http://localhost:${APP_PORT}/welcome 169 | 170 | # generate site incl. reports for spotbugs, dependencies, javadocs, licenses 171 | [group("development")] 172 | site: compile 173 | @./mvnw site && \ 174 | echo "Reports are available under {{build_dir}}/site/" && \ 175 | echo "Javadocs are available under {{build_dir}}/site/apidocs/" 176 | 177 | # static code analysis with spotbugs 178 | [group("development")] 179 | spotbugs: compile 180 | @./mvnw spotbugs:check 181 | 182 | # start the app 183 | [group("development")] 184 | start: 185 | #!/usr/bin/env bash 186 | declare -r JVM_ARGS="-XX:+UseZGC -XX:+ZGenerational" 187 | ./mvnw spring-boot:run -Dspring-boot.run.jvmArguments="$JVM_ARGS" 188 | 189 | # start the app via its packaged jar (requires 'package' step) 190 | [group("development")] 191 | start-jar: 192 | #!/usr/bin/env bash 193 | APP_JAR="{{app_uber_jar}}" 194 | if [ ! -f "$APP_JAR" ]; then 195 | just package 196 | else 197 | echo "Using existing application uber jar at $APP_JAR." 198 | echo "If you want to recompile the uber jar, run \`./mvnw package\` (or \`just package\`) manually." 199 | fi 200 | declare -r JVM_ARGS="-XX:+UseZGC -XX:+ZGenerational" 201 | java $JVM_ARGS -jar "$APP_JAR" 202 | 203 | # run unit tests 204 | [group("development")] 205 | test: 206 | @./mvnw test 207 | 208 | # run unit and integration tests, coverage check, static code analysis 209 | [group("development")] 210 | verify: 211 | @./mvnw verify 212 | 213 | -------------------------------------------------------------------------------- /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 | # http://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 | # Apache Maven Wrapper startup batch script, version 3.3.2 23 | # 24 | # Optional ENV vars 25 | # ----------------- 26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source 27 | # MVNW_REPOURL - repo url base for downloading maven distribution 28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output 30 | # ---------------------------------------------------------------------------- 31 | 32 | set -euf 33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x 34 | 35 | # OS specific support. 36 | native_path() { printf %s\\n "$1"; } 37 | case "$(uname)" in 38 | CYGWIN* | MINGW*) 39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" 40 | native_path() { cygpath --path --windows "$1"; } 41 | ;; 42 | esac 43 | 44 | # set JAVACMD and JAVACCMD 45 | set_java_home() { 46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched 47 | if [ -n "${JAVA_HOME-}" ]; then 48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 49 | # IBM's JDK on AIX uses strange locations for the executables 50 | JAVACMD="$JAVA_HOME/jre/sh/java" 51 | JAVACCMD="$JAVA_HOME/jre/sh/javac" 52 | else 53 | JAVACMD="$JAVA_HOME/bin/java" 54 | JAVACCMD="$JAVA_HOME/bin/javac" 55 | 56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then 57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 59 | return 1 60 | fi 61 | fi 62 | else 63 | JAVACMD="$( 64 | 'set' +e 65 | 'unset' -f command 2>/dev/null 66 | 'command' -v java 67 | )" || : 68 | JAVACCMD="$( 69 | 'set' +e 70 | 'unset' -f command 2>/dev/null 71 | 'command' -v javac 72 | )" || : 73 | 74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then 75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 76 | return 1 77 | fi 78 | fi 79 | } 80 | 81 | # hash string like Java String::hashCode 82 | hash_string() { 83 | str="${1:-}" h=0 84 | while [ -n "$str" ]; do 85 | char="${str%"${str#?}"}" 86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) 87 | str="${str#?}" 88 | done 89 | printf %x\\n $h 90 | } 91 | 92 | verbose() { :; } 93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } 94 | 95 | die() { 96 | printf %s\\n "$1" >&2 97 | exit 1 98 | } 99 | 100 | trim() { 101 | # MWRAPPER-139: 102 | # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. 103 | # Needed for removing poorly interpreted newline sequences when running in more 104 | # exotic environments such as mingw bash on Windows. 105 | printf "%s" "${1}" | tr -d '[:space:]' 106 | } 107 | 108 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties 109 | while IFS="=" read -r key value; do 110 | case "${key-}" in 111 | distributionUrl) distributionUrl=$(trim "${value-}") ;; 112 | distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; 113 | esac 114 | done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" 115 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" 116 | 117 | case "${distributionUrl##*/}" in 118 | maven-mvnd-*bin.*) 119 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ 120 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in 121 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; 122 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; 123 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; 124 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;; 125 | *) 126 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 127 | distributionPlatform=linux-amd64 128 | ;; 129 | esac 130 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" 131 | ;; 132 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; 133 | *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; 134 | esac 135 | 136 | # apply MVNW_REPOURL and calculate MAVEN_HOME 137 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 138 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" 139 | distributionUrlName="${distributionUrl##*/}" 140 | distributionUrlNameMain="${distributionUrlName%.*}" 141 | distributionUrlNameMain="${distributionUrlNameMain%-bin}" 142 | MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" 143 | MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" 144 | 145 | exec_maven() { 146 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : 147 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" 148 | } 149 | 150 | if [ -d "$MAVEN_HOME" ]; then 151 | verbose "found existing MAVEN_HOME at $MAVEN_HOME" 152 | exec_maven "$@" 153 | fi 154 | 155 | case "${distributionUrl-}" in 156 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; 157 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; 158 | esac 159 | 160 | # prepare tmp dir 161 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then 162 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } 163 | trap clean HUP INT TERM EXIT 164 | else 165 | die "cannot create temp dir" 166 | fi 167 | 168 | mkdir -p -- "${MAVEN_HOME%/*}" 169 | 170 | # Download and Install Apache Maven 171 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 172 | verbose "Downloading from: $distributionUrl" 173 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 174 | 175 | # select .zip or .tar.gz 176 | if ! command -v unzip >/dev/null; then 177 | distributionUrl="${distributionUrl%.zip}.tar.gz" 178 | distributionUrlName="${distributionUrl##*/}" 179 | fi 180 | 181 | # verbose opt 182 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' 183 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v 184 | 185 | # normalize http auth 186 | case "${MVNW_PASSWORD:+has-password}" in 187 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; 188 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; 189 | esac 190 | 191 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then 192 | verbose "Found wget ... using wget" 193 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" 194 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then 195 | verbose "Found curl ... using curl" 196 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" 197 | elif set_java_home; then 198 | verbose "Falling back to use Java to download" 199 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" 200 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" 201 | cat >"$javaSource" <<-END 202 | public class Downloader extends java.net.Authenticator 203 | { 204 | protected java.net.PasswordAuthentication getPasswordAuthentication() 205 | { 206 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); 207 | } 208 | public static void main( String[] args ) throws Exception 209 | { 210 | setDefault( new Downloader() ); 211 | java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); 212 | } 213 | } 214 | END 215 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java 216 | verbose " - Compiling Downloader.java ..." 217 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" 218 | verbose " - Running Downloader.java ..." 219 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" 220 | fi 221 | 222 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 223 | if [ -n "${distributionSha256Sum-}" ]; then 224 | distributionSha256Result=false 225 | if [ "$MVN_CMD" = mvnd.sh ]; then 226 | echo "Checksum validation is not supported for maven-mvnd." >&2 227 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 228 | exit 1 229 | elif command -v sha256sum >/dev/null; then 230 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then 231 | distributionSha256Result=true 232 | fi 233 | elif command -v shasum >/dev/null; then 234 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then 235 | distributionSha256Result=true 236 | fi 237 | else 238 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 239 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 240 | exit 1 241 | fi 242 | if [ $distributionSha256Result = false ]; then 243 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 244 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 245 | exit 1 246 | fi 247 | fi 248 | 249 | # unzip and move 250 | if command -v unzip >/dev/null; then 251 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" 252 | else 253 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" 254 | fi 255 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" 256 | mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" 257 | 258 | clean || : 259 | exec_maven "$@" 260 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 3.5.0 11 | 12 | 13 | 14 | com.miguno 15 | java-docker-build 16 | jar 17 | 1.0.0-SNAPSHOT 18 | java-docker-build 19 | A template project to create a minimal Docker image for a Java application 20 | https://github.com/miguno/java-docker-build-tutorial 21 | 2018 22 | 23 | 24 | 25 | Apache License 2.0 26 | https://www.apache.org/licenses/LICENSE-2.0.html 27 | repo 28 | 29 | 30 | 31 | 32 | 33 | miguno 34 | Michael G. Noll 35 | 36 | author 37 | 38 | Europe/Berlin 39 | 40 | 41 | 42 | 43 | https://github.com/miguno/java-docker-build-tutorial 44 | scm:git:https://github.com/miguno/java-docker-build-tutorial.git 45 | scm:git:git@github.com:miguno/java-docker-build-tutorial.git 46 | HEAD 47 | 48 | 49 | 50 | 51 | UTF-8 52 | UTF-8 53 | 54 | 22 55 | ${java.version} 56 | true 57 | ${java.version} 58 | ${java.version} 59 | ${java.version} 60 | 61 | 1.15.0 62 | 2.8.8 63 | 3.4.5 64 | 65 | 3.9.0 66 | 3.5.0 67 | 3.14.0 68 | 3.11.2 69 | 3.5.3 70 | 3.5.0 71 | 0.8.13 72 | 4.9.3 73 | 4.9.3.0 74 | 1.14.0 75 | 2.44.5 76 | 0.15.0 77 | 78 | false 79 | ${skipTests} 80 | ${skipTests} 81 | 90 | 91 | 92 | 93 | 94 | 95 | org.springframework.boot 96 | spring-boot-starter-web 97 | 98 | 99 | 100 | 101 | org.springframework.boot 102 | spring-boot-starter-actuator 103 | 104 | 105 | 106 | org.springframework.boot 107 | spring-boot-starter-security 108 | 109 | 110 | 111 | org.springframework.boot 112 | spring-boot-devtools 113 | true 114 | 115 | 116 | 117 | org.springframework.boot 118 | spring-boot-starter-test 119 | test 120 | 121 | 122 | 123 | 126 | com.github.therapi 127 | therapi-runtime-javadoc 128 | ${therapi.version} 129 | 130 | 131 | 132 | 141 | io.micrometer 142 | micrometer-registry-prometheus 143 | ${micrometer.version} 144 | 145 | 146 | 147 | 155 | org.springdoc 156 | springdoc-openapi-starter-webmvc-ui 157 | ${springdoc.version} 158 | 159 | 160 | org.springdoc 161 | springdoc-openapi-starter-common 162 | ${springdoc.version} 163 | 164 | 165 | 166 | 170 | com.github.spotbugs 171 | spotbugs-annotations 172 | ${spotbugs.version} 173 | provided 174 | 175 | 176 | 177 | 178 | 182 | app 183 | 184 | 185 | 186 | 187 | org.apache.maven.plugins 188 | maven-enforcer-plugin 189 | ${maven-enforcer-plugin.version} 190 | 191 | 192 | 193 | org.apache.maven.plugins 194 | maven-compiler-plugin 195 | ${maven-compiler-plugin.version} 196 | 197 | 198 | 199 | 200 | org.apache.maven.plugins 201 | maven-surefire-plugin 202 | ${maven-surefire-plugin.version} 203 | 204 | 205 | 206 | 207 | org.apache.maven.plugins 208 | maven-failsafe-plugin 209 | ${maven-surefire-plugin.version} 210 | 211 | 212 | 213 | 214 | org.apache.maven.plugins 215 | maven-javadoc-plugin 216 | ${maven-javadoc-plugin.version} 217 | 218 | 219 | 220 | 221 | com.github.spotbugs 222 | spotbugs-maven-plugin 223 | ${spotbugs-maven-plugin.version} 224 | 225 | 229 | 230 | com.github.spotbugs 231 | spotbugs 232 | ${spotbugs.version} 233 | 234 | 235 | 236 | 237 | 238 | 239 | org.jacoco 240 | jacoco-maven-plugin 241 | ${jacoco.version} 242 | 243 | 244 | 245 | 246 | com.diffplug.spotless 247 | spotless-maven-plugin 248 | ${spotless.version} 249 | 250 | 251 | 252 | 253 | com.rudikershaw.gitbuildhook 254 | git-build-hook-maven-plugin 255 | ${git-build-hook-maven-plugin.version} 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | true 264 | org.apache.maven.plugins 265 | maven-enforcer-plugin 266 | 267 | 268 | enforce-maven-version 269 | 270 | enforce 271 | 272 | 273 | 274 | 275 | 3.9.0 276 | 277 | 278 | true 279 | 280 | 281 | 282 | 283 | 284 | 285 | org.apache.maven.plugins 286 | maven-compiler-plugin 287 | 288 | 293 | -proc:full 294 | ${maven.compiler.parameters} 295 | 296 | 301 | 302 | 303 | com.github.therapi 304 | therapi-runtime-javadoc-scribe 305 | ${therapi.version} 306 | 307 | 308 | 309 | 310 | 311 | 312 | org.apache.maven.plugins 313 | maven-surefire-plugin 314 | 315 | 321 | @{argLine} -XX:+EnableDynamicAgentLoading 322 | 323 | 1 324 | ${skipUTs} 325 | 326 | 327 | 328 | 329 | org.apache.maven.plugins 330 | maven-failsafe-plugin 331 | 332 | 338 | @{argLine} -XX:+EnableDynamicAgentLoading 339 | 340 | 1 341 | ${skipTests} 342 | ${skipITs} 343 | 344 | 345 | 346 | 347 | integration-test 348 | verify 349 | 350 | 351 | 352 | 353 | 354 | 355 | org.apache.maven.plugins 356 | maven-javadoc-plugin 357 | 358 | 359 | attach-javadocs 360 | 361 | jar 362 | 363 | 364 | 365 | 366 | 367 | 368 | 373 | org.jacoco 374 | jacoco-maven-plugin 375 | 376 | 377 | default-prepare-agent 378 | 379 | prepare-agent 380 | 381 | 382 | ${project.basedir}/target/jacoco-unit.exec 383 | 384 | 385 | 386 | default-prepare-agent-integration 387 | 388 | prepare-agent-integration 389 | 390 | 391 | ${project.basedir}/target/jacoco-integration.exec 392 | true 393 | 394 | 395 | 396 | default-report 397 | post-integration-test 398 | 399 | merge 400 | report 401 | 402 | 403 | 404 | ${project.basedir}/target/jacoco.exec 405 | 406 | 407 | ${project.basedir}/target/ 408 | 409 | jacoco-*.exec 410 | 411 | 412 | 413 | 414 | ${project.basedir}/target/jacoco.exec 415 | 416 | 417 | 418 | default-report-integration 419 | 420 | report-integration 421 | 422 | 423 | ${project.basedir}/target/jacoco.exec 424 | 425 | 426 | 427 | default-check 428 | 429 | check 430 | 431 | 432 | 433 | 434 | BUNDLE 435 | 436 | 437 | COMPLEXITY 438 | COVEREDRATIO 439 | 0.60 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 453 | 454 | com.github.spotbugs 455 | spotbugs-maven-plugin 456 | 457 | 458 | Max 459 | Low 460 | true 461 | spotbugs-include.xml 462 | spotbugs-exclude.xml 463 | 464 | 465 | 466 | com.h3xstream.findsecbugs 467 | findsecbugs-plugin 468 | ${findsecbugs.version} 469 | 470 | 471 | 472 | 473 | true 474 | ${project.basedir}/target/site 475 | 476 | 477 | 478 | 479 | mvn-package-runs-spotbugs 480 | 481 | check 482 | 483 | package 484 | 485 | 486 | 487 | 488 | 489 | com.diffplug.spotless 490 | spotless-maven-plugin 491 | 492 | 493 | 494 | apply 495 | 496 | compile 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | src/main/java/**/*.java 507 | src/test/java/**/*.java 508 | 509 | 510 | 511 | java|javax,,\# 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | com.rudikershaw.gitbuildhook 521 | git-build-hook-maven-plugin 522 | ${git-build-hook-maven-plugin.version} 523 | 524 | 525 | 526 | hooks/ 527 | 528 | 529 | 530 | 531 | 532 | 533 | configure 534 | 535 | 536 | 537 | 538 | 539 | 540 | org.springframework.boot 541 | spring-boot-maven-plugin 542 | 543 | 544 | pre-integration-test 545 | 546 | start 547 | 548 | 549 | 550 | post-integration-test 551 | 552 | stop 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | org.apache.maven.plugins 564 | maven-project-info-reports-plugin 565 | ${maven-project-info-reports-plugin.version} 566 | 567 | 568 | 569 | org.apache.maven.plugins 570 | maven-javadoc-plugin 571 | ${maven-javadoc-plugin.version} 572 | 573 | 574 | 575 | 576 | com.github.spotbugs 577 | spotbugs-maven-plugin 578 | ${spotbugs-maven-plugin.version} 579 | 580 | 581 | -Duser.language=en 582 | Max 583 | Low 584 | true 585 | spotbugs-include.xml 586 | spotbugs-exclude.xml 587 | 588 | 589 | 590 | com.h3xstream.findsecbugs 591 | findsecbugs-plugin 592 | ${findsecbugs.version} 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | -------------------------------------------------------------------------------- /spotbugs-exclude.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /spotbugs-include.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/java/com/miguno/javadockerbuild/App.java: -------------------------------------------------------------------------------- 1 | package com.miguno.javadockerbuild; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** An example application that exposes an HTTP endpoint. */ 7 | @SpringBootApplication 8 | public class App { 9 | 10 | public static void main(String[] args) { 11 | SpringApplication.run(App.class, args); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/miguno/javadockerbuild/controllers/RootController.java: -------------------------------------------------------------------------------- 1 | package com.miguno.javadockerbuild.controllers; 2 | 3 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | /** Implements a basic landing page at endpoint `/`. */ 9 | @SuppressFBWarnings("SPRING_ENDPOINT") 10 | @RestController 11 | public class RootController { 12 | 13 | @Value("${app.spring-boot-admin.role.user.name}") 14 | private String roleUserName; 15 | 16 | @Value("${spring.application.name}") 17 | private String appName; 18 | 19 | @Value("${app.spring-boot-admin.role.user.password}") 20 | private String roleUserPassword; 21 | 22 | @Value("${app.spring-boot-admin.role.admin.name}") 23 | private String roleAdminName; 24 | 25 | @Value("${app.spring-boot-admin.role.admin.password}") 26 | private String roleAdminPassword; 27 | 28 | /** 29 | * Returns a basic landing page for this application. 30 | * 31 | * @return Basic landing page in HTML format. 32 | */ 33 | @GetMapping("/") 34 | @SuppressFBWarnings("VA_FORMAT_STRING_USES_NEWLINE") 35 | public String root() { 36 | return String.format( 37 | """ 38 |

Welcome to %s

39 |

Enjoy playing around with this application!

40 |

Example Endpoints

41 | 47 |

User Accounts

48 |

For endpoints that require login.

49 |
    50 |
  • Admin user: %s with password %s
  • 51 |
  • Regular user: %s with password %s
  • 52 |

    53 |
      54 |
    55 | """, 56 | appName, roleAdminName, roleAdminPassword, roleUserName, roleUserPassword); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/miguno/javadockerbuild/controllers/WelcomeController.java: -------------------------------------------------------------------------------- 1 | package com.miguno.javadockerbuild.controllers; 2 | 3 | import com.miguno.javadockerbuild.models.Welcome; 4 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.PathVariable; 7 | import org.springframework.web.bind.annotation.RequestParam; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | /** The API endpoint exposed by this example application. */ 11 | @SuppressFBWarnings("SPRING_ENDPOINT") 12 | @RestController 13 | public class WelcomeController { 14 | 15 | private static final String template = "Hello, %s!"; 16 | 17 | /** 18 | * Returns a welcome message to the client. 19 | * 20 | * @param name The name to greet. 21 | * @return A personalized welcome message. 22 | */ 23 | @GetMapping("/welcome") 24 | public Welcome welcome(@RequestParam(value = "name", defaultValue = "World") String name) { 25 | // Note: If you make changes to the URL path, remember to update AppSecurityConfiguration. 26 | return new Welcome(String.format(template, name)); 27 | } 28 | 29 | /** 30 | * Returns a welcome message to the client. 31 | * 32 | * @param name The name to greet. 33 | * @return A personalized welcome message. 34 | */ 35 | @GetMapping("/welcome/{name}") 36 | public Welcome welcomeWithPathVariable(@PathVariable(value = "name") String name) { 37 | // Note: If you make changes to the URL path, remember to update AppSecurityConfiguration. 38 | return new Welcome(String.format(template, name)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/miguno/javadockerbuild/models/Welcome.java: -------------------------------------------------------------------------------- 1 | package com.miguno.javadockerbuild.models; 2 | 3 | public record Welcome(String welcome) {} 4 | -------------------------------------------------------------------------------- /src/main/java/com/miguno/javadockerbuild/security/AppSecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.miguno.javadockerbuild.security; 2 | 3 | import java.util.UUID; 4 | 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.boot.autoconfigure.security.SecurityProperties; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.security.config.Customizer; 10 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 11 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 12 | import org.springframework.security.core.userdetails.User; 13 | import org.springframework.security.core.userdetails.UserDetails; 14 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 15 | import org.springframework.security.crypto.password.PasswordEncoder; 16 | import org.springframework.security.provisioning.InMemoryUserDetailsManager; 17 | import org.springframework.security.web.SecurityFilterChain; 18 | import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; 19 | import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; 20 | import org.springframework.security.web.csrf.CookieCsrfTokenRepository; 21 | import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; 22 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 23 | 24 | /** Secures the endpoints of this application. */ 25 | @Configuration(proxyBeanMethods = false) 26 | @EnableWebSecurity 27 | public class AppSecurityConfiguration { 28 | 29 | @Value("${app.spring-boot-admin.role.user.name}") 30 | private String roleUserName; 31 | 32 | @Value("${app.spring-boot-admin.role.user.password}") 33 | private String roleUserPassword; 34 | 35 | @Value("${app.spring-boot-admin.role.admin.name}") 36 | private String roleAdminName; 37 | 38 | @Value("${app.spring-boot-admin.role.admin.password}") 39 | private String roleAdminPassword; 40 | 41 | public AppSecurityConfiguration(SecurityProperties security) {} 42 | 43 | /** 44 | * Applies security policies such as authentication requirements to endpoints. 45 | * 46 | * @param http Supplied by Spring. 47 | * @return The applications' security filter chain. 48 | * @throws Exception Unclear when that happens. 49 | */ 50 | @Bean 51 | protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { 52 | SavedRequestAwareAuthenticationSuccessHandler successHandler = 53 | new SavedRequestAwareAuthenticationSuccessHandler(); 54 | successHandler.setTargetUrlParameter("redirectTo"); 55 | successHandler.setDefaultTargetUrl("/"); 56 | 57 | // NOTE: In this project, the Spring Boot Admin server and client are colocated in the same 58 | // application for demonstration purposes. In production, you would typically not do that 59 | // and instead separate the code and functionality. See the recommendations of Spring Boot 60 | // Admin at https://docs.spring-boot-admin.com/current/faq.html. 61 | // The effect of this colocation is that this application contains endpoints for both 62 | // server and client, and the authorization settings below also apply to both: if you 63 | // permit access to a URL in the "for the server" section you also permit access for the 64 | // client, and vice versa. Again, this would be different in production where the server 65 | // and the clients would be separate applications and processes. 66 | http.authorizeHttpRequests( 67 | (authorizeRequests) -> 68 | authorizeRequests 69 | .requestMatchers( 70 | new AntPathRequestMatcher("/"), 71 | // Permit public access to this app's example endpoint at `/welcome`. 72 | new AntPathRequestMatcher("/welcome/**"), 73 | // Permit public access to Swagger. 74 | new AntPathRequestMatcher("/swagger-ui.html"), 75 | new AntPathRequestMatcher("/v3/api-docs"), 76 | // Permit public access to a subset of actuator endpoints. 77 | new AntPathRequestMatcher("/actuator/health"), 78 | new AntPathRequestMatcher("/actuator/info"), 79 | new AntPathRequestMatcher("/actuator/prometheus")) 80 | .permitAll() 81 | // All other requests must be authenticated. 82 | .anyRequest() 83 | .authenticated()) 84 | // Enables HTTP Basic Authentication support. 85 | .httpBasic(Customizer.withDefaults()); 86 | 87 | // Enables CSRF-Protection using cookies. 88 | http.addFilterAfter(new CustomCsrfFilter(), BasicAuthenticationFilter.class) 89 | .csrf( 90 | (csrf) -> 91 | csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) 92 | .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())); 93 | 94 | http.rememberMe( 95 | (rememberMe) -> rememberMe.key(UUID.randomUUID().toString()).tokenValiditySeconds(1209600)); 96 | 97 | return http.build(); 98 | } 99 | 100 | /** Required to provide UserDetailsService for "remember functionality". */ 101 | @Bean 102 | public InMemoryUserDetailsManager userDetailsService(PasswordEncoder passwordEncoder) { 103 | // NOTE: HTTP Basic Authentication itself is not recommended for production. 104 | UserDetails user = 105 | User.withUsername(roleUserName) 106 | .password(passwordEncoder.encode(roleUserPassword)) 107 | .roles("USER") 108 | .build(); 109 | UserDetails admin = 110 | User.withUsername(roleAdminName) 111 | .password(passwordEncoder.encode(roleAdminPassword)) 112 | .roles("ADMIN", "USER") 113 | .build(); 114 | return new InMemoryUserDetailsManager(user, admin); 115 | } 116 | 117 | @Bean 118 | public PasswordEncoder passwordEncoder() { 119 | return new BCryptPasswordEncoder(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main/java/com/miguno/javadockerbuild/security/CustomCsrfFilter.java: -------------------------------------------------------------------------------- 1 | package com.miguno.javadockerbuild.security; 2 | 3 | import java.io.IOException; 4 | 5 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 6 | import jakarta.servlet.FilterChain; 7 | import jakarta.servlet.ServletException; 8 | import jakarta.servlet.http.Cookie; 9 | import jakarta.servlet.http.HttpServletRequest; 10 | import jakarta.servlet.http.HttpServletResponse; 11 | import org.springframework.lang.NonNull; 12 | import org.springframework.security.web.csrf.CsrfToken; 13 | import org.springframework.web.filter.OncePerRequestFilter; 14 | import org.springframework.web.util.WebUtils; 15 | 16 | /** A custom CSRF Filter, derived from the Spring Boot Admin documentation. */ 17 | public class CustomCsrfFilter extends OncePerRequestFilter { 18 | 19 | public static final String CSRF_COOKIE_NAME = "XSRF-TOKEN"; 20 | 21 | @SuppressFBWarnings( 22 | value = "COOKIE_USAGE", 23 | justification = 24 | "CSRF tokens are designed to be stored in cookies with appropriate security controls") 25 | @Override 26 | protected void doFilterInternal( 27 | @NonNull HttpServletRequest request, 28 | @NonNull HttpServletResponse response, 29 | @NonNull FilterChain filterChain) 30 | throws ServletException, IOException { 31 | CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); 32 | if (csrf != null) { 33 | Cookie cookie = WebUtils.getCookie(request, CSRF_COOKIE_NAME); 34 | String token = csrf.getToken(); 35 | 36 | if (cookie == null || token != null && !token.equals(cookie.getValue())) { 37 | cookie = new Cookie(CSRF_COOKIE_NAME, token); 38 | cookie.setPath("/"); 39 | cookie.setHttpOnly(true); 40 | cookie.setSecure(true); 41 | response.addCookie(cookie); 42 | } 43 | } 44 | filterChain.doFilter(request, response); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | ### Custom app-specific settings ### 3 | ################################################################################ 4 | # Credentials of role "ADMIN" for HTTP Basic Authentication. 5 | app.spring-boot-admin.role.admin.name=admin 6 | app.spring-boot-admin.role.admin.password=admin 7 | # Credentials of role "USER" for HTTP Basic Authentication. 8 | app.spring-boot-admin.role.user.name=demouser 9 | app.spring-boot-admin.role.user.password=demopass 10 | 11 | ################################################################################ 12 | ### Spring settings ### 13 | ################################################################################ 14 | spring.application.name=java-docker-build-tutorial 15 | server.port=8123 16 | 17 | # Enable virtual threads (requires Java 21+). 18 | # Virtual threads may come with downsides for your Spring Boot application, 19 | # see read the documentation before you enable them here. 20 | # https://docs.spring.io/spring-boot/reference/features/spring-application.html#features.spring-application.virtual-threads 21 | #spring.threads.virtual.enabled=true 22 | 23 | # Spring Actuator configuration 24 | # https://docs.spring.io/spring-boot/reference/actuator/endpoints.html 25 | # 26 | # Expose all endpoints by default via `*`. However, `AppSecurityConfiguration` 27 | # only permits public access to a subset of endpoints, whereas the rest requires 28 | # HTTP Basic Authentication. 29 | # 30 | # IMPORTANT: In production, you should choose carefully what endpoints to expose! 31 | management.endpoints.web.exposure.include=* 32 | # Enable the env contributor. 33 | management.info.env.enabled=true 34 | -------------------------------------------------------------------------------- /src/main/resources/templates/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miguno/java-docker-build-tutorial/d884d121ae5d6eb7ebfcab38263bac6fd4065e4c/src/main/resources/templates/.keep -------------------------------------------------------------------------------- /src/test/java/com/miguno/javadockerbuild/SmokeTest.java: -------------------------------------------------------------------------------- 1 | package com.miguno.javadockerbuild; 2 | 3 | import com.miguno.javadockerbuild.controllers.WelcomeController; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | 8 | @SpringBootTest 9 | class SmokeTest { 10 | 11 | @Autowired private WelcomeController controller; 12 | 13 | @Test 14 | void verifyThatApplicationContextLoads() { 15 | // Will fail if the application context cannot start. 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/com/miguno/javadockerbuild/controllers/WelcomeControllerIT.java: -------------------------------------------------------------------------------- 1 | package com.miguno.javadockerbuild.controllers; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.skyscreamer.jsonassert.JSONAssert; 5 | import org.skyscreamer.jsonassert.JSONCompareMode; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.boot.test.web.client.TestRestTemplate; 9 | import org.springframework.http.ResponseEntity; 10 | 11 | /** An example integration test for the API endpoint `/welcome`. */ 12 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 13 | public class WelcomeControllerIT { 14 | 15 | @Autowired private TestRestTemplate template; 16 | 17 | @Test 18 | public void welcome() throws Exception { 19 | ResponseEntity response = template.getForEntity("/welcome", String.class); 20 | String expectedJson = 21 | """ 22 | {"welcome":"Hello, World!"} 23 | """; 24 | JSONAssert.assertEquals(expectedJson, response.getBody(), JSONCompareMode.STRICT); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/com/miguno/javadockerbuild/controllers/WelcomeControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.miguno.javadockerbuild.controllers; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.http.MediaType; 8 | import org.springframework.test.web.servlet.MockMvc; 9 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 10 | 11 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 12 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 13 | 14 | /** Example unit tests for the API endpoint `/welcome`. */ 15 | @SpringBootTest 16 | @AutoConfigureMockMvc 17 | public class WelcomeControllerTest { 18 | 19 | @Autowired private MockMvc mvc; 20 | 21 | @Test 22 | public void getWelcome() throws Exception { 23 | String expectedJson = 24 | """ 25 | {"welcome":"Hello, World!"} 26 | """; 27 | 28 | mvc.perform(MockMvcRequestBuilders.get("/welcome").accept(MediaType.APPLICATION_JSON)) 29 | .andExpect(status().isOk()) 30 | .andExpect(content().json(expectedJson)); 31 | } 32 | 33 | @Test 34 | public void getWelcomeWithPathVariable() throws Exception { 35 | String expectedJson = 36 | """ 37 | {"welcome":"Hello, Gandalf!"} 38 | """; 39 | 40 | mvc.perform(MockMvcRequestBuilders.get("/welcome/Gandalf").accept(MediaType.APPLICATION_JSON)) 41 | .andExpect(status().isOk()) 42 | .andExpect(content().json(expectedJson)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tools/create_image.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC2155 3 | 4 | # https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ 5 | # `-u`: Errors if a variable is referenced before being set 6 | # `-o pipefail`: Prevent errors in a pipeline (`|`) from being masked 7 | set -uo pipefail 8 | 9 | declare -r SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) 10 | declare -r PROJECT_DIR=$(readlink -f "$SCRIPT_DIR/..") 11 | 12 | # Import environment variables from .env 13 | set -o allexport && source "$PROJECT_DIR/.env" && set +o allexport 14 | 15 | # Check requirements 16 | if ! command -v docker &>/dev/null; then 17 | echo "ERROR: 'docker' command not available. Is Docker installed?" 18 | exit 1 19 | fi 20 | 21 | echo "Building image '$DOCKER_IMAGE_NAME:$DOCKER_IMAGE_TAG'..." 22 | # TIP: Add `--progress=plain` to see the full docker output when you are 23 | # troubleshooting the build setup of your image. 24 | # 25 | # Force amd64 as the platform. This workaround is needed on Apple Silicon 26 | # machines. Details at https://stackoverflow.com/questions/72152446/. 27 | declare -r DOCKER_OPTIONS="--platform linux/amd64" 28 | # Use BuildKit, i.e. `buildx build` instead of just `build` 29 | # https://docs.docker.com/build/ 30 | # 31 | # shellcheck disable=SC2086 32 | docker buildx build $DOCKER_OPTIONS -t "$DOCKER_IMAGE_NAME":"$DOCKER_IMAGE_TAG" . 33 | -------------------------------------------------------------------------------- /tools/start_container.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC2155 3 | 4 | # https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ 5 | # `-u`: Errors if a variable is referenced before being set 6 | # `-o pipefail`: Prevent errors in a pipeline (`|`) from being masked 7 | set -uo pipefail 8 | 9 | declare -r SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) 10 | declare -r PROJECT_DIR=$(readlink -f "$SCRIPT_DIR/..") 11 | 12 | # Import environment variables from .env 13 | set -o allexport && source "$PROJECT_DIR/.env" && set +o allexport 14 | 15 | # Check requirements 16 | if ! command -v docker &>/dev/null; then 17 | echo "ERROR: 'docker' command not available. Is Docker installed?" 18 | exit 1 19 | fi 20 | 21 | # shellcheck disable=SC2155 22 | declare -r OS="$(uname -s)" 23 | # "arm64" for Apple Silicon (M1/M2/M3/etc.) 24 | # shellcheck disable=SC2155 25 | declare -r ARCH="$(uname -m)" 26 | 27 | DOCKER_OPTIONS="" 28 | if [[ "$OS" = "Darwin" && "$ARCH" = "arm64" ]]; then 29 | # Force amd64 as the platform. This workaround is needed on Apple Silicon 30 | # machines. Details at https://stackoverflow.com/questions/72152446/. 31 | DOCKER_OPTIONS="--platform linux/amd64" 32 | fi 33 | 34 | echo "Starting container for image '$DOCKER_IMAGE_NAME:$DOCKER_IMAGE_TAG', exposing port ${APP_PORT}/tcp" 35 | echo "- Run 'curl http://localhost:${APP_PORT}/welcome' to send a test request to the containerized app." 36 | echo "- Enter Ctrl-C to stop the container." 37 | # shellcheck disable=SC2086 38 | docker run $DOCKER_OPTIONS -p "$APP_PORT:$APP_PORT" "$DOCKER_IMAGE_NAME":"$DOCKER_IMAGE_TAG" 39 | --------------------------------------------------------------------------------