以下の内容はhttps://kamatimaru.hatenablog.com/entry/2025/01/18/140723より取得しました。


Docker Compose環境でKeycloakの前段にリバースプロキシサーバーを立てる(Nginx、Caddyで検証)

前提

  • アクセスログを取得したいなどの理由で、docker compose環境でKeycloakの前段にリバースプロキシサーバーを立てたくなった
  • 使用したKeycloakのDockerイメージ: quay.io/keycloak/keycloak:26.0.6

まずはNginx

まずは使い慣れているNginxでやろうとした。Dockerイメージはnginx:1.27.3を使用。

compose.yml

services:
  proxy:
    image: nginx:1.27.3
    ports:
      - "18080:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf

  keycloak:
    image: quay.io/keycloak/keycloak:26.0.6
    command:
      - start-dev
    environment:
      KC_BOOTSTRAP_ADMIN_USERNAME: admin
      KC_BOOTSTRAP_ADMIN_PASSWORD: password
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://keycloak_db:5432/keycloak
      KC_DB_USER: keycloak
      KC_DB_PASSWORD: password
      KC_HOSTNAME: http://localhost:18080
    depends_on:
      keycloak_db:
        condition: service_healthy

  keycloak_db:
    image: postgres:16
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: password
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "admin"]
      timeout: 1s
      retries: 10

nginx.conf

worker_processes 1;

events {
    worker_connections 1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    server {
        listen 80;
        server_name proxy;
        
        location / {
            proxy_pass http://keycloak:8080;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

結果

  • 前提: keycloakはポスグレコンテナをwaitしてから起動する
  • → コンテナの起動順がnginx > keycloakのため、host not found in upstreamが発生してnginxのコンテナが異常終了してしまう
$ docker compose up
...省略
proxy-1        | 2025/01/18 01:54:59 [emerg] 1#1: host not found in upstream "keycloak" in /etc/nginx/nginx.conf:16
proxy-1        | nginx: [emerg] host not found in upstream "keycloak" in /etc/nginx/nginx.conf:16
...省略

keycloakコンテナのwaitを試みる

以下の公式ドキュメントとZennの記事を参考に、depends_onhealthcheckを使って以下のようにnginxコンテナがkeycloakコンテナをwaitするように試みた。

www.keycloak.org

zenn.dev

services:
  proxy:
    # 省略
    depends_on: # 追加
      keycloak:
        condition: service_healthy
  # 省略
  keycloak:
    # 省略
    environment:
      # 省略
      KC_HEALTH_ENABLED: true # 追加
    # 省略
    healthcheck: # 追加
      test: ["CMD", "curl", "--head", "-fsS", "localhost:8080/health/ready"]
      interval: 1s
      retries: 10

結果は、以下のようにkeycloakコンテナがhealthyにならず、起動に失敗した。

dependency failed to start: container keycloak-proxy-poc-keycloak-1 is unhealthy

$ docker ps
CONTAINER ID   IMAGE                              COMMAND                   CREATED         STATUS                     PORTS                          NAMES
e3c71923a3be   quay.io/keycloak/keycloak:26.0.6   "/opt/keycloak/bin/k…"   3 minutes ago   Up 3 minutes (unhealthy)   8080/tcp, 8443/tcp, 9000/tcp   keycloak-proxy-poc-keycloak-1
7ca44f2f1438   postgres:16                        "docker-entrypoint.s…"   3 minutes ago   Up 3 minutes (healthy)     5432/tcp                       keycloak-proxy-poc-keycloak_db-1

docker inspectで調査すると、コンテナ内でcurlコマンドがnot foundになっていた。

OCI runtime exec failed: exec failed: unable to start container process: exec: \"curl\": executable file not found in $PATH: unknown

$ docker inspect e3c71923a3be
...省略
       "State": {
            "Status": "running",
            "Running": true,
            "Paused": false,
            "Restarting": false,
            "OOMKilled": false,
            "Dead": false,
            "Pid": 35784,
            "ExitCode": 0,
            "Error": "",
            "StartedAt": "2025-01-18T02:24:47.83521138Z",
            "FinishedAt": "0001-01-01T00:00:00Z",
            "Health": {
                "Status": "unhealthy",
                "FailingStreak": 337,
                "Log": [
                    {
                        "Start": "2025-01-18T11:30:36.773188347+09:00",
                        "End": "2025-01-18T11:30:36.821814797+09:00",
                        "ExitCode": -1,
                        "Output": "OCI runtime exec failed: exec failed: unable to start container process: exec: \"curl\": executable file not found in $PATH: unknown"
                    },
                    {
                        "Start": "2025-01-18T11:30:37.823004357+09:00",
                        "End": "2025-01-18T11:30:37.872562768+09:00",
                        "ExitCode": -1,
                        "Output": "OCI runtime exec failed: exec failed: unable to start container process: exec: \"curl\": executable file not found in $PATH: unknown"
                    },
                    {
                        "Start": "2025-01-18T11:30:38.873552379+09:00",
                        "End": "2025-01-18T11:30:38.921239493+09:00",
                        "ExitCode": -1,
                        "Output": "OCI runtime exec failed: exec failed: unable to start container process: exec: \"curl\": executable file not found in $PATH: unknown"
                    },
                    {
                        "Start": "2025-01-18T11:30:39.926112776+09:00",
                        "End": "2025-01-18T11:30:39.973088461+09:00",
                        "ExitCode": -1,
                        "Output": "OCI runtime exec failed: exec failed: unable to start container process: exec: \"curl\": executable file not found in $PATH: unknown"
                    },
                    {
                        "Start": "2025-01-18T11:30:40.974976076+09:00",
                        "End": "2025-01-18T11:30:41.01868977+09:00",
                        "ExitCode": -1,
                        "Output": "OCI runtime exec failed: exec failed: unable to start container process: exec: \"curl\": executable file not found in $PATH: unknown"
                    }
                ]
            }
        },
...省略

公式ドキュメントやissueの議論を読むと、以下のようなことらしい

  • セキュリティを考慮してアタックサーフェスを減らすため、bashとKeycloakを実行するのに必要な最小限のパッケージしかインストールされていない
  • microdnf, dnf, rpmといったパッケージ管理用のコマンドもインストールされていない
If you try to install new software in a stage FROM quay.io/keycloak/keycloak, you will notice that microdnf, dnf, and even rpm are not installed. 

www.keycloak.org

github.com

Nginx以外のリバースプロキシを試す

  • keycloakのイメージをカスタマイズしてcurlを使えるようにするのも大変そう
  • → やりたいことは何らかのリバースプロキシを立てることであって、Nginxである必要はない
  • → リバースプロキシ先のホストが立ち上がってない場合にwaitしてくれるリバースプロキシ製品があるならそれを使えばいいのでは?

と考えて、ChatGPTに相談してみた。すると、以下のようなプロダクトを紹介してくれた。

Envoyは名前だけ知ってたが、他の2つは初めて聞いた。

まずはChatGPT曰く設定がシンプルというCaddyを試してみる

caddyserver.com

Caddyで再チャレンジ

Caddyも公式のDockerイメージが存在する。

https://hub.docker.com/_/caddy

nginx部分を以下のように書き換える。

compose.yml

services:
  proxy:
    image: caddy:2.9.1
    ports:
      - "18080:80"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
  # ...省略

Caddyの場合は、Caddyfileというのが設定ファイルの名前らしい。 以下のように公式ドキュメントを参考にしつつみよう見真似で書いてみる。

デフォルトだと443ポートで起動する仕様だったので、80ポートで起動させたい場合は明示が必要だった。

Caddyfile

localhost:80 {
  reverse_proxy keycloak:8080 {
    health_uri /health/ready
    health_interval 1s
    health_timeout 3s
  }
}

proxyコンテナのdepends_onkeycloakコンテナのhealthcheckは不要なはずなので削除する。

  • 起動すると、Caddyは何度もリトライしてくれていて、途中でno such hostからconnection refusedに切り替わっている(=ホストは認識できている)。
    • → Nginxで詰まった課題は乗り越えられた。
  • ただ、次にhttp://keycloak:8080/health/readyが404になってしまっている。

{"level":"info","ts":1737170935.4018724,"logger":"http.handlers.reverse_proxy.health_checker.active","msg":"status code out of tolerances","status_code":404,"host":"keycloak:8080"}

$ docker compose up
...省略
proxy-1        | {"level":"info","ts":1737170894.3574111,"logger":"http.handlers.reverse_proxy.health_checker.active","msg":"HTTP request failed","host":"keycloak:8080","error":"Get \"http://keycloak:8080/health/ready\": dial tcp: lookup keycloak on 127.0.0.11:53: no such host"}
...省略
proxy-1        | {"level":"info","ts":1737170926.3481672,"logger":"http.handlers.reverse_proxy.health_checker.active","msg":"HTTP request failed","host":"keycloak:8080","error":"Get \"http://keycloak:8080/health/ready\": dial tcp 192.168.107.4:8080: connect: connection refused"}
proxy-1        | 
...省略{"level":"info","ts":1737170935.4018724,"logger":"http.handlers.reverse_proxy.health_checker.active","msg":"status code out of tolerances","status_code":404,"host":"keycloak:8080"}
...省略

Keycloakのバージョン26.0.0からヘルスチェックのエンドポイントはデフォルトで9000ポートに変わったらしい。

https://www.keycloak.org/docs/latest/upgrading/#management-port-for-metrics-and-health-endpoints

Caddyfileにhealth_port 9000を追加する。

localhost:80 {
  reverse_proxy keycloak:8080 {
    health_port 9000
    # 省略
  }
}

これでCaddyからヘルスチェック成功のログが出るようになった。

proxy-1 | {"level":"info","ts":1737174779.735086,"logger":"http.handlers.reverse_proxy.health_checker.active","msg":"host is up","host":"keycloak:9000"}

localhost:18080にアクセスしたら、Caddy越しにKeycloakのTOPが表示できた。

アクセスログの出力

この設定だけだと、アクセスログがコンソールに出なかったので出したい。

公式ドキュメントにlogというページがあるので、参照しながらCaddyfileに以下の設定を追加する。

caddyserver.com

localhost:80 {
  reverse_proxy keycloak:8080 {
    # ...省略
  }
  log { # 追加
        output stdout
    }
}

これでアクセスログは出るようになったが、ほしいのはKeycloak自体へのアクセスログのみなのに、JSやCSSのログも出力されてノイズになってしまった。

log_skipというディレクティブがあるので、これで対応することができた。

caddyserver.com

Keycloakの場合は、staticコンテンツは/resoources/配下に配置されているようなので、これをパスごと除外する。

localhost:80 {
  reverse_proxy keycloak:8080 {
   # 省略
  }
  log {
    output stdout
  }
  log_skip /resources/*
}

上記の設定でstaticコンテンツのアクセスログが出力されなくなった。ただ、Keycloakの画面自体が1画面で複数のhtmlを取得する設計でできているみたいで、1回の画面へのアクセスでログが8行くらい出る。

目的は達成できたのでこの記事はここまでとする。

所感

  • 今回は技術検証だったが、仕事の場合もアプリケーションサーバーの前段のプロキシサーバーは深く考えずにNginxを選定してしまうことが多かった。
  • → 言われてみるとNginxを使い倒している訳ではない。
  • → リバースプロキシが必要なだけなら、もっと適したプロダクトがあるのかもしれない。
  • → 今回は初手のCaddyでうまくいってしまったので触る機会がなかったTraefikやEnvoyも触ってみたいなと思った



以上の内容はhttps://kamatimaru.hatenablog.com/entry/2025/01/18/140723より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14