#docker #devops
π’ Containerizing a Java Microservice ποΈ
This is the Ad microservice from OpenTelemetry. π
π Easy Steps to Run the Microservice π οΈ
(Usually provided by developers) π¨βπ»
π Multi-Stage Docker Build for a Java App (using Gradle)
Stage 1: Build Stage (Builder Image) ποΈ
This stage compiles the Java application using Gradle.
1οΈβ£ Use a JDK Base Image π₯οΈ -> Uses eclipse-temurin:21-jdk
to build the app.
2οΈβ£ Set Working Directory π -> Sets /usr/src/app/
as the working directory.
3οΈβ£ Copy Gradle Files π -> Copies gradlew
, settings.gradle
, build.gradle
, and the gradle
directory.
4οΈβ£ Give Execute Permission π§ -> Runs chmod +x ./gradlew
to allow execution.
5οΈβ£ Download Dependencies π¦ -> Runs ./gradlew
to set up Gradle and fetch dependencies.
6οΈβ£ Copy Source Code π -> Copies project source code and pb
directory (for protocol buffers).
7οΈβ£ Build the Application βοΈ -> Runs ./gradlew installDist -PprotoSourceDir=./proto
to compile & package.
Stage 2: Runtime Stage (Final Image) π¦
This stage creates a lightweight image to run the app.
1οΈβ£ Use a Minimal JRE Base Image ποΈββοΈ -> Uses eclipse-temurin:21-jre
to reduce size.
2οΈβ£ Set Working Directory π -> Uses /usr/src/app/
as the working directory.
3οΈβ£ Copy Built App π₯ -> Copies the compiled app from the builder stage.
4οΈβ£ Set Environment Variables π -> Sets AD_PORT=9099
.
5οΈβ£ Run the Application βΆοΈ -> Executes ./build/install/opentelemetry-demo-ad/bin/Ad
.
π Why Not Just Use COPY . .
?
We can use COPY . .
, but it's not the best approach in this case.
When building a Docker image, caching is crucial for speeding up builds. Using
COPY . .
copies everything in the build context, which can lead to inefficient caching.
β οΈ Problems with COPY . .
Breaks Docker Layer Caching:
If any file in your project changes (even an unrelated one), Docker invalidates all cached layers and re-runs all subsequent steps, including dependency downloads and compilation.
Since Gradle dependencies donβt change as often as source code, we should separate them for better caching.
Unnecessary Files Get Copied:
Copies files like
.git
, logs, IDE settings, and other junk.This increases build time and image size.
π Why we donβt combine these two COPY
commands
COPY ./src/ad/gradlew* ./src/ad/settings.gradle* ./src/ad/build.gradle ./
COPY ./src/ad/gradle ./gradle
into a single command like this:
COPY ./src/ad/gradlew* ./src/ad/settings.gradle* ./src/ad/build.gradle ./src/ad/gradle ./
π The Key Reason: COPY
Doesn't Expand *
for Directories
The reason this won't work as expected is that COPY
treats wildcards (*
) differently for files vs. directories:
Wildcards (
*
) only work for files, NOT directories.COPY ./src/ad/gradlew* ./src/ad/settings.gradle* ./src/ad/build.gradle ./
β (Works β these are files.)COPY ./src/ad/gradle ./
β (Fails βgradle
is a directory.)
If you include a directory (
./src/ad/gradle
), the wildcard will NOT be expanded, and Docker will throw an error.
π Why Are Wildcards (*
) Used Here?
COPY ./src/ad/gradlew* ./src/ad/settings.gradle* ./src/ad/build.gradle ./
The
gradlew*
wildcard is likely used because there might be two files:gradlew
(Linux/macOS)gradlew.bat
(Windows)
Using
gradlew*
ensures both are copied if they exist.