開発時にdockerを使ってmavenをいい感じに動かす

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内でなんとかできるなら、そっちのほうが好みですね。 ケースバイケースですけれど。