Es una aplicación sencilla que solo tiene un endpoint /hello/${name}
que devuelve Hello, <name>.
El objetivo del proyecto es poder ejecutar la aplicación con un docker run -p 8081:8081 mi-scala-app
. Para ello se muestran diferentes opciones.
Cada opción está en un branch distinto basado en master para que se pueda hacer un diff y ver que archivos se agregaron o modificaron: git diff master..HEAD
git checkout assembly_docker_single_stage
Requisitos:
- jdk
- sbt
- docker
Pasos:
- Agregar
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.9")
- Crear un archivo
Dockerfile
en la raiz de proyecto
FROM openjdk:8u201-jre-alpine3.9
RUN mkdir /app
COPY ./target/scala-2.12/mi-scala-app-assembly-*.jar /app/
WORKDIR /app
ENTRYPOINT java -jar mi-scala-app-assembly-*.jar
- Ejecutar
sbt assembly
- Ejecutar
docker build . -t mi-scala-app
- Enjoy
docker run -p 8081:8081 mi-scala-app
Ventajas:
- Se aprovecha la cache de .ivy
Desventajas:
- Se requiere tener jdk instalada y es tedioso si se necesita otra versión de jdk
Se puede reducir la cantidad de archivos que se envían al docker daemon agregando un .dockerignore
:
*
!target/scala-2.12/mi-scala-app*.jar
git checkout assembly_docker_single_stage_opt
- Copiar las depdendencias por separado para aprovechar la cache de docker:
FROM openjdk:8u201-jre-alpine3.9
RUN mkdir /app
COPY ./target/scala-2.12/mi-scala-app-assembly-*-deps.jar /app/lib/
COPY ./target/scala-2.12/mi-scala-app_2.12-*.jar /app/lib/
WORKDIR /app
ENTRYPOINT java -cp "lib/*" com.example.MainApp
- Ejecutar
sbt assemblyPackageDependency
- Ejecutar
sbt package
- Ejecutar
docker build . -t mi-scala-app
- Enjoy
docker run -p 8081:8081 mi-scala-app
git checkout assembly_docker_single_stage_opt_distroless
- Construir una imagen distroless. Esto aporta mayor seguridad:
FROM gcr.io/distroless/java:8
COPY ./target/scala-2.12/mi-scala-app-assembly-*.jar /app/
WORKDIR /app
CMD ["mi-scala-app-assembly-0.1.jar"]
Ventajas:
- Imagen más segura.
Desventajas:
- No se tiene acceso al container.
- Es necesario poner el nombre completo del jar, sin wildcards.
git checkout assembly_docker_multi_stage
Requisitos:
- docker
Pasos:
- Agregar
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.9")
- Crear un archivo
Dockerfile
en la raiz de proyecto
FROM hseeberger/scala-sbt:8u181_2.12.8_1.2.8 as building
RUN mkdir /building
WORKDIR /building
COPY . /building
RUN sbt assembly
FROM openjdk:8u201-jre-alpine3.9
RUN mkdir /app
COPY --from=building /building/target/scala-2.12/mi-scala-app-assembly-*.jar /app/
WORKDIR /app
ENTRYPOINT java -jar mi-scala-app-assembly-*.jar
- (optimización) Agregar un .dockerignore:
target
.idea
.git
**/target
- Ejecutar
docker build . -t mi-scala-app
- Enjoy
docker run -p 8081:8081 mi-scala-app
Ventajas:
- Solo se necesita docker para el building de la imagen (útil para CI)
Desventajas:
- No se aprovecha la caché de .ivy. sbt se toma su tiempo cuando no tiene caches
git checkout assembly_docker_multi_stage_opt1
- Crear un Dockerfile multistage y montar una cache de ivy2 para que reutilice las dependencias bajadas previamente:
# syntax = docker/dockerfile:experimental
FROM hseeberger/scala-sbt:8u181_2.12.8_1.2.8 as building
COPY . /building
RUN --mount=type=cache,id=ivy2,target=/root/.ivy2 \
cd /building && sbt package
FROM hseeberger/scala-sbt:8u181_2.12.8_1.2.8 as building_dependencies
COPY project /building/project/
COPY build.sbt /building/
COPY version.sbt /building/
RUN --mount=type=cache,id=ivy2,target=/root/.ivy2 \
cd /building && sbt assemblyPackageDependency
FROM openjdk:8u201-jre-alpine3.9
RUN mkdir /app
COPY --from=building_dependencies /building/target/scala-2.12/mi-scala-app-assembly-*-deps.jar /app/lib/
COPY --from=building /building/target/scala-2.12/mi-scala-app_2.12*.jar /app/lib/
WORKDIR /app
ENTRYPOINT java -cp "lib/*" com.example.MainApp
- Ejecutar
docker build
habilitando buildkit:
DOCKER_BUILDKIT=1 docker build . -t mi-scala-app
Ventajas:
- Reutiliza la cache de .ivy2.
Desventajas:
- ivy tiene el nefasto .lock por lo que no deberíamos utilizar concurrentemente la cache.
git checkout assembly_docker_multi_stage_opt2
- Crear un dockerfile multistage utilizando coursier para bajar las dependencias en paralelo:
# syntax = docker/dockerfile:experimental
FROM hseeberger/scala-sbt:8u181_2.12.8_1.2.8 as sbt_with_coursier
RUN --mount=type=cache,id=coursier,target=/root/.cache/coursier/v1 \
mkdir -p /root/.sbt/1.0/plugins/ && \
echo 'addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.1.0-M14")' >> /root/.sbt/1.0/plugins/build.sbt && \
sbt version && rm -f /root/.ivy2/.sbt.ivy.lock
FROM sbt_with_coursier as building
COPY . /building
RUN --mount=type=cache,id=coursier,target=/root/.cache/coursier/v1 \
cd /building && sbt package
FROM sbt_with_coursier as building_dependencies
COPY project /building/project/
COPY build.sbt /building/
COPY version.sbt /building/
RUN --mount=type=cache,id=coursier,target=/root/.cache/coursier/v1 \
cd /building && sbt assemblyPackageDependency
FROM openjdk:8u201-jre-alpine3.9 as final
RUN mkdir /app
COPY --from=building_dependencies /building/target/scala-2.12/mi-scala-app-assembly-*-deps.jar /app/lib/
COPY --from=building /building/target/scala-2.12/mi-scala-app_2.12*.jar /app/lib/
WORKDIR /app
ENTRYPOINT java -cp "lib/*" com.example.MainApp
- Ejecutar:
DOCKER_BUILDKIT=1 docker build . -t81 mi-scala-app
git checkout assembly_and_sbt_docker
Requisitos:
- jdk
- sbt
- docker Pasos:
- Agregar los plugins:
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.9")
addSbtPlugin("se.marcuslonnberg" % "sbt-docker" % "1.5.0")
- Agregar la configuración de sbt-docker al `build.sbt:
lazy val root = (project in file("."))
//...
.enablePlugins(DockerPlugin)
.settings(
dockerfile in docker := {
// The assembly task generates a fat JAR file
val artifact: File = assembly.value
val artifactTargetPath = s"/app/${artifact.name}"
new Dockerfile {
from("openjdk:8-jre")
add(artifact, artifactTargetPath)
entryPoint("java", "-jar", artifactTargetPath)
}
}
)
- Ejecutar
sbt docker
Ventajas:
- Todo integrado dentro de sbt
- No tengo que ejecutar el comando
docker
Desventajas:
- Necesito tener docker, sbt y jdk.
- Manejo de versiones de imagen integrado
git checkout assembly_and_sbt_docker_mejora
En algún momento es necesario publicar la imagen. Para ello tenemos que agregar a la imagen el nombre del repo y el tag.
- Agregar al
build.sbt
:
lazy val root = (project in file("."))
//...
.settings(
imageNames in docker := Seq(
ImageName(
registry = Some("localhost:5000"),
//namespace = Some(organization.value),
repository = name.value,
tag = Some(version.value)
),
ImageName(
registry = Some("localhost:5000"),
//namespace = Some(organization.value),
repository = name.value,
tag = Some("latest")
)
)
git checkout sbt_native_packager
Requisitos:
- jdk
- sbt
- docker
- Agregar el plugin:
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.4")
- Agregar al build.sbt:
lazy val root = (project in file("."))
//...
.enablePlugins(JavaAppPackaging)
- Ejecutar
sbt docker:publishLocal
Ventajas:
- Simple, todo desde sbt. Poca configuración inicial.
Desventajas:
- La imagen que genera está basada en
openjdk:latest
, muy grande ylatest
git checkout sbt_native_packager_mejora1
- Usar una imagen más pequeña. Es necesario habilitar el plugin
AshScriptPlugin
para que el script generado por sbt-native-packager no requiera bash sino ash. - No producir archivos .bat.
- Generar el tag latest
- Especificar el registry privado para nuestras imágenes. Luego con
sbt docker:publish
podemos publicarla en el registry (para levantar un registry local ejecutar:docker run -p 5000:5000 registry:2
). - Cambiar el directorio base para nuestra app.
.enablePlugins(AshScriptPlugin)
.settings(
dockerBaseImage := "openjdk:8u201-jre-alpine3.9",
makeBatScripts := Seq(),
dockerUpdateLatest := true,
dockerRepository := Some("localhost:5000"),
defaultLinuxInstallLocation in Docker := "/app",
dockerExposedPorts := Seq(8081)
)
git checkout sbt_native_packager_mejora2
- Según twelve factor app conviene utilizar variables de entorno para la configuración.
- Agregar al
build.sbt
:
bashScriptExtraDefines ++= Seq(
"""addJava "-Dconfig.file=${app_home}/../conf/application.conf"""",
"""addJava "-Dlogback.configurationFile=${app_home}/../conf/logback.xml"""",
)
- Para ejecutar se necesitará pasar las variables
HOST
yPORT
:
docker run --rm -p 8081:8081 -e HOST=0.0.0.0 -e PORT=8081 localhost:5000/mi-scala-app:0.1
- Como alternativa se puede montar un archivo
/tmp/application.conf
mi-scala-app {
server-config {
host = "0.0.0.0"
port = 8081
}
}
Utilizando el siguiente comando:
docker run --rm -p 8081:8081 -v /tmp/application.conf:/app/conf/application.conf:ro localhost:5000/mi-scala-app:0.1