|
1 | | -# Maven's resource lifecycle builds the Vite assets, so provide both toolchains |
2 | | -# to the builder while keeping the runtime image limited to the JRE. |
3 | | -FROM node:24-alpine AS node-toolchain |
4 | | - |
5 | | -FROM maven:3.9.16-eclipse-temurin-21-alpine AS backend-build |
| 1 | +# syntax=docker/dockerfile:1 |
| 2 | +# Java and Node are build tools only; the published image contains a jlink runtime. |
| 3 | +FROM node:26.10.0-alpine@sha256:0b36e8c136b94cd4fcf02188228e76c31ad5872eef3fec8cbd2eee500cfd9e80 AS node-toolchain |
6 | 4 |
|
| 5 | +FROM eclipse-temurin:25.0.4.1_1-jdk-alpine-3.24@sha256:3fd2d245c4e0eba615fe366a71b8bd25f5db7104f53e4026b24bf508b880bd2a AS backend-build |
7 | 6 | RUN apk add --no-cache libstdc++ |
8 | 7 | COPY --from=node-toolchain /usr/local/ /usr/local/ |
9 | | - |
10 | 8 | WORKDIR /app |
11 | | - |
12 | | -COPY pom.xml ./ |
13 | | -COPY mvnw ./ |
| 9 | +COPY pom.xml mvnw ./ |
14 | 10 | COPY .mvn ./.mvn |
15 | 11 | COPY frontend ./frontend |
16 | 12 | COPY src ./src |
17 | 13 |
|
18 | | -# The Maven lifecycle runs npm ci and the frontend build before packaging. |
19 | | -RUN ./mvnw --batch-mode clean package -DskipTests |
20 | | - |
21 | | -# Stage 3: Runtime |
22 | | -FROM eclipse-temurin:21-jre-alpine AS runtime-base |
23 | | - |
24 | | -WORKDIR /app |
25 | | - |
26 | | -# Create non-root user |
27 | | -RUN addgroup -g 1001 -S spring && \ |
| 14 | +# Run the same backend tests and frontend checks used for local verification. |
| 15 | +RUN ./mvnw --batch-mode --no-transfer-progress clean verify |
| 16 | + |
| 17 | +FROM backend-build AS java-runtime |
| 18 | +# jdeps finds static JDK dependencies. Reflection, charset and service-provider |
| 19 | +# use in Spring/Hibernate/Netty/PDFBox needs the additional modules below. |
| 20 | +RUN mkdir /app/unpacked && cd /app/unpacked && \ |
| 21 | + jar -xf /app/target/website-0.0.1-SNAPSHOT.jar && \ |
| 22 | + modules="$(jdeps --ignore-missing-deps --multi-release 25 --recursive \ |
| 23 | + --print-module-deps --class-path 'BOOT-INF/lib/*' BOOT-INF/classes)" && \ |
| 24 | + jlink --add-modules "$modules,java.desktop,java.instrument,java.management,java.naming,java.sql,jdk.charsets,jdk.crypto.ec,jdk.localedata,jdk.unsupported,jdk.zipfs" \ |
| 25 | + --strip-debug --no-header-files --no-man-pages --compress=zip-6 \ |
| 26 | + --output /opt/java-runtime |
| 27 | + |
| 28 | +FROM alpine:3.24.2@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 AS runtime-base |
| 29 | +RUN apk upgrade --no-cache && \ |
| 30 | + apk add --no-cache ca-certificates libstdc++ zlib fontconfig font-dejavu && \ |
| 31 | + addgroup -g 1001 -S spring && \ |
28 | 32 | adduser -u 1001 -S spring -G spring |
29 | | - |
30 | | -# Copy JAR from build stage |
31 | | -COPY --from=backend-build /app/target/*.jar app.jar |
32 | | - |
33 | | -# Copy resources directory (for markdown files) |
34 | | -COPY --from=backend-build /app/src/main/resources ./resources |
35 | | - |
36 | | -# Change ownership |
37 | | -RUN chown -R spring:spring /app |
38 | | - |
39 | | -# Switch to non-root user |
40 | | -USER spring:spring |
41 | | - |
42 | | -# Expose port |
| 33 | +ENV JAVA_HOME=/opt/java |
| 34 | +ENV PATH="$JAVA_HOME/bin:$PATH" \ |
| 35 | + JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Djava.awt.headless=true" |
| 36 | +WORKDIR /app |
| 37 | +COPY --from=java-runtime /opt/java-runtime /opt/java |
| 38 | +COPY --from=backend-build --chmod=0444 /app/target/website-0.0.1-SNAPSHOT.jar /app/app.jar |
| 39 | +USER 1001:1001 |
43 | 40 | EXPOSE 8080 |
44 | | - |
45 | | -# Actuator exposes the application's health status without relying on a |
46 | | -# client-information API as a readiness signal. |
47 | 41 | HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \ |
48 | | - CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1 |
49 | | - |
50 | | -# Set JVM options |
51 | | -ENV JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200" |
| 42 | + CMD wget -q -T 2 -O /dev/null http://127.0.0.1:8080/actuator/health || exit 1 |
| 43 | +ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/app.jar"] |
52 | 44 |
|
53 | | -# Run application |
54 | | -ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] |
55 | | - |
56 | | -# BuildKit runs this stage as the same non-root runtime user. Starting the |
57 | | -# packaged application here verifies the runtime filesystem without needing a |
58 | | -# Docker daemon or exporting an image from the remote builder. |
| 45 | +# Run the packaged app as the production UID to verify the linked Java modules, |
| 46 | +# PDF extraction, content parsing, and the actual generated web assets. |
59 | 47 | FROM runtime-base AS smoke-test |
60 | 48 | RUN set -eu; \ |
61 | | - java $JAVA_OPTS -jar app.jar > /tmp/website-smoke.log 2>&1 & \ |
| 49 | + test "$(id -u)" = 1001; \ |
| 50 | + test ! -e /opt/java/bin/javac; \ |
| 51 | + java -version; \ |
| 52 | + java $JAVA_OPTS -jar /app/app.jar > /tmp/website-smoke.log 2>&1 & \ |
62 | 53 | app_pid=$!; \ |
63 | 54 | cleanup() { kill "$app_pid" 2>/dev/null || true; wait "$app_pid" 2>/dev/null || true; }; \ |
64 | 55 | trap cleanup EXIT; \ |
65 | 56 | attempt=0; \ |
66 | 57 | while [ "$attempt" -lt 60 ]; do \ |
67 | 58 | if ! kill -0 "$app_pid" 2>/dev/null; then cat /tmp/website-smoke.log; exit 1; fi; \ |
68 | | - if wget -T 5 -qO /tmp/website-health.json http://127.0.0.1:8080/actuator/health; then break; fi; \ |
| 59 | + if wget -T 5 -qO /tmp/website-health.json http://127.0.0.1:8080/actuator/health; then break; fi; \ |
69 | 60 | attempt=$((attempt + 1)); \ |
70 | 61 | sleep 2; \ |
71 | 62 | done; \ |
72 | 63 | if [ "$attempt" -ge 60 ]; then cat /tmp/website-smoke.log; exit 1; fi; \ |
73 | | - if ! grep -Eq '"status"[[:space:]]*:[[:space:]]*"UP"' /tmp/website-health.json; then cat /tmp/website-health.json; exit 1; fi; \ |
74 | | - if ! wget -T 5 -qO /tmp/website-client-info.json http://127.0.0.1:8080/api/client/info; then cat /tmp/website-smoke.log; exit 1; fi; \ |
75 | | - grep -Eq '"hostname"[[:space:]]*:[[:space:]]*"javadevjt\.tech"' /tmp/website-client-info.json; \ |
| 64 | + grep -Eq '"status"[[:space:]]*:[[:space:]]*"UP"' /tmp/website-health.json; \ |
| 65 | + wget -T 5 -qO /tmp/website-client-info.json http://127.0.0.1:8080/api/client/info; \ |
| 66 | + grep -Eq '"hostname"[[:space:]]*:[[:space:]]*"javadevjt[.]tech"' /tmp/website-client-info.json; \ |
76 | 67 | grep -Eq '"username"[[:space:]]*:[[:space:]]*"visitor"' /tmp/website-client-info.json; \ |
77 | | - if ! wget -T 5 -qO /tmp/website-portfolio.json http://127.0.0.1:8080/api/content/portfolio/list; then cat /tmp/website-smoke.log; exit 1; fi; \ |
78 | | - if ! grep -Eq '"filename"|RouteListToTesla' /tmp/website-portfolio.json; then cat /tmp/website-portfolio.json; exit 1; fi; \ |
79 | | - if ! wget -T 5 -qO /tmp/website-index.html http://127.0.0.1:8080/; then cat /tmp/website-smoke.log; exit 1; fi; \ |
80 | | - js_asset="$(grep -oE 'src="/assets/[^"]+\.js"' /tmp/website-index.html | head -n 1 | cut -d '"' -f 2)"; \ |
81 | | - css_asset="$(grep -oE 'href="/assets/[^"]+\.css"' /tmp/website-index.html | head -n 1 | cut -d '"' -f 2)"; \ |
| 68 | + wget -T 5 -qO /tmp/website-portfolio.json http://127.0.0.1:8080/api/content/portfolio/list; \ |
| 69 | + grep -q 'Discord Option Tailer' /tmp/website-portfolio.json; \ |
| 70 | + grep -q 'Embedify' /tmp/website-portfolio.json; \ |
| 71 | + wget -T 5 -qO /tmp/website-resume.json http://127.0.0.1:8080/api/content/resume; \ |
| 72 | + grep -q 'Senior Software Engineer' /tmp/website-resume.json; \ |
| 73 | + wget -T 5 -qO /tmp/website-index.html http://127.0.0.1:8080/; \ |
| 74 | + js_asset="$(grep -oE 'src="/assets/[^"]+[.]js"' /tmp/website-index.html | head -n 1 | cut -d '"' -f 2)"; \ |
| 75 | + css_asset="$(grep -oE 'href="/assets/[^"]+[.]css"' /tmp/website-index.html | head -n 1 | cut -d '"' -f 2)"; \ |
82 | 76 | test -n "$js_asset" && test -n "$css_asset"; \ |
83 | 77 | wget -T 5 -qO /tmp/website-app.js "http://127.0.0.1:8080$js_asset"; \ |
84 | 78 | wget -T 5 -qO /tmp/website-app.css "http://127.0.0.1:8080$css_asset"; \ |
85 | 79 | test -s /tmp/website-app.js && test -s /tmp/website-app.css |
86 | 80 |
|
87 | | -# Keep normal image builds on the production runtime stage; CI explicitly |
88 | | -# selects smoke-test to execute the verification above. |
| 81 | +# Scan the runtime filesystem, including OS packages and nested Java libraries. |
| 82 | +# CI always reruns this stage against fresh vulnerability data. |
| 83 | +FROM aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969 AS security-scan |
| 84 | +COPY --from=smoke-test / /scan-root/ |
| 85 | +RUN trivy rootfs --scanners vuln --exit-code 1 --timeout 10m /scan-root && touch /scan-passed |
| 86 | + |
| 87 | +# Default builds publish only the application and linked runtime. |
89 | 88 | FROM runtime-base AS runtime |
| 89 | +# This read-only build mount forces publication to depend on a scan of this |
| 90 | +# exact runtime-base filesystem without shipping the scanner or scan files. |
| 91 | +RUN --mount=type=bind,from=security-scan,source=/scan-passed,target=/scan-passed \ |
| 92 | + test -f /scan-passed |
0 commit comments