Apache HTTPd + Tomcat連携をDocker上で構築する

はじめに

今更感があるのですが、Apache HTTPd + Tomcatの連携を実装する必要があり、Docker上で実装してみることにしてみました。なお、ここで設定している内容は動作するための最小設定であるため、足りない設定などは適宜補足してください。

アプリの実装

Tomcat上で動くJavaアプリを作成します。あまり手間をかけたくなかったので、Spring Bootで以下のサンプルアプリを使って実装します。

spring.io

warファイルとしてパッケージ化したかったので、Spring InitializrでPackagingにwarを指定します。

https://start.spring.io/

これだけで、必要な設定がなされたファイル群が生成されます。

あとは実装して./gradlew warとしてwarファイルを生成、webappsフォルダ以下にapp.warとしてコピーしておきます。

設定ファイルの生成

Apache HTTPdTomcatの設定ファイルを生成します。ネット上には設定ファイルの断片しか記述されていないことが多く、それだけをコピペするとうまく動かないです。

今回はhttpd:2.4.57-alpinetomcat:10.1.11-jre17を使うことにしたので、以下のコマンドを実行します。

$ docker run --rm httpd:2.4.57-alpine cat /usr/local/apache2/conf/httpd.conf > my-httpd.conf
$ docker run --rm tomcat:10.1.11-jre17 cat /usr/local/tomcat/conf/server.xml > my-server.xml

設定ファイルの編集(Apache HTTPd

上記で生成した設定ファイルを編集します。まずはApache HTTPd。以下のLoadModuleの部分を有効化(先頭の#を削除)します。

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_ajp_module modules/mod_proxy_ajp.so

また、以下の通りVirtualHostの設定を追記します。ここでは、/appにアクセスがあったらAJPTomcatに対して処理を流すことを設定しています。

<VirtualHost *:80>
  ProxyPass /app ajp://tomcat:8009/app
</VirtualHost>

記述内にあるtomcatは後述するdocker-compose.yml内のservices内にあるTomcatの設定名と同じにします。

編集したファイルをconf/httpd.confとして保存します。

設定ファイルの編集(Tomcat

上記で生成した設定ファイルを編集します。ここではTomcat。以下の記述を追記します。

    <Connector protocol="AJP/1.3"
               secretRequired="false"
               address="0.0.0.0"
               port="8009"
               redirectPort="8080"
               maxParameterCount="1000"
               />

もともと<Connector protocol="AJP/1.3"で始まる部分のコメントを有効化するだけではなく、addressの部分を"0.0.0.0"としておく必要があります。これをしないと起動時に以下の例外を出力します。

 13-Aug-2023 04:56:03.128 SEVERE [main] org.apache.catalina.util.LifecycleBase.handleSubClassException Failed to initialize component [Connector[AJP/1.3-8009]]
    org.apache.catalina.LifecycleException: Protocol handler initialization failed
        at org.apache.catalina.connector.Connector.initInternal(Connector.java:1015)
        at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:136)
        at org.apache.catalina.core.StandardService.initInternal(StandardService.java:549)
        at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:136)
        at org.apache.catalina.core.StandardServer.initInternal(StandardServer.java:1011)
        at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:136)
        at org.apache.catalina.startup.Catalina.load(Catalina.java:747)
        at org.apache.catalina.startup.Catalina.load(Catalina.java:769)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
        at java.base/java.lang.reflect.Method.invoke(Unknown Source)
        at org.apache.catalina.startup.Bootstrap.load(Bootstrap.java:307)
        at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:477)
    Caused by: java.nio.channels.UnsupportedAddressTypeException
        at java.base/sun.nio.ch.Net.checkAddress(Unknown Source)
        at java.base/sun.nio.ch.ServerSocketChannelImpl.netBind(Unknown Source)
        at java.base/sun.nio.ch.ServerSocketChannelImpl.bind(Unknown Source)
        at org.apache.tomcat.util.net.NioEndpoint.initServerSocket(NioEndpoint.java:247)
        at org.apache.tomcat.util.net.NioEndpoint.bind(NioEndpoint.java:202)
        at org.apache.tomcat.util.net.AbstractEndpoint.bindWithCleanup(AbstractEndpoint.java:1278)
        at org.apache.tomcat.util.net.AbstractEndpoint.init(AbstractEndpoint.java:1291)
        at org.apache.coyote.AbstractProtocol.init(AbstractProtocol.java:622)
        at org.apache.catalina.connector.Connector.initInternal(Connector.java:1013)
        ... 13 more

また、secretRequired="false"も必要です。これがないと起動時に以下の例外が出力されます。

 13-Aug-2023 08:01:29.453 SEVERE [main] org.apache.catalina.util.LifecycleBase.handleSubClassException Failed to start component [Connector[AJP/1.3-8009]]
    org.apache.catalina.LifecycleException: Protocol handler start failed
        at org.apache.catalina.connector.Connector.startInternal(Connector.java:1046)
        at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
        at org.apache.catalina.core.StandardService.startInternal(StandardService.java:445)
        at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
        at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:918)
        at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
        at org.apache.catalina.startup.Catalina.start(Catalina.java:795)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
        at java.base/java.lang.reflect.Method.invoke(Unknown Source)
        at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:347)
        at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:478)
    Caused by: java.lang.IllegalArgumentException: The AJP Connector is configured with secretRequired="true" but the secret attribute is either null or "". This combination is not valid.
        at org.apache.coyote.ajp.AbstractAjpProtocol.start(AbstractAjpProtocol.java:271)
        at org.apache.catalina.connector.Connector.startInternal(Connector.java:1043)
        ... 12 more

最後に標準で開いている8080ポートを閉じます。以下の記述全体を<!---->で囲みます。

    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443"
               maxParameterCount="1000"
               />

結果として、以下のようなものとなります。

    <!--
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443"
               maxParameterCount="1000"
               />
    -->

編集したファイルをconf/server.xmlとして保存します。

docker-compose.ymlファイルを作成する

docker-compose.ymlファイルを作成します。内容は以下の通り。

version: '3'

services:
  httpd:
    image: httpd:2.4.57-alpine
    ports:
      - 8080:80
    volumes:
      - ./conf/httpd.conf:/usr/local/apache2/conf/httpd.conf
  tomcat:
    image: tomcat:10.1.11-jre17
    ports:
      - 8009
    volumes:
      - ./webapps:/usr/local/tomcat/webapps
      - ./conf/server.xml:/usr/local/tomcat/conf/server.xml

これで準備はOK。あとはdocker compose upを実行し、ブラウザでhttp://localhost:8080/appにアクセスしたら、Tomcat上で動いているアプリからの応答が確認できます。