Containerization of a Java based microservice

Containerization of a Java based microservice

Β·

3 min read

#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 . .

  1. 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.

  2. 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:

  1. 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.)

  2. 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.

Β