Subsystem
Server
Is your feature request related to a problem? Please describe.
I see that the ktor Docker deployment docs show a Dockerfile similar to the official Docker Labs docs. Currently the JAR is built on the developer's machine, and then the Dockerfile uses the Docker COPY command to copy it to the Docker machine. I understand that the JVM already has excellent isolation (even without Docker), and that it's rare for the developer to not have a compatible JVM or JRE already installed (for running a command such as ./gradlew build). But it would be great if we could have the Dockerfile create the JAR by itself so as to fully benefit from containerization (e.g., reduce the deployment steps from ./gradlew build + docker build + docker run to just docker build + docker run).
Describe the solution you'd like
It'd be great if ktor's docs could show how to create the image using a Dockerfile which creates the JAR itself.
Motivation to include to ktor
I managed to make my own multistage build like:
FROM openjdk:8 AS builder
WORKDIR /app
COPY gradle/ gradle/
COPY src/main/ src/main/
COPY build.gradle.kts gradle.properties gradlew settings.gradle.kts ./
RUN ./gradlew shadowJar
FROM openjdk:8-jre-alpine
RUN apk --no-cache add curl
COPY --from=builder /app/build/libs/*.jar crystal-skull-all.jar
COPY --from=builder /app/build/resources/main/ src/main/resources/
ENV PORT 80
EXPOSE 80
HEALTHCHECK --timeout=5s --start-period=5s --retries=1 \
CMD curl -f http://localhost:$PORT/health_check
CMD [ \
"java", \
"-server", \
"-XX:+UnlockExperimentalVMOptions", \
"-XX:+UseCGroupMemoryLimitForHeap", \
"-XX:InitialRAMFraction=2", \
"-XX:MinRAMFraction=2", \
"-XX:MaxRAMFraction=2", \
"-XX:+UseG1GC", \
"-XX:MaxGCPauseMillis=100", \
"-XX:+UseStringDeduplication", \
"-jar", \
"crystal-skull-all.jar" \
]
but Gradle takes something like ten minutes to build the JAR inside the container. I've tried allocating the maximum possible resources for Docker for Mac, as well as use newer OpenJDK images to no avail. I even discussed it with the Gradle image author on this issue but couldn't figure it out.
I think this issue should be moved to ktorio.github.io.
I finally got it working, there were a lot of gotchas, but I still haven't figured out how to run as a non-root user.
# Gradle use carriage returns when logging by default, which aren't visible during Docker builds. Since gradle
# invocations take several minutes, it looks like it froze. Hence, we set the log level to info to see the progress.
FROM gradle:jdk8 AS builder
WORKDIR /app
# Without this environment variable, Gradle will not cache dependencies. When building this image without cached layers,
# it would seem that the environment variable is unnecessary, because the second Gradle task uses the cache from the
# first Gradle task. This is because they execute in the same context. When building this image with only the first
# Gradle task's layer cached, the second Gradle task will unnecessarily redownload dependencies.
ENV GRADLE_USER_HOME /cache
COPY build.gradle.kts gradle.properties settings.gradle.kts ./
# The shadowJar task maximizes cacheable outputs when compared to the build task, or the --refresh-dependencies flag.
RUN gradle --no-daemon -i shadowJar
COPY src/main/ src/main/
RUN gradle --no-daemon -i shadowJar
FROM openjdk:8-jre-alpine
WORKDIR /app
COPY --from=builder /app/build/libs/app-all.jar .
COPY --from=builder /app/build/resources/main/ src/main/resources/
COPY docker/ docker/
# VisualVM uses port 9010.
EXPOSE 80 443 9010
RUN apk --no-cache add bash curl
HEALTHCHECK --timeout=5s --start-period=5s --retries=1 \
CMD curl -f http://localhost:80/health_check
CMD [ \
"java" \
"-server" \
"-XX:+UnlockExperimentalVMOptions" \
"-XX:+UseCGroupMemoryLimitForHeap" \
"-XX:InitialRAMFraction=2" \
"-XX:MinRAMFraction=2" \
"-XX:MaxRAMFraction=2" \
"-XX:+UseG1GC" \
"-XX:MaxGCPauseMillis=100" \
"-XX:+UseStringDeduplication" \
"-jar" \
"app-all.jar" \
]
Perhaps we could remove things like the profiler and HEALTHCHECK.
The Docker docs don't show anything ktor-specific. It's only there to help beginners. A "better" Dockerfile could be provided since it's for beginners, but then different people would have different opinions on what the content should be. For example, even though a multistage build would help someone starting out with containers, one might argue that it would do more harm since the target audience apparently doesn't even know about Docker, and so readers may think it's too complicated. Of course, the issue of what a "beginner" is plagues every community (e.g., Discord's API docs even document how to create variables in JavaScript).
So I think the Docker docs are fine as they are, and I'll close this.
Since someone seems to have upvoted my previous comment, I'll leave my current Dockerfile here in case it helps someone. Note that I am targeting JVM 10+ bytecode so that I (supposedly) don't need the experimental compiler options.
FROM gradle:6.4.0-jdk14 AS builder
WORKDIR /home/gradle/project
COPY build.gradle.kts gradle.properties settings.gradle.kts ./
RUN gradle --no-daemon -i shadowJar
COPY src/main/ src/main/
RUN gradle --no-daemon -i shadowJar
FROM adoptopenjdk:14-jre-hotspot
WORKDIR /app
COPY --from=builder /home/gradle/project/build/libs/omni-chat-all.jar .
EXPOSE 80/tcp
HEALTHCHECK --timeout=5s --start-period=5s --retries=1 \
CMD wget -q http://localhost:80/health_check || exit 1
CMD java -server -jar omni-chat-all.jar
Most helpful comment
I think this issue should be moved to ktorio.github.io.
I finally got it working, there were a lot of gotchas, but I still haven't figured out how to run as a non-root user.
Perhaps we could remove things like the profiler and
HEALTHCHECK.