dockerを使ってJavaの開発しているとき、時折docker buildの遅さが気になる。
遅さというのは、ほぼ mvn package
実行時の依存解決にかかる時間に対して言っています。
というのも、例えば以下のようなDockerfileがあったとき
# Dockerfile FROM maven:3.6.2-jdk-13 as builder COPY . /usr/src/ WORKDIR /usr/src/ RUN mvn -B package FROM openjdk:13-alpine COPY --from=builder /usr/src/target/example-1.0.0.jar / ENV CLASSPATH /example-1.0.0.jar ENTRYPOINT [] CMD ["java", "-jar", "Example"]
依存量にもよりますが、 mvn -B package
のところで2分半~3分半ほどかかります。
docker buildを叩く度、毎回数分以上かかるのは、さすがに開発効率が悪すぎる。
~/.m2/
を永続化しようとしても、docker buildではvolume mountできませんし。
ちょっと、解決策を模索してみました。
docker buildのビルドキャッシュを活用してみる
docker build時には、ビルドキャッシュが効くはずなので、効くようにしてみました。
# Dockerfile FROM maven:3.6.2-jdk-13 as builder WORKDIR /usr/src/ # pom.xmlだけCOPYし、先に依存解決 COPY ./pom.xml /usr/src/ RUN mvn -B dependency:resolve dependency:resolve-plugins # ビルドで必要となるファイルのみCOPY COPY ./src/ /usr/src/src/ RUN mvn -B package FROM openjdk:13-alpine COPY --from=builder /usr/src/target/example-1.0.0.jar / ENV CLASSPATH /example-1.0.0.jar ENTRYPOINT [] CMD ["java", "-jar", "Example"]
結果、初回のdocker buildに4分半ほど、2回目以降のdocker buildでは1分~1分半ほどまで高速化しました。
mvn -B dependency:resolve dependency:resolve-plugins
では、 mvn -B package
に必要な依存が含まれていないため、
mvn -B package
時にまだ依存解決をする必要があるため、分単位で時間がかかっているようです。
ということは、依存解決のために mvn -B package
を先に実行してやれば良いはず。
# Dockerfile FROM maven:3.6.2-jdk-13 as builder WORKDIR /usr/src/ # pom.xmlだけCOPYし、先に依存解決 COPY ./pom.xml /usr/src/ RUN mvn -B package; echo "" # ビルドで必要となるファイルのみCOPY COPY ./src/ /usr/src/src/ RUN mvn -B package FROM openjdk:13-alpine COPY --from=builder /usr/src/target/example-1.0.0.jar / ENV CLASSPATH /example-1.0.0.jar ENTRYPOINT [] CMD ["java", "-jar", "Example"]
mvn -B package; echo ""
としている理由は、依存をキャッシュすることが目的のため、ビルドエラーなんかでキャッシュできなかったら悲しいからです。
これで、初回のdocker buildeに2分半~3分半ほど、2回目以降のdocker buildは5秒ほどになりました。
ローカルで実行したのと変わりない時間で実行できるようになりました。
ビルド用のコンテナを立ててみる
ビルドキャッシュを効かせれば目的は達成できますが、別解としてビルド用のコンテナを立ててみる方法を試しました。 今回はMakefileと組み合わせてみます。
# Makefile CONTAINER_NAME=builder .PHONY: package package: up-builder docker exec "${CONTAINER_NAME}" mvn -B package .PHONY: up-builder up-builder: if [ -z "$$(docker ps --filter "name=${CONTAINER_NAME}" --all --quiet)" ]; then \ docker run --rm --detach \ --name "${CONTAINER_NAME}" \ --volume '${CURDIR}:/workspace' \ --workdir '/workspace' \ maven:3.6.2-jdk-13 \ tail -f /dev/null; fi .PHONY: down-builder down-builder: docker stop "${CONTAINER_NAME}"
# Dockerfile FROM openjdk:13-alpine COPY ./target/example-1.0.0.jar / ENV CLASSPATH /example-1.0.0.jar ENTRYPOINT [] CMD ["java", "-jar", "Example"]
これで make package
を実行すれば、自動的にビルド用のコンテナがバックグラウンドで起動し、コンテナは止めない限り起動し続けるため、あまり難しいことを考えずとも2回目以降のキャッシュが効きます。
また実質的に、 maven:3.6.2-jdk-13
を使ってビルドした成果物を openjdk:13-alpine
にCOPYしているため、docker buildの結果生成されるdocker imageは同じものだと期待できます。
このようにすると、ビルドキャッシュを効くようにしたときと同様、初回のdocker buildeに2分半~3分半ほど、2回目以降のdocker buildは5秒ほどになります。
とはいえ、例えばwindowsで開発しているときに、誤ってwindows上で mvn package
を実行した成果物をCOPYし、誤ったdocker imageを生成してしまうリスクがあります。
個人的には、Dockerfile内でなんとかできるなら、そっちのほうが好みですね。
ケースバイケースですけれど。