🐳

Dockerで開発環境を構築する。 Rails6.1/postgreSQL14

2022/11/27に公開約21,300字

ローカルでの開発環境をdocker上で再現したい!

コンテナ基盤での本番環境構築やCIなどの導入も見据えて、dockerを学習し始めました。第一歩として、お手元のPCで開発していた環境をコンテナでほぼ再現させた記録です。

なぜ書くのか?

自身で開発環境をdocker上で構築しようと思った際に、Docker公式のquick startではあまりに簡素であったり、DBの違いによりインストールの仕方が曖昧であったりと、自分が求めていた情報にピッタリ合うものがなかったためです。

今回構築する開発環境

  • (PC)mac Monterey 12.6
  • ruby 3.0.4
  • rails 6.1.7
  • postgreSQL 14
  • chromeブラウザ

想定する読者層

  • 自分のローカルPC上にrubyやrailsをインストールして簡単にでも開発を行ったことがある人
  • docker基本的な概念は理解したつもりだけど、実際に自分の開発環境はどうやって構築するのかわからない人(過去の私)

概要

基本的なdockerの仕組みや、コマンドについては適宜補足しますが、あくまで構成と設定に主眼を置き、その過程でのつまづいたところを共有できればと思ってます。
つまり、
Dockerfileとdocker-compose.ymlをどう書くのか?
ということについて書ければと思っています。
今回は、既存のアプリの開発環境を移行する形でやっていきます。
また、技術記事投稿に関しても慣れていないため、NGな部分があれば、ご指摘ください。

前提

開発環境って何ができないといけないのか?

ローカルPCで開発する際に、あたり前すぎて意識していなかったことをあらためて洗い出してみました。

  • ソースコードの管理。
    コンテナ上でサーバを立ち上げるけど、エディタでいじるコードはコンテナ・ローカル間で同期していてほしい。
  • DBデータの保持。開発環境でのDBのデータはdocker-compose downしても残っててほしい。
  • アプリ側からDBに接続できること。
    アプリのコンテナに潜ってpsqlコマンドで接続したり、Railsの設定でDBと通信ができるようにする。
  • デバッグ
    ソースコード中にbyebugメソッドを差し込んだ時に、ログを止めてプロンプト立ち上がってほしい。
  • テストが実行できること
    アプリコンテナの中に入って、bin/rails spec(もしくはbundle exec rspec)でテストができること

以上のことがdocker上で開発する上でやりたいことになるかと思います。

用意するコンテナ(service)と設定について

それを踏まえて必要になるコンテナ達、その他諸々を挙げてみます。

  1. アプリケーションサーバーとして機能するコンテナ
    一言でいうと、rails serverコマンドでローカルサーバー起動するためのコンテナです。 詳細は後述します。
  2. rails固有の問題を解決するためのシェルスクリプト
  3. コンテナ同士をオーケストレートするためのdocker-compose.yml
    ポートの設定やvolumeの同期などをして、コンテナ間やコンテナとローカル(お手元PC)間がうまくやりとりできるようにします。
    1. db用のコンテナ
      データベースとしての役割だけを持たせたコンテナです。先ほどのアプリサーバとなるコンテナと通信できるように設定します。
    2. (永続化させたいデータを保存するvolume コンテナの作成)
      dockerでは、DBのデータなど永続化させたいデータを扱うために、volumeという仕組みがありますが、今回は中でもvolumeコンテナ(データを保存しておくためだけのコンテナ)を使っていきます。
    3. capybaraを使ってユーザのブラウザ動作を再現するためのドライバーとブラウザ
      コンテナ環境にはデフォルトでブラウザが入っている訳ではありません。webブラウザが導入されていないと、systemspec等のE2Eテストでブラウザの動作を再現するテストが実行できません。そのため、ブラウザも入れます。(もし、テストなんて必要ないという方は飛ばしてください。)
  4. 現状docker-composeで解消できるのはよく分かってない要素
    作成したコンテナに、自ら潜っていくつか手動でインストールする。
    e.g.) rails db:create ….、bin/rails webpacker install

それでは、早速ファイルを作成しながら詳しく見ていきます。


ディレクトリ構成

今回は、以下のようなディレクトリ構成にしてみました。

.(アプリのルート)
├── app
├── bin
├── config
├── ..(その他railsアプリに関するディレクトリ群)
├── ..
├── Gemfile
├── Gemfile.lock
├── docker
|   ├── Dockerfile
|   └── entrypoint.sh (# 前述6.のスクリプト)
└── docker-compose.yml

"全体"としての環境を束ねるdocker-compose.ymlはappのルートディレクトリにおいて、dockerディレクトリには、nginxやpumaなども導入するようなより本格的な構築をすることになった時に、docker/nginx/Dockefileなどと、なんのためのDockefileなのかを区別しやすくするためです。

僕が調べている時には、以下のような構成も見かけました。
webディレクトリ階下にアプリのソースコードを配置すると、github等にpushする際にも、機密情報が含まれたdocker関連ファイルを.gitignoreをいじらずとも管理できるのかな?とも思いました。

app
└─ docker
    ├── nginx
    │   ├── Dockerfile
    │   └── nginx.conf
    └── web
        ├── Dockerfile
        ├── entrypoint.sh
        ├── start-server.sh
	└──(例えばここ)

参考にさせていただいた記事)

https://itoka.hatenadiary.com/entry/2022/03/04/004453

0. ディレクトリとソースコードの用意。

自分のソースコードをcloneするなり、ローカルにあるものを移すなりして、作業するディレクトリを準備してください。

# e.g.)
$ git clone ....

1. アプリケーションサーバの作成

Dockerfileを記述して、rails serverしたらアプリにアクセスできるような環境を作っていきます。
ベースイメージにはdocker公式のrubyイメージを選択して、rubyやrailsだけでなくlinuxのパッケージも色々必要になるので、そちらも含めて記述します。
以下必要になるパッケージの一覧です。

  • 一般的なLinuxベースのパッケージ達。(vim、curl, git etc…)
  • データベースのクライアントパッケージ(mysql-client等にあたるもの)
  • railsはGemfileに記述する形でinstall
  • Rails 6.0系でデフォルトになったwebpackerを扱うために、nodejsとyarnもインストール

Dockerfile

docker/Dockerfile
FROM ruby:3.0.4

ARG PG_MAJOR=14
ARG NODE_MAJOR=14
ARG YARN_VERSION=1.22.19
ARG BUNDLED_WITH=2.2.33

# 一般的な依存関係をインストール
RUN apt-get update -qq\
  && apt-get install -yq --no-install-recommends\
    build-essential\
    gnupg2\
    curl\
    less\
    git\
    vim\
  && apt-get clean\
  && rm -rf /var/cache/apt/archives/*\
  && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*\
  && truncate -s 0 /var/log/*log

# posgresqlの依存関係をインストール
RUN curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc |\
    gpg --dearmor -o /usr/share/keyrings/postgres-archive-keyring.gpg\
    && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/postgres-archive-keyring.gpg] https://apt.postgresql.org/pub/repos/apt/"\
    bullseye-pgdg main $PG_MAJOR | tee /etc/apt/sources.list.d/postgres.list > /dev/null
RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade &&\
  DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends\
    libpq-dev\
    postgresql-client-$PG_MAJOR\
    && apt-get clean\
    && rm -rf /var/cache/apt/archives/*\
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*\
    && truncate -s 0 /var/log/*log


# NodeJSとYarnもインストール
RUN curl -sL https://deb.nodesource.com/setup_$NODE_MAJOR.x | bash -
RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade &&\
  DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends\
    nodejs\
    && apt-get clean\
    && rm -rf /var/cache/apt/archives/*\
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*\
    && truncate -s 0 /var/log/*log
RUN npm install -g yarn@$YARN_VERSION

RUN mkdir /your_app
WORKDIR /your_app

# railsなどgemの記載がされているものを想定してます。
COPY Gemfile /your_app
COPY Gemfile.lock /your_app
RUN gem install bundler -v $BUNDLED_WITH
RUN bundle install

COPY docker/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT [ "entrypoint.sh" ]
EXPOSE 3000

CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]

参考にさせていただいた記事)

https://techracho.bpsinc.jp/hachi8833/2022_04_07/116843

具体的な内容に触れていきます。

FROM ruby:3.0.4

ARG PG_MAJOR=14
ARG NODE_MAJOR=14
ARG YARN_VERSION=1.22.19
ARG BUNDLED_WITH=2.2.33

まずベースのイメージにruby:3.0.4を指定しました。
(docker hubのソースコードによると、tag:3.0.4を指定した場合には、debian系linuxのbullseyeを指定しているようです。そのためパッケージマネージャは必然的にaptになります)

次にARGを使っています。

ARGとは

Dockerfileからイメージをビルド時(docker buildコマンドを打つ時)に、使用できる変数を指定できる。ENVとは異なり、実際に起動したコンテナで扱う環境変数にはならず、飽くまでビルド時の一時的な引数です。

このように記述した理由は2つあります。まず、$NODE_MAJORの形で、Dockerfile内で以後参照できることを生かして、ファイルの冒頭で必要なパッケージのバージョンを把握できるようにすること。また、テンプレートとして今後使い回すことを考えた時に管理が容易になるためです。

Dockerfileの細かい仕様について

FROM以降では引数がリセットされてしまうので、もしFROM以前にARGを使用している場合には再宣言が必要になります

次に一般的なlinuxパッケージを依存関係も含めてインストールしています。

# 一般的な依存関係をインストール
RUN apt-get update -qq\
  && apt-get install -yq --no-install-recommends\
    build-essential\
    gnupg2\
    curl\
    less\
    git\
    vim\
  && apt-get clean\
  && rm -rf /var/cache/apt/archives/*\
  && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*\
  && truncate -s 0 /var/log/*log

apt-getを使ってインストールしていきます。
&&以降の呪文の部分ですが、
apt-get clean
で/var/cache/apt以下のファイル(パッケージのローカルリポジトリ)を削除
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
で、インストール中に作成される一時ファイルを削除
truncate -s 0 /var/log/*log
で、ログファイルもファイルサイズを0にしているようです。
これらの工夫はDockerレイヤに余計なデータを残さないようにして、コンテナ自体の容量を節約する目的があるみたいです。

次に、postgreSQLとNode+ yarnをインストールしていきますが、
aptコマンドでPostgreSQLやNodeJSをインストールするには、それらのdebパッケージをソース一覧に追加しておく必要があるので、その分の記述が追加されています。

以降は、他の記事でもよく見かけるような記述になっていますが、

RUN mkdir /your_app
WORKDIR /your_app

COPY Gemfile /your_app #自身のGemfileをdocker側で参照できるように
COPY Gemfile.lock /your_app # 同上
RUN gem install bundler -v $BUNDLED_WITH
RUN bundle install

まず、mkdirで作業ディレクトリを作成して、WORKDIRで作業ディレクトリを指定します。その後既存のGemfileとGemfile.lockをコンテナ上にコピーさせてbundle installしています。今回は、既存のアプリが存在する体なので、この手順で導入できます。

この時、アプリのルートからみるとGemfileは../Gemfileのパス指定が良い気がしますが、これだとうまく動作しません。詳細はdocker-compose.ymlの記述の際に説明します。

もし、新規アプリの場合には

local上に、Gemfileと白紙のGemfile.lockを準備して,バンドラー経由でrailsをインストールすると良いと思います。

Gemfile

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem 'rails', '~>7.0.2'

補足

  • bundlerのバージョン指定について
    bundler2.3.0以降ではGemfile.lockのBUNDLED_WITH部分の記述で指定されたバージョンのbundlerが自動でインストールされるため、手動で指定する必要がないそうです。
    https://github.com/rubygems/rubygems/pull/4076

最後に、毎回コンテナを起動するたびに走らせたいシェルスクリプトを登録して、CMDの部分で、毎回サーバーを起動するようにしています。

COPY docker/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT [ "entrypoint.sh" ]
EXPOSE 3000

CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]

entrypoint.shの中身については後述しますが、そうゆうシェルスクリプトをCOPYでコンテナに乗せて、実行権限を付与(RUN chmod...)して、コンテナが起動した際に実行されるよう指定しています。(ENTRYPOINT)

EXPOSEは、コンテナ起動時に公開するポートを記述しています。
が、実際に公開するコマンドでという意味合いではなく、ドキュメンテーションとしての役割に留まります。

最後に、rails serverをするように指定しています。CMDの記述をコマンドライン上で表現すると

$ bundle exec rails server -b 0.0.0.0

となりますが、この-b 0.0.0.0オプションの意味は、一言でいうと、0.0.0.0のIPアドレスを結びつける(bindする)。ということになります。(defaultでは、自分自身であるlocalhost(127.0.0.1)だけになってます。)

このように指定するのは、Docker上でコンテナを起動するときに、その起動中のプロセスは外から独立したひとつのマシンのように扱われることに起因しています。そのため、コンテナから見ると外部にあたるお手元のPC環境でlocalhost(127.0.0.1)を指定してもアクセスはできません。(ローカル上では、rails sされてないから)
ここから先の理解は少し、あやふやなのですが、、、
0.0.0.0というのは、LAN(Local Area Network)上の任意のプライベートIPアドレスを意味するらしいです。
つまり、コンテナ上で

$ bundle exec rails server -b 0.0.0.0

することは、Railsのサーバーをコンテナ上で起動し、かつ "LAN内の任意のIPアドレスからのアクセス許可する" ということになるかと思います。これによって、コンテナから見ると外部にあたるお手元のPCから、起動しているコンテナのホスト名とポート番号でアクセスできるようになるはずです。

rails sコマンドとbindingオプションについて参考にさせていただいた記事

https://pikawaka.com/rails/rails-s

2.rails固有の問題を解決するためのシェルスクリプトを仕込む

先ほどの例に上がったentrypoint.shという名前のシェルスクリプトについて説明してみます。

entrypoint.sh

docker/entrypoint.sh
#!/bin/bash

# setコマンドで、エラーが発生したら即時終了するように
set -e
# server.pidというファイルを削除
rm -f /your_app/tmp/pids/server.pid
# CMDの部分に渡されたコマンドを実行する(rails s -b ..)
exec "$@"

ここでおこなっているのは、rails sをする度に作成されるserverに関するプロセスIDの削除です。これを行わないとコンテナを再起動した際に、Railsが前回の起動時に生成したserver.pidを読み込んでしまうことによって起きる、A server is already runningというエラーを解消しています。

以上で、Railsに関する設定は大方終わりになります。次は、実際にコンテナ同時を協調できるようにするためのdocker-compose.ymlについて書いていきます。

3. コンテナ同士を協調させる

docker-compose.yml

ここから、docker-compose.ymlを書いていきますが、先に全体としてどのような設定になったのかを載せておきます。

version: '3'
services:
  # postgresql
  postgres_db:
    image: postgres:14
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: postgres

  # Rails
  rails:
    build:
      context: .
      dockerfile: ./docker/Dockerfile
    volumes:
      - .:/your_app
      - node_modules:/your_app/node_modules
      - bundle:/usr/local/bundle
    stdin_open: true
    tty: true
    ports:
      - 3000:3000
    depends_on:
      - postgres_db
      - chrome
  # chrome
  chrome:
    image: selenium/standalone-chrome:latest
    ports:
      - 4444:4444
    logging:
      driver: none
      
volumes:
  node_modules:
  db-data:
  bundle:

簡単にそれぞれの項目について説明すると、

  • versionで、使用するdocker-composeの書式のversionを選択
  • services階下に、rails,postgres_db,chromeの3つのサービスを配置しています。この文脈では、サービスとコンテナはほぼ同義です。
  • 3つのコンテナの階下には、使用するイメージやportsの設定など、それぞれのコンテナについての設定情報が書かれています。
  • 最後のvolumesの部分は、volumeコンテナ(データを永続化するためだけのコンテナ)の定義を書いています。postgres_dbやrailsコンテナの説明の際に適宜説明を加えます。

3-1. DB用のコンテナ

今回は、postgreSQL14にしたかったので、以下のようになりました。

services:
  # postgresql
  postgres_db: # サービスに名前を付ける
    image: postgres:14 # ベースにするイメージを指定
    volumes: 
      - db-data:/var/lib/postgresql/data
    environment: # DB用のpostgres_dbのコンテナがもつ環境変数の設定
      POSTGRES_PASSWORD: postgres
  • volumesの部分
    <data volumeコンテナ名>:<コンテナ上のパス>の形式で、volumeコンテナをコンテナ上のファイルにマウントしています。(ここではdb-dataと名づけました)
    volumeを使う方法として、<volumeコンテナ名>の代わりに、
    <ホスト側のパス>:<コンテナ上のパス>で、ホストマシンのディレクトリを指定してマウントすることもできます。このとき、ホスト側とコンテナ上のファイルは同期させることができますが、ローカルから直接参照することがないと思ったものに関しては今回、名前のわかりやすさからもdata volume コンテナを使いました。
volumesコンテナの実際の場所

<ローカル上のパス>と<コンテナ上のパス>で同期させていない今回のような場合に、どこにvolumeコンテナがあるのか調べて見ました。
docker volume inspectコマンドを使います。
(docker volume lsコマンドで,volumeの名前を確認できます。)

bashでvolumeの場所を検査
$ docker volume inspect your_app_db-data
[
    {
        "CreatedAt": "2022-11-22T11:04:38Z",
        "Driver": "local",
        "Labels": {
            "com.docker.compose.project": "your_app",
            "com.docker.compose.version": "2.12.2",
            "com.docker.compose.volume": "db-data"
        },
        "Mountpoint": "/var/lib/docker/volumes/your_app_db-data/_data",
        "Name": "your_app_db-data",
        "Options": null,
        "Scope": "local"
    }
]

どうやら、ローカルの/var/lib/docker/volumes/下に存在しているようです。

  • 最後の、environmentの部分
    postgreSQLについての設定を行なっています。このコンテナは環境変数に値を設定することで、DBの設定を行えるようになっています。ちなみに、postgresの場合には、POSTGRES_PASSWORDだけは必須項目です。こちらに設定する項目と環境変数の対応が書かれてます。

database.yml

次に、RailsアプリのDBの設定ファイルを編集します。先ほど設定してきたDBコンテナのパスワードの値(POSTGRES_PASSWORD)などを反映させていく作業です。
以下、developmentとtest環境の設定部分です。(開発環境でも個別にテストも走らせたいので)

config/databse.yml
default: &default
  adapter: postgresql
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000
  encoding: unicode
  username: postgres
  password: postgres
  host: postgres_db

development:
  <<: *default
  database: your_app_development
test:
  <<: *default
  database: your_app_test

defaultで共通の設定をしています。

  • usernameは、特に設定しなかったので、postgreSQLデフォルトのpostgresユーザ。
  • password DBコンテナで指定した環境変数の値を入れます。
  • hostdocker-composeで作成したサービス(コンテナ)達は、別のコンテナに対してサービス名での名前解決できるので、postgres_dbと書きます。

1.リターンズ Rails用のコンテナを他のコンテナと協調させる。

先ほど、Dockerfileに記述したイメージをもとに、アプリケーション用のコンテナ(service)を設定していきます。
該当の部分だけを前述のdocker-compose.ymlから抜き出すと以下の様になります。

services:
  rails: #サービスの名前をつけます。railsのアプリ用なのでrailsにしました。
    build:
      context: .
      dockerfile: ./docker/Dockerfile
    volumes:
      - .:/your_app
      - node_modules:/your_app/node_modules
      - bundle:/usr/local/bundle
    stdin_open: true
    tty: true
    ports: 
      - 3000:3000
    depends_on: #ここでサービス名を指定して依存関係を作る。
      - postgres_db
      - chrome
  • build:について
    使用するイメージとなるDockerfileの指定と、contextの指定します。今回のようにdockerディレクトリをつくって、その中にDockerfileを置く場合には、このcontextにも気を遣う必要あります。

[補足]Dockefileとdocker-compose.ymlとそのビルドコンテキストについて

つまるところ、問題は、Dockerfileはディレクトリで分けて管理しながらも、そのディレクトリの外のファイル(ここではGemfile)を参照する必要があって、実際にコマンドライン上でbuildすることはなく、docker-compose.ymlを通してbuildを行う必要がある点にあります。docker-compose.ymlをアプリのルートに配置した上で、そのcontextをアプリルート(実際には(.)current_directory)と指定することで、今ビルドするDockerfileを指定しつつ、アプリルートからの読み込みによって他のファイルを参照できるようにして解決します。

参考にさせていただいた記事一覧)

https://qiita.com/sam8helloworld/items/e7fffa9afc82aea68a7a
https://qiita.com/toshihirock/items/c85f3eb5f4752b15ca3d
https://zukucode.com/2020/08/docker-compose-parent-directory.html

  • volumesについて
    1行目では、ローカル上のアプリケーションソースコードを、コンテナ上にマウントして同期させる。
    2,3行目はjsライブラリとgemライブラリを保存する専用をvolumesコンテナを作ることで、gemのデータを永続化しつつ、Railsアプリコンテナ自体はスリムに保てるように工夫している

  • stdin_openとtty
    この設定は、シェルでのやり取りを可能にするものです。標準入力を受け付けて、接続している擬似端末を有効にします(よくわかってない)。

具体的には
ローカルのターミナル
# 起動中のコンテナに対して
$ docker-compose exec rails(設定したサービス名) bash

とすると次のように、コンテナ上のrails server環境に入って、ターミナルでコマンドを使うことができるようになります。

アプリコンテナ上
# e.g.)
root@920904eb8631:/your_app# 
  • ports
    ポートフォワーディングの設定です。
    <ホスト側のポート>: <コンテナ側のポート>の形を取ります。こうすることで、今まで通りlocalhost:3000でアクセスした際に、railsコンテナ上のコンテナポート3000にdockerがフォワーディングしてくれることで、コンテナ上で起動するサーバに対して、お手元のPCからアクセスできるようになります。

  • depends_onについて
    他のコンテナとの依存関係を設定します。
    参考にさせていただいた記事)

https://gotohayato.com/content/533/

実際にビルドしてみる。

webpackerをインストールしたり、データベースをdatabase.ymlを元に作るためにrailsコンテナの動作確認も含めて、まずはビルドしてみます

ローカルのターミナル上
$ docker-compose build

実際にアプリケーションのコンテナに潜ってDBを作成する。

chromeのブラウザコンテナを立てる前に、DBコンテナ上に先ほどのdatabase.ymlにも記載したDBを作成していきます。
(chromeコンテナは、RSpecでのcapybaraを使ったテストでのみ使用するので、一度保留しておいても動作はするはずです。)
今回はすでに作成しているアプリがある前提ですので、以下のようにします。

ローカルのターミナル上
$ docker-compose run rails bundle exec rails db:create \
&& bundle exec rails db:migrate \
&& bundle exec rails db:seed # 必要に応じて

ここではまだコンテナを起動していないのでexecではなく、runを使用しています

webpackerもインストールして、サーバを起動してみる

Gemfileにwebpackがあることを確認して、

ローカルのターミナル上
$ docker-compose run rails bin/rails webpacker:install

実際にrails serverで起動できるかを試します。

ローカルのターミナル上
$ docker-compose up

ここまで問題がなければ、あとはテストのためのchromeコンテナを設定するだけです。

3-3 ブラウザをシュミレートしたテストを実行できるようにブラウザ用コンテナを設定する

これまでの構成で、http://localhost:3000にアクセスできるし、テストの実行もbundleを使って、gemがコンテナにインストールされているので問題ないかと思います。

ここからは、capybaraを使って、E2Eテストをする際にブラウザ操作をシュミレートしたい場合に、追加するべき項目について書いていきます。
railsアプリ用のDockerfileの中にchromeをインストール方法もネットでいくつか見ましたが、今回はブラウザのためのコンテナを用意することにしました。

[補足]用語の整理

  • capybara
    capybaraはRuby用のテストフレームワーク。デフォルトであるブラウザ操作のシュミレータは:rack_test?
  • selenium
    webアプリをブラウザで自動化するためのプロジェクトのこと。webdriver等を用いて、主にユーザのブラウザ操作などをシュミレートするテスト目的に用いられることが多い。
    https://www.selenium.dev/
  • webdriver
    ブラウザ操作を自動化するためのAPI群とプロトコル。実際にこの人に頼んで、ブラウザ操作をシュミレートしているイメージ
  • chromedriver
    chrome用のwebdriver
    参考にさせていただいた記事)

https://blog.mothule.com/tools/selenium/tools-selenium-glossary

Gemfile

Gemfileに必要なgemが入っているか確認します。

group :test do
  gem 'capybara', '>= 3.26'
  gem 'selenium-webdriver', '>= 4.0.0.rc1'
  gem 'webdrivers'
end

webdriversgemの方に、selenium-webdriverの依存性が記されていて、selenium-webdriverだけでも動作している記事も見たのですが、僕は全てのテストを回した際になんだかうまくいかなかったので、どちらも大人しく記述しています。(いずれ調べます。)

次に、記述したdocker-compose.ymlを見ていきます。

  chrome:
    image: selenium/standalone-chrome:latest
    ports:
      - 4444:4444

ここでは、seleniumのイメージを指定して、portsの設定もしています.

Capybara.rb

最後に、capybaraの設定をしていきます。今回はspec/support/capybara.rbというようにディレクトリを分けたので、Rspecが、自前で作成した設定ファイル(capybara.rb)も読み込んでくれることをまず確認します。

spec/rails_helper.rb
Dir[Rails.root.join('spec/support/**/*.rb')].sort.each { |f| require f }

こちらの記述をさせていることを確認してください。この話はdockerではなく、capybaraおよびRspecの設定に関することなので、これ以上は割愛します。

次に、capybaraに新たにドライバーを登録していきます。

spec/support/capybara.rb
Capybara.register_driver :remote_chrome do |app|
  url = 'http://chrome:4444/wd/hub'
  # http://<作成したコンテナ名>:<portsでフォワーディングさせたポート番号>/wd/hub
  capabilities = ::Selenium::WebDriver::Remote::Capabilities.chrome(
    'goog:chromeOptions' => {
      'args' => [
        'no-sandbox',
        'headless',
        'disable-gpu',
        'window-size=1680,1050'
      ]
    }
  )
  
  Capybara::Selenium::Driver.new(app, browser: :remote, url: url, capabilities: capabilities)
end

実際にwebdriverを動かす時は、remoteのブラウザ、urlでいうとhttp://chrome:4444/...にあたるもの(作ったコンテナ)を指定しているという認識です。
また、capabilitiesの部分が、古い記述だと動作しませんでした。
参考にさせていただいた記事)

https://qiita.com/mass584/items/c30e0762050a10503da7

同ファイル内で今登録したドライバーを Rspecでのテストで使う設定をします。

spec/support/capybara.rbの続き
RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :rack_test
  end
  # ここから追加した部分です。jsを用いるブラウザ動作がある時に、先ほど登録したドライバーで動作させます。
  config.before(:each, js: true, type: :system) do
      driven_by :remote_chrome #さっき登録したドライバ
      Capybara.server_host = IPSocket.getaddress(Socket.gethostname)
      Capybara.server_port = 4444
      Capybara.app_host = "http://#{Capybara.server_host}:#{Capybara.server_port}"
    end
  end
end

Capybara.server_host以降の文言の部分では、railsコンテナのホスト情報を取得して、railsコンテナ:4444 -> chrome:4444へのポートファワードをさせていると認識しています。いずれかの記述が欠けると動作しないので、あらかた間違っていないかと思いますが、まだ詳しくはわかってないです。
コンテナで、テストを回して見てください。

$ docker-compose exec rails bin/rails spec

テストについて参考にさせていただいた記事)

https://qiita.com/ryo_kh/items/2249c13d30648f50b9c8
https://qiita.com/at-946/items/e96eaf3f91a39d180eb3
https://qiita.com/masarashi/items/84761a4e8de494f4d073

デバッグするためには

前提のところで挙げたやりたいことは、docker-compose execなどでコンテナに入れば大抵できるようになっているかと思います。
debugについては、docker container attachコマンドを使う必要があります。

docker container attach

ローカル環境のstdin stdout stderrorを実行中のコンテナに対して取り付ける。あたかもコマンドを自分のターミナル上で直接実行しているかのように、それらの継続的な出力を見られるようになったり、インタラクティブ(双方向)に制御できます。終わる時は、ctrl P + Q
(実行中のコンテナでtty: true / stdin_open: trueにしておくこと。)

byebugメソッドなどをソースコードに仕込み、
docker-compose ps等で、railsコンテナのIDを調べてたあとに、

$ docker container attach <container ID>

これで、ログ表示が止まって,デバッグのプロンプトが立ち上がるはずです。
終了する時は ctrl P + Q

終わりに

技術にまつわる記事投稿自体が初めてかつ、自身にとって未だ学習中の分野であるために、思った5倍は長い記事になってしまいました。僕自身の不明点や、詰まった部分に適宜説明を加えたために、かなり曖昧なスコープの記事になってしまった感も否めません。
一応、dockerに関わる設定については、ファイル名ごとに見出しもつけてますので、コピペでもある程度動作させられるのかなと思っています。あとは適宜railsの設定などを調整してあげてください。
また、文中の所々に明確な説明しきれていない部分や、間違えている部分があるかと思いますので、読んでくださる方で詳しい方がいましたら、ぜひご指摘お願いします。読んで下さった皆様から忌憚なきご意見お聞かせいただけると幸いです。(文章が乱文であるとか、用語の使い方があまりに不明瞭など)。
最後に今回の環境構築は、ネット上でのたくさんの記事や情報によって成立していると思ってます、参考にさせていただいた記事を書いた方々へは本当に感謝します。また、僕自身もほんの少しでも、学習でつまづいている方の助けになる内容を提供できれば幸甚です。
あんまり固くなりすぎましたが、いいねでもなんでもレスポンスが欲しいです。よろしくおねがいします。

Discussion

ログインするとコメントできます