Docker+xvfbで仮想ディスプレイ環境を構築!コンテナでGUIテストを実行する

CI/CDパイプラインでSeleniumテストを実行しようとして、「Error: no display specified」というエラーに遭遇したことありませんか?ローカルでは完璧に動くブラウザテストが、Dockerコンテナ内では画面がないために実行できない…

この問題を解決するのがxvfb(仮想ディスプレイ)です。物理的な画面がないサーバー環境でも、仮想的なディスプレイを作成してGUIアプリケーションを実行できるようになります。

目次

コンテナでGUIアプリを動かしたい!でも画面がない…

多くの開発チームが直面するこの問題。特に以下のような状況でよく発生します:

よくある困った場面

  • CI/CDでのブラウザテスト実行
    ・GitHub ActionsやJenkinsでSeleniumテストが失敗
    ・「DISPLAY environment variable not set」エラー
  • Dockerコンテナでの開発
    ・GUIアプリケーションのテスト環境構築
    ・スクリーンショット撮影やPDF生成処理
  • クラウド環境での自動化
    ・AWSやGCPでのヘッドレス処理
    ・Webスクレイピングやデータ収集

根本的な原因は、Linuxサーバー環境には物理的なディスプレイが接続されていないことです。GUIアプリケーションは表示先となる画面を要求しますが、それが存在しないためエラーになってしまいます。

xvfbが解決する課題

xvfbを使用することで以下が可能になります:

  • ヘッドレス環境でのGUIアプリ実行
  • CI/CDパイプラインでのブラウザテスト
  • スクリーンショット撮影の自動化
  • コンテナ内でのElectronアプリテスト

xvfb(仮想ディスプレイ)とは何か?

xvfb(X Virtual Framebuffer)は、物理的なディスプレイハードウェアなしでX Window Systemを実行できる仮想ディスプレイサーバーです。

X Virtual Framebufferの仕組み

通常のディスプレイ構成:

アプリケーション → X Server → 物理ディスプレイ → ユーザーの目

xvfbを使用した構成:

アプリケーション → xvfb → メモリ内仮想画面 → (必要に応じてファイル出力)

物理ディスプレイとの違い

物理ディスプレイ:

  • 実際のモニターハードウェアが必要
  • ユーザーが視覚的に確認可能
  • マウス・キーボード操作が直接反映

xvfb仮想ディスプレイ:

  • メモリ内でのみ動作
  • 画面出力はファイルまたはプログラム経由で取得
  • 自動化・バッチ処理に最適
  • 複数の仮想画面を同時実行可能

Docker+xvfbの基本セットアップ

実際にDockerコンテナでxvfb環境を構築してみましょう。まずは最小構成から始めます。

基本的なDockerfile

# Ubuntu ベースイメージ
FROM ubuntu:22.04

# タイムゾーン設定(対話型プロンプトを回避)
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Tokyo

# 必要なパッケージのインストール
RUN apt-get update && apt-get install -y \
    xvfb \
    x11-utils \
    x11-xserver-utils \
    dbus-x11 \
    fonts-liberation \
    fonts-dejavu-core \
    && rm -rf /var/lib/apt/lists/*

# ディスプレイ環境変数の設定
ENV DISPLAY=:99
ENV XVFB_RES="1920x1080x24"

# xvfb開始スクリプトの作成
RUN echo '#!/bin/bash\n\
Xvfb :99 -screen 0 $XVFB_RES -ac -nolisten tcp -dpi 96 &\n\
export DISPLAY=:99\n\
exec "$@"' > /usr/local/bin/start-xvfb.sh \
    && chmod +x /usr/local/bin/start-xvfb.sh

# 作業ディレクトリ設定
WORKDIR /app

# エントリーポイント設定
ENTRYPOINT ["/usr/local/bin/start-xvfb.sh"]
CMD ["bash"]

動作確認用テストファイル

# test-display.sh - xvfb動作確認スクリプト
#!/bin/bash

echo "=== xvfb動作確認テスト ==="

# 1. DISPLAY環境変数の確認
echo "DISPLAY: $DISPLAY"

# 2. Xvfbプロセスの確認
echo "Xvfbプロセス:"
ps aux | grep [X]vfb

# 3. ディスプレイ情報の取得
echo "ディスプレイ情報:"
xdpyinfo | head -10

# 4. 画面解像度の確認
echo "画面解像度:"
xrandr

echo "=== テスト完了 ==="

ビルドと実行

# Dockerイメージのビルド
docker build -t xvfb-base .

# 動作確認の実行
docker run --rm xvfb-base bash /app/test-display.sh

# インタラクティブモードで起動
docker run --rm -it xvfb-base bash

実践:Seleniumブラウザテスト環境の構築

実用的な例として、SeleniumでChromeブラウザを使用したテスト環境を構築します。

Selenium対応Dockerfile

FROM ubuntu:22.04

ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Tokyo

# 基本パッケージ + Chrome依存パッケージのインストール
RUN apt-get update && apt-get install -y \
    xvfb \
    x11-utils \
    x11-xserver-utils \
    dbus-x11 \
    fonts-liberation \
    fonts-dejavu-core \
    wget \
    curl \
    unzip \
    python3 \
    python3-pip \
    python3-venv \
    ca-certificates \
    gnupg \
    lsb-release \
    && rm -rf /var/lib/apt/lists/*

# Google Chrome の追加
RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" \
       > /etc/apt/sources.list.d/google-chrome.list \
    && apt-get update \
    && apt-get install -y google-chrome-stable \
    && rm -rf /var/lib/apt/lists/*

# ChromeDriverのインストール
RUN CHROME_VERSION=$(google-chrome --version | cut -d ' ' -f3 | cut -d '.' -f1) \
    && CHROMEDRIVER_VERSION=$(curl -s "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_${CHROME_VERSION}") \
    && wget -O /tmp/chromedriver.zip \
       "https://chromedriver.storage.googleapis.com/${CHROMEDRIVER_VERSION}/chromedriver_linux64.zip" \
    && unzip /tmp/chromedriver.zip -d /tmp/ \
    && mv /tmp/chromedriver /usr/local/bin/chromedriver \
    && chmod +x /usr/local/bin/chromedriver \
    && rm /tmp/chromedriver.zip

# Python環境のセットアップ
RUN python3 -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

# Selenium関連パッケージのインストール
RUN pip install --no-cache-dir \
    selenium==4.15.0 \
    webdriver-manager==4.0.1 \
    pytest==7.4.3 \
    pytest-html==4.1.1

# ディスプレイ設定
ENV DISPLAY=:99
ENV XVFB_RES="1920x1080x24"

# xvfb + Chrome用スタートアップスクリプト
RUN echo '#!/bin/bash\n\
# xvfbの開始\n\
Xvfb :99 -screen 0 $XVFB_RES -ac -nolisten tcp -dpi 96 &\n\
\n\
# xvfbの起動を待機\n\
sleep 2\n\
\n\
# ディスプレイ環境変数のエクスポート\n\
export DISPLAY=:99\n\
\n\
echo "xvfb started on $DISPLAY with resolution $XVFB_RES"\n\
\n\
# 渡されたコマンドを実行\n\
exec "$@"' > /usr/local/bin/start-test-env.sh \
    && chmod +x /usr/local/bin/start-test-env.sh

WORKDIR /app

ENTRYPOINT ["/usr/local/bin/start-test-env.sh"]
CMD ["bash"]

Seleniumテストスクリプトの例

# test_browser.py - Seleniumテストの実装例
import os
import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class BrowserTest:
    def __init__(self):
        self.driver = None
        self.setup_driver()
    
    def setup_driver(self):
        """Chromeドライバーのセットアップ"""
        chrome_options = Options()
        
        # ヘッドレスモード(xvfb使用時は必須ではないが推奨)
        chrome_options.add_argument('--headless')
        
        # コンテナ環境での推奨オプション
        chrome_options.add_argument('--no-sandbox')
        chrome_options.add_argument('--disable-dev-shm-usage')
        chrome_options.add_argument('--disable-gpu')
        chrome_options.add_argument('--window-size=1920,1080')
        chrome_options.add_argument('--remote-debugging-port=9222')
        
        # ディスプレイ設定の確認
        display = os.environ.get('DISPLAY', ':99')
        print(f"Using display: {display}")
        
        # ChromeDriverサービスの設定
        service = Service('/usr/local/bin/chromedriver')
        
        try:
            self.driver = webdriver.Chrome(service=service, options=chrome_options)
            print("Chrome driver initialized successfully")
        except Exception as e:
            print(f"Failed to initialize Chrome driver: {e}")
            raise
    
    def test_google_search(self):
        """Google検索のテスト"""
        try:
            print("Navigating to Google...")
            self.driver.get("https://www.google.com")
            
            # ページタイトルの確認
            assert "Google" in self.driver.title
            print(f"Page title: {self.driver.title}")
            
            # 検索ボックスの取得と入力
            search_box = WebDriverWait(self.driver, 10).until(
                EC.presence_of_element_located((By.NAME, "q"))
            )
            
            search_query = "Docker xvfb selenium"
            search_box.send_keys(search_query)
            search_box.submit()
            
            # 検索結果の待機
            WebDriverWait(self.driver, 10).until(
                EC.presence_of_element_located((By.ID, "search"))
            )
            
            print(f"Search for '{search_query}' completed successfully")
            
            # スクリーンショットの撮影
            screenshot_path = "/app/screenshots/google_search.png"
            os.makedirs(os.path.dirname(screenshot_path), exist_ok=True)
            self.driver.save_screenshot(screenshot_path)
            print(f"Screenshot saved: {screenshot_path}")
            
            return True
            
        except Exception as e:
            print(f"Test failed: {e}")
            return False
    
    def test_page_load_performance(self):
        """ページ読み込み性能のテスト"""
        try:
            start_time = time.time()
            self.driver.get("https://example.com")
            
            # ページの完全読み込み待機
            WebDriverWait(self.driver, 10).until(
                lambda driver: driver.execute_script("return document.readyState") == "complete"
            )
            
            load_time = time.time() - start_time
            print(f"Page load time: {load_time:.2f} seconds")
            
            # ページ要素の確認
            heading = self.driver.find_element(By.TAG_NAME, "h1")
            print(f"Page heading: {heading.text}")
            
            return load_time < 5.0  # 5秒以内なら合格
            
        except Exception as e:
            print(f"Performance test failed: {e}")
            return False
    
    def run_all_tests(self):
        """全テストの実行"""
        print("=== Browser Test Suite ===")
        
        results = {
            "google_search": self.test_google_search(),
            "page_performance": self.test_page_load_performance()
        }
        
        print("\n=== Test Results ===")
        for test_name, result in results.items():
            status = "PASS" if result else "FAIL"
            print(f"{test_name}: {status}")
        
        return all(results.values())
    
    def cleanup(self):
        """リソースのクリーンアップ"""
        if self.driver:
            self.driver.quit()
            print("Browser driver closed")

if __name__ == "__main__":
    test = BrowserTest()
    try:
        success = test.run_all_tests()
        exit_code = 0 if success else 1
    except Exception as e:
        print(f"Test execution failed: {e}")
        exit_code = 1
    finally:
        test.cleanup()
    
    exit(exit_code)

docker-compose.ymlでの統合

# docker-compose.yml
version: '3.8'

services:
  selenium-test:
    build: .
    container_name: xvfb-selenium-test
    volumes:
      - ./tests:/app/tests
      - ./screenshots:/app/screenshots
      - ./reports:/app/reports
    environment:
      - DISPLAY=:99
      - XVFB_RES=1920x1080x24
      - PYTHONPATH=/app
    working_dir: /app
    command: python3 tests/test_browser.py
    
  # VNCサーバー付きの開発環境(オプション)
  selenium-vnc:
    build: .
    container_name: xvfb-selenium-vnc
    ports:
      - "5900:5900"  # VNC接続用
    volumes:
      - ./tests:/app/tests
      - ./screenshots:/app/screenshots
    environment:
      - DISPLAY=:99
      - XVFB_RES=1920x1080x24
    command: bash -c "
      apt-get update && apt-get install -y x11vnc &&
      x11vnc -display :99 -nopw -listen localhost -xkb -forever &
      python3 tests/test_browser.py &&
      tail -f /dev/null
    "

テスト実行とデバッグ

# プロジェクトディレクトリ構造の作成
mkdir -p selenium-test/{tests,screenshots,reports}
cd selenium-test

# 必要なファイルの配置
# Dockerfile, docker-compose.yml, test_browser.py

# 環境のビルドと実行
docker-compose build
docker-compose up selenium-test

# VNC接続でデバッグ(必要な場合)
docker-compose up -d selenium-vnc
# VNCビューアで localhost:5900 に接続

# 個別テストの実行
docker-compose run --rm selenium-test python3 tests/specific_test.py

# 環境のクリーンアップ
docker-compose down --volumes

高度な設定とトラブルシューティング

実際の運用で遭遇する問題と、その解決方法をまとめました。

画面解像度とパフォーマンスの最適化

# 高解像度設定(4K対応)
export XVFB_RES="3840x2160x24"

# 低リソース設定(軽量化)
export XVFB_RES="1280x720x16"

# 複数ディスプレイ設定
Xvfb :99 -screen 0 1920x1080x24 -screen 1 1920x1080x24 &

# カラーデプス最適化
# 24bit: フルカラー(デフォルト)
# 16bit: 軽量化
# 8bit: 最軽量(モノクロテストなど)

よくあるエラーと解決方法

エラー1: "Cannot open display :99"

# 原因: xvfbが正常に起動していない
# 解決方法:

# 1. プロセス確認
ps aux | grep Xvfb

# 2. 手動でxvfb再起動
pkill Xvfb
Xvfb :99 -screen 0 1920x1080x24 -ac -nolisten tcp &

# 3. ディスプレイ番号の変更
export DISPLAY=:100
Xvfb :100 -screen 0 1920x1080x24 -ac -nolisten tcp &

エラー2: Chrome crashed with status 1

# 原因: Chromeオプションの不備
# 解決方法: 追加オプションの設定

chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')
chrome_options.add_argument('--disable-gpu')
chrome_options.add_argument('--disable-extensions')
chrome_options.add_argument('--disable-plugins')
chrome_options.add_argument('--disable-images')  # 画像読み込み無効化
chrome_options.add_argument('--disable-javascript')  # JS無効化(テストに応じて)

# メモリ制限への対応
chrome_options.add_argument('--memory-pressure-off')
chrome_options.add_argument('--max_old_space_size=4096')

エラー3: スクリーンショットが真っ黒

# 原因: ウィンドウマネージャーの不在またはフォント問題
# 解決方法:

# 1. 軽量ウィンドウマネージャーのインストール
apt-get install -y openbox

# 2. フォントの追加インストール
apt-get install -y fonts-liberation fonts-dejavu-core

# 3. 起動スクリプトの修正
openbox &
sleep 1
export DISPLAY=:99

リソース使用量の監視

# メモリ使用量の監視
#!/bin/bash
# monitor-resources.sh

echo "=== Resource Usage Monitor ==="
while true; do
    echo "$(date): Memory: $(free -h | grep Mem | awk '{print $3"/"$2}') | CPU: $(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d% -f1)%"
    sleep 10
done

本番運用での考慮点

実際のプロダクション環境での運用時に考慮すべきポイントをまとめました。

CI/CDパイプラインでの活用

# .github/workflows/selenium-test.yml
name: Selenium Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  browser-tests:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Build test environment
      run: docker build -t selenium-test .
    
    - name: Run Selenium tests
      run: |
        docker run --rm \
          -v ${{ github.workspace }}/test-results:/app/screenshots \
          -e DISPLAY=:99 \
          selenium-test python3 tests/test_suite.py
    
    - name: Upload test results
      uses: actions/upload-artifact@v3
      if: always()
      with:
        name: test-results
        path: test-results/
        retention-days: 30
    
    - name: Publish test report
      uses: dorny/test-reporter@v1
      if: success() || failure()
      with:
        name: Selenium Test Results
        path: test-results/pytest-results.xml
        reporter: java-junit

セキュリティ考慮事項

  • ネットワーク分離
    ・テスト環境のネットワークを本番から分離
    ・不要な外部通信の制限
  • 機密情報の管理
    ・テストデータに機密情報を含めない
    ・環境変数での認証情報管理
  • リソース制限
    ・Dockerコンテナのメモリ・CPU制限
    ・実行時間の上限設定
# docker-compose.ymlでのリソース制限
services:
  selenium-test:
    build: .
    deploy:
      resources:
        limits:
          memory: 2G
          cpus: '1.0'
        reservations:
          memory: 1G
          cpus: '0.5'
    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs:
      - /tmp
      - /var/tmp

スケーリングと並列実行

# 並列テスト実行の設定
version: '3.8'

services:
  selenium-hub:
    image: selenium/hub:4.15.0
    ports:
      - "4444:4444"

  selenium-chrome-1:
    build: .
    depends_on:
      - selenium-hub
    environment:
      - DISPLAY=:99
      - HUB_HOST=selenium-hub
    volumes:
      - /dev/shm:/dev/shm
    
  selenium-chrome-2:
    build: .
    depends_on:
      - selenium-hub
    environment:
      - DISPLAY=:98  # 異なるディスプレイ番号
      - HUB_HOST=selenium-hub
    volumes:
      - /dev/shm:/dev/shm

仮想ディスプレイ技術をさらに活用する関連記事

xvfbでの仮想ディスプレイ環境構築を習得したら、コンテナ技術やAI開発支援ツールも活用してより効率的な開発環境を構築しましょう:

🤖 AI開発支援・コード管理

📊 開発効率化・可視化ツール

まとめ:コンテナでのGUIテスト環境完成

xvfbを使用することで、物理ディスプレイがないコンテナ環境でも完全なGUIテスト環境を構築できます。CI/CDパイプラインでのブラウザテスト自動化から、大規模なWebアプリケーションのテストまで、幅広い用途で活用できます。

今日から始められるアクション:

  • 既存のSeleniumテストをDockerコンテナ化
  • CI/CDパイプラインにxvfb環境を統合
  • スクリーンショットテストの自動化導入
  • 並列テスト実行環境の構築

次回のプロジェクトでは、ぜひxvfbを活用して「いつでも、どこでも実行できる」堅牢なテスト環境を構築してみてください!

xvfb仮想ディスプレイ よくある質問

❓ xvfbはメモリをどれくらい消費しますか?

解像度に依存しますが、一般的な1920x1080x24bitの設定で約60-100MBのメモリを使用します。複数の仮想ディスプレイを同時実行する場合は、その分メモリ使用量が増加するため、適切なリソース制限を設定することが重要です。

❓ ヘッドレスブラウザとxvfbの違いは何ですか?

ヘッドレスブラウザは画面描画を行わないため高速ですが、一部のCSSアニメーションやビジュアル要素のテストが困難です。xvfbは完全な画面描画を行うため、実際のブラウザ表示と同等の環境でテストでき、スクリーンショット撮影も可能です。ビジュアルテストが重要な場合はxvfbが適しています。

❓ 本番環境でxvfbを長時間稼働させても大丈夫ですか?

はい、xvfbは本番環境での長時間稼働に対応できます。ただし、メモリリークを防ぐために定期的な再起動、適切なログローテーション、リソース監視の設定を行うことを推奨します。また、使用しない仮想ディスプレイは適切に終了させましょう。

❓ Windowsコンテナでもxvfbは使用できますか?

xvfbはX Window System用のツールのため、Windowsコンテナでは直接使用できません。Windows環境では、Windows Subsystem for Linux(WSL)を使用するか、Linuxコンテナを使用してxvfb環境を構築する必要があります。Windowsネイティブの場合は、別のヘッドレス実行手法を検討してください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次