🛟

CircleCI上でCypress起動時に `Missing X server or $DISPLAY` で落ちる原因と回避策

に公開

CircleCIで、CypressによるE2Eテストを実行しようとした時に、5~10%の頻度でMissing X server or $DISPLAY というエラーでflakyに落ちる問題に悩んでいました。

同様のエラーに悩んでいる方がこの記事を読んでいると仮定して、結論からお伝えします。
もし、Dockerイメージとして、cimg/node:*-browsers を利用し、Cypress CircleCI Orb ではない形で直接実行しているなら、以下の回避策を適用できます。

https://github.com/CircleCI-Public/cimg-node/issues/502#issuecomment-4084305637

▼ 上記Issueのサンプルコードより抜粋

jobs:
  test:
    docker:
        - image: cimg/node:24.14.0-browsers
    environment:
        DISPLAY: '' # ここがポイント!
    steps:
      - checkout
      - node/install-packages
      - browser-tools/install_chrome
      - run:
          command: npx cypress run --browser 

この回避策を適用してから1000回以上、CircleCI上でCypressを起動していますが、今のところ一度も Missing X server or $DISPLAY が出ていません。

re-runによるコストをざっくり計算すると、月に15,000円以上を節約することができました。コスト削減だけでなく、E2Eがflakyに落ちることがなくなり、開発者体験・開発速度の向上に寄与しました。

回避策を提供してくださった MikeMcC399 さんに心から感謝しています。

ここまででこの記事の目的の大半は達成できました。ここからは、なぜこのエラーが起きて、なぜこの回避策が有効なのか、という点を解説します。

Missing X server or $DISPLAY となる原因

まず、CypressをLinux上(CircleCI上での実行ではDockerコンテナ)で動かす場合、X11サーバーを必要とします。

以下の通り、実行時にX11サーバーがない場合、Cypressは自前のX11サーバーを起動します。

When running on Linux, Cypress needs an X11 server; otherwise it spawns its own X11 server during the test run.
ref: https://docs.cypress.io/app/continuous-integration/overview#Xvfb

cimg/node:*-browsers ではX11サーバーを用意してくれているため、Cypressは自前のサーバーを起動せず、用意されているものを参照します。

また、CypressインスタンスがX11サーバーの起動ポートを参照するための、DISPLAY 環境変数も設定されています。

https://github.com/CircleCI-Public/cimg-node/blob/7849ae27e6553bb10fe512a0e67abeb21f76c2cd/variants/browsers.Dockerfile.template#L63

https://github.com/CircleCI-Public/cimg-node/blob/7849ae27e6553bb10fe512a0e67abeb21f76c2cd/variants/browsers.Dockerfile.template#L71

この時、cimg/node:*-browsers で用意したX11サーバーの起動が完了していない状態でCypressを実行すると、DISPLAY に指定した :99 ポートをみにいってもX11サーバーが存在せず、Missing X server or $DISPLAY になっていたと考えられます。

先ほどのDockerfileから分かる通り、起動完了まで待つためのwait処理が入っていたりと対策が施されていますが、5~10%の確率で起動未完了のままとなるようです。

なぜ DISPLAY: '' の回避策が有効か

前項で述べた以下が回答のほとんどです。

実行時にX11サーバーがない場合、Cypressは自前のX11サーバーを起動します。

DISPLAY: '' とすることで、Cypressは cimg/node:*-browsers で起動されたX11サーバーを見にいかず、自前のX11サーバーを起動します。

CypressのX11サーバーの起動プロセスはどうやら安定しているようで、前述の通り、1000回以上実行して一度も接続に失敗していません。

MikeMcC399 さんのこちらのレス によると、Cypress側では起動時に30秒のタイムアウトを設定しており、以来、起動完了が安定したとのことでした。

https://github.com/cypress-io/cypress/blob/1b8c5ddd9757def9bbdf25c65ca46c2267568fa0/cli/lib/exec/xvfb.ts#L16

ただ、複数のCypressインスタンスで並列実行する場合は自前のX11サーバー起動でも接続エラーになることがあるらしく、以下にその回避策が記載されています。

https://docs.cypress.io/app/continuous-integration/overview#Xvfb

cimg/node:*-browsers を使用し、かつそのjob内でCypressを並列実行する場合は、Xvfb を :99 以外のポートで起動し、DISPLAY にそのポートを割り当てることで同様に回避できると考えられます。

根本解決策について

ここまで、CircleCI上で cimg/node:*-browsers を使い、cypress run で直接実行する場合に生じうる、Missing X server or $DISPLAY エラーの回避策をご紹介しました。

Cypress CircleCI Orb を使った実行では、この回避策は適用できないようなのでご注意ください。

upstream側(cimg/node:*-browsers)の根本解決策を自分なりに調査してみたところ、 -displayfd オプションを使ってXvfb が ready になるまで待つ修正を入れることで、cimg-node 側で確実にX11サーバーを立ち上げることができるかもしれないと考えています。

▼投稿したレス
https://github.com/CircleCI-Public/cimg-node/issues/502#issuecomment-4346034591

Cypress CircleCI Orb の default executor は以下のように cimg/node:*-browsers をDockerイメージとして参照しているため、上記の解決策が有効であれば、Cypress CircleCI Orb による実行でも安定してX11サーバーに接続できるようになるのではと期待しています。

https://github.com/cypress-io/circleci-orb/blob/173d253f8be1a58bcfb44a211cc8d752c3af9028/src/executors/default.yml#L9-L10

もしメンテナの方が他の対応で忙しそうな状況なら、自分の方でPRを上げてみようと思っています。

以上、同じような問題で悩んでいた方の参考になれば幸いです。

Discussion