Creating a multi stage docker docker (node, gradle and Java)

Hello,

I currently use Github Actions to do the following - checkout a repo, setup java (11), setup Gradle (5.6, I know it’s very old…), setup nodeJs and then run the gradle wrapper.

jobs:
  build_and_push:
    runs-on: [k8s-runner]
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-java@v3
        with:
          #distribution: temurin
          distribution: 'temurin'
          java-version: '11'
      - uses: actions/setup-node@v3
        with:
          node-version: 18
      - name: Setup Gradle
        uses: gradle/gradle-build-action@v2
        with:
          gradle-version: 5.6

      - name: Execute Gradle build
        run: ./gradlew clean build

I asked ChatGTP to covert these steps into a multi staged docker file.

This is what it rendered out (after I deleted the repo checkout stuff).

# Stage 1: Java and Node Setup
FROM adoptopenjdk:11-jdk-hotspot AS setup
RUN apt-get update && \
    apt-get install -y curl && \
    curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \
    apt-get install -y nodejs

# Stage 2: Gradle Build
FROM gradle:5.6 AS builder
WORKDIR /app
COPY . .
RUN ./gradlew clean build

# Stage 3: Final image
FROM adoptopenjdk:11-jre-hotspot
RUN addgroup --system userapp && \
    adduser -system --no-create-home --uid 1001 userapp --ingroup userapp

WORKDIR /app
COPY --from=builder /app /app

# Expose the application port
EXPOSE 8080

# Run the application
ENTRYPOINT ["java", "-jar", "/app/frontend.jar"]

However I’m seeing this error which is a NodeJS (npm) issue. It thinks Node isn’t installed.

#0 13.03 Starting a Gradle Daemon (subsequent builds will be faster)
#0 32.14 > Task :clean UP-TO-DATE
#0 32.23 > Task :bootBuildInfo
#0 62.23 > Task :compileJava
#0 71.83 > Task :nodeSetup SKIPPED
#0 71.83 > Task :npmSetup SKIPPED
#0 71.93 > Task :installGulp FAILED
#0 71.93 
#0 71.93 FAILURE: Build failed with an exception.
#0 71.93 
#0 71.93 * What went wrong:
#0 71.93 Execution failed for task ':installGulp'.
#0 71.93 > A problem occurred starting process 'command 'npm''
#0 71.93 
#0 71.93 * Try:
#0 71.93 Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
#0 71.93 
#0 71.93 * Get more help at https://help.gradle.org
#0 71.93 
#0 71.93 BUILD FAILED in 1m 11s
#0 71.93 4 actionable tasks: 3 executed, 1 up-to-date
------
Dockerfile:12
--------------------
  10 |     WORKDIR /app
  11 |     COPY . .
  12 | >>> RUN ./gradlew clean build
  13 |     
  14 |     # Stage 3: Final image
--------------------
ERROR: failed to solve: process "/bin/sh -c ./gradlew clean build" did not complete successfully: exit code: 1

Can anyone advise on the correct usages of when to use SETUP, BUILDER and BASE for my use case please?

The problem with ChatGPT is that people believe that it will create a valid code. It could sometimes, depending on what you ask it about, but you still need to interpret the result and ask back if something seems wrong. Then it will say something like “You are right. I apologise for the confusion. That’s the right code:” and can make a mistake again. For example I don’t see that you do anything with the “setup” stage where you installed nodejs.

The other issue I think is that you asked ChatGPT to create a multi-stage Dockerfile from the GitHub actions and it didn’t tell you that might not what you need or the steps are not easily convertable to stages so it created something that looks like a multi-stage build and contains similar lines that the GitHub actions had.

If you want a final image that contains Java and nodejs, choose a base image either nodejs or java and install the missing dependencies in it in one stage. Multistage build could be good for gradle if it creates files that compatible with the base image into which you copy the result, but installed packages in a previous stage will not be installed in the other stages. That is basically one of the points of multi-stage builds :slight_smile:

I will try to modify your dockerfile without testing, so it might not work either but can give you an idea:

# Stage 1: Gradle Build
FROM gradle:5.6 AS builder
RUN apt-get update && \
    apt-get install -y curl && \
    curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \
    apt-get install -y nodejs
WORKDIR /app
COPY . .
RUN ./gradlew clean build

# Stage 2: Final image
FROM adoptopenjdk:11-jre-hotspot
RUN addgroup --system userapp && \
    adduser -system --no-create-home --uid 1001 userapp --ingroup userapp

WORKDIR /app
COPY --from=builder /app /app

# Expose the application port
EXPOSE 8080

# Run the application
ENTRYPOINT ["java", "-jar", "/app/frontend.jar"]

Hi @rimelek!

Thanks for your reply - yes always take ChatGPT and co with a pinch of salt but to fair it did point me in the right direction.

I managed to get it working with this rather ugly / inelegant solution

# Stage 1: Java and Node Setup
FROM adoptopenjdk:11-jdk-hotspot AS builder
RUN apt-get update && \
    apt-get install -y curl && \
    curl -fsSL https://deb.nodesource.com/setup_16.x | bash - && \
    apt-get install -y nodejs

RUN apt-get update && apt-get install -y unzip
WORKDIR /gradle
RUN curl -L https://services.gradle.org/distributions/gradle-5.6.1-bin.zip -o gradle-5.6.1-bin.zip
RUN unzip gradle-5.6.1-bin.zip
ENV GRADLE_HOME=/gradle/gradle-5.6.1
ENV PATH=$PATH:$GRADLE_HOME/bin
RUN gradle --version

# # Stage 2: Gradle Build
# FROM gradle:5.6 AS builder
WORKDIR /app
COPY . .
RUN ./gradlew clean build

# Stage 3: Final image
FROM adoptopenjdk:11-jre-hotspot

RUN addgroup --system bmjapp && \
    adduser -system --no-create-home --uid 1001 appuser --ingroup appuser

# Set the working directory
WORKDIR /app

# Copy the built application from the builder stage
COPY --from=builder /app/build/libs/frontend-1.0.jar /app

# Expose the application port
EXPOSE 8080

# Run the application
ENTRYPOINT ["java","-jar","/app/frontend-1.0.jar"]

I’ve just tried your version and it also compiled - however the image is double the size :astonished:

REPOSITORY              TAG       IMAGE ID       CREATED         SIZE
bp-fe                   forum     7977d595cc4e   4 minutes ago   827MB
bp-fe                   latest    48c887f5378a   18 hours ago    319MB

I didn’t change the COPY instruction because I had no idea what it contained. In your version you copy only a single jar file not the whole /app folder. You could do that with “my version” as well, which was basically your version :slight_smile: as I didn’t change anything in the last stage and that is the only stage which had the COPY instruction.

So the choice is yours. If you think the code you shared is not elegant, you can just change the COPY instruction in the other code I shared.

Thanks, your version with my COPY command is a winner! I actually needed node 16!

# Stage 1: Gradle Build
FROM gradle:5.6 AS builder
RUN apt-get update && \
    apt-get install -y curl && \
    curl -fsSL https://deb.nodesource.com/setup_16.x | bash - && \
    apt-get install -y nodejs
WORKDIR /app
COPY . .
RUN ./gradlew clean build

# Stage 2: Final image
FROM adoptopenjdk:11-jre-hotspot
RUN addgroup --system userapp && \
    adduser -system --no-create-home --uid 1001 userapp --ingroup userapp

# Set the working directory
WORKDIR /app

# Copy the built application from the builder stage
COPY --from=builder /app/build/libs/app-frontend-1.0.jar /app

# Expose the application port
EXPOSE 8080

# Run the application
ENTRYPOINT ["java","-jar","/app/app-frontend-1.0.jar"]