Martians流Docker+Ruby/Rails開発環境構築(翻訳)|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社エンジニアの「?」を「!」に。原著者の許諾を得て翻訳・公開いたします。Rails開発会社です。良質のブログ記事を多数公開し、多くのgemのスポンサーでもあります。Dolphin」にかけているのかもしれないと思いました。Whales(クジラ)はもちろんDockerのシンボルです。applications」↑の、いわばB面に相当します。この記事を読んで、皆さんがアプリケーション開発をDockerに乗り換えるとまでは考えていません(皆さんが以下の動画で若干言及しているのをご覧になっていたとしても)。本記事の狙いは、私が現在のRailsプロジェクトで用いている設定を皆さんと共有することです。development環境で生まれたものです。ご自由にお使いください。RAMのノートパソコンではあまりに重たかったのでした)。最初からバラ色のDocker人生だったわけではありません。自分のみならず、チームにとっても「十分にふさわしい」Docker設定を見つけるまでに2年という月日を費やしました。私の設定を、(ほぼほぼ)すべての行に解説を付けてご覧に入れたいと思います。「Dockerをわかっている」前提のわかりにくいチュートリアルはもうたくさんですよね。本記事のソースコードは、GitHubのevilmartians/terraforming-railsでご覧いただけます。本記事の例では以下を用います。Martians流DockerfileRailsアプリケーションの「環境」は、Dockerfileで定義します。c)、テスト、rakeタスク、開発者としてコードとのインタラクティブなやりとりは、ここで行います。/appこのDockerfileの設定は必要不可欠なもののみを含んでいますので、これを出発点にできます。このDockerfileで何が行われるかを解説します。最初の2行に少々妙なことが書かれています。ruby:2.6.3みたいに適当な安定版Rubyのバージョンを書いておけばよさそうなものですよね。ここではDockerfileを一種のテンプレートとして用い、環境を外部から設定可能にしたいのです。ランタイム依存の厳密なバージョンは、docker-compose.ymlの方で指定することにします(後述)。aptコマンドでインストール可能な依存のリストは、これも別ファイルに保存することにします(これも後述)。上に続く4行は、PostgreSQL、NodeJS、Yarn、Bundlerのバージョンを定義します。Composeなしで使う人などいないという前提なので、Dockerfileではデフォルト値を指定しないことにします。PostgreSQL、NodeJS、Yarnをaptコマンドでインストールするために、それらのdebパッケージのリポジトリをソースリストに追加する必要があります。installの実行など)。/var/log/*logまずAptfileという裏技について解説します。xargs)Aptfileというアイデアはheroku-buildpack-aptから拝借しました。heroku-buildpack-aptは、Herokuに追加パッケージをインストールできます。このbuildpackを使っていれば、同じAptfileをローカルでもproduction環境でも再利用できます(buildpackのAptfileの方が多くの機能を提供していますが)。私たちのデフォルトAptfileに含まれているのはたったひとつのパッケージです(私たちはRailsのcredentialの編集にVimを使っています)。vim私が携わっていた直前のプロジェクトでは、LaTeXやTexLiveを用いてPDFを生成しました。そのときのAptfileは、さしずめ以下のような感じにできたでしょう(当時私はこの技を使っていませんでしたが)。vimtexlivetexlive-latex-recommendedtexlive-fonts-recommendedtexlive-lang-cyrillicこのようにすることで、タスク固有の依存関係を別ファイルに切り出し、Dockerfileの普遍性を高めています。Ubuntu」という記事をご覧になることをおすすめします。--no-install-recommendsスイッチを指定すると、推奨パッケージのインストールを行わなくなるので容量を節約でき、ひいてはイメージをもっとスリムにできます。Xubuntu」をご覧ください。/var/log/*log)も目的は同じです。取得したパッケージファイルのローカルリポジトリ(ここまでで必要なものはすべてインストールできているので、これらはもはや不要です)や、インストール中に作成されたすべての一時ファイルやログをクリーンアップします。特定のDockerレイヤにごみを残さないようにするため、このクリーンアップ作業は同じRUNステートメントの中で行う必要があります。最後の部分は、もっぱらBundlerのためのものです。bundler:$BUNDLER_VERSIONLANG=C.UTF-8は、デフォルトロケールをUTF-8に設定します。これを行わないとRubyが文字列でUS-ASCIIを使ってしまうので、かわいいかわいい絵文字たちとおさらば?になってしまいます。gemインストールのパスはGEM_HOME=/bundleで設定します。この/bundleが何だかおわかりでしょうか?このパスは、依存関係を(開発環境などの)ホストシステムで永続化するために、後でボリュームとしてマウントすることになります。詳しくは後述のdocker-compose.ymlをご覧ください。BUNDLE_PATH変数とBUNDLE_BIN変数は、gemやRuby実行ファイルを探索する場所をBundlerに伝えます。最後に、Rubyとアプリケーションバイナリをグローバルに公開します。execを冒頭に付けなくてもrailsやrakeやrspecといった「binstub化されたコマンド」を実行できるようになります。Composeは、コンテナ化された環境をオーケストレーションするツールで、これを用いてコンテナ同士を接続し、永続化ボリュームやサービスを定義できます。以下は、データベースとしてPostgreSQLを、バックグラウンドジョブの処理にSidekiqを用いた、Railsアプリケーションの典型的な開発環境のためのdocker-compose.ymlです。packs:このdocker-compose.ymlでは8つのサービスを定義しています。サービスを8つも定義しているのを不思議に思うかもしれませんが、その一部は単に他と共有する設定を定義しているだけで(appやbackendといった抽象サービス)、残りはアプリケーションコンテナを用いる特定のコマンド(runnerなど)のためのものです。railsのように、実行したいサービスを常にピンポイントで指定するようにしています。development環境ではWebpackerやSidekiqなどを全部立ち上げる必要はめったにないので、合理的です。それでは各サービスを詳しく見ていくことにしましょう。appappサービスの主な目的は、(上のDockerfileで定義した)アプリケーションコンテナの構築に必要な情報をすべて提供することです。contextを定義します。これはビルドプロセスで用いる一種のワーキングディレクトリであり、COPYコマンドなどで用いられます。私たちの設定ではDockerfileへのパスを明示的に指定しています。理由は、私たちはDockerファイルをプロジェクトのルートディレクトリに配置するのではなく、.dockerdevという隠しディレクトリの中に他のすべてのDocker関連ファイルと一緒に配置しているからです。前述したように、Dockerfileではargsを用いて依存関係の正確なバージョンを指定しています。ここでひとつ注意すべきは、イメージにタグ付けする方法です。example-dev:1.0.0Dockerを開発に用いるメリットのひとつは、設定の変更を自動的にチーム全体で同期できることです。これは、ローカルイメージ(引数や、イメージが依存するファイルでもよい)を変更するたびにローカルイメージのバージョンをアップグレードしておきさえすれば可能です。逆に最悪なのは、ビルドタグにexample-dev:latestを使うことです。イメージのバージョンを維持しておけば、異なる2つの環境同士で余分な追加作業を一切行わずに済むようにもできます。長期間実行するchore/upgrade-to-ruby-3ブランチで作業している最中に、いつでもmasterブランチに切り替えて古いイメージや古いRubyを利用できます。docker-compose.yml内のイメージにlatestタグを使うのは最悪です。他にも、コンテナ内で/tmpフォルダにDockerのtmpfsマウントを用いるように指定することでスピードアップしています。/tmpbackendいよいよ本記事で一番美味しい部分にたどり着きました。このbackendサービスは、あらゆるRubyサービスで共有する振る舞いを定義します。volumes:を見てみましょう。.:/app:cached」では、現在のワーキングディレクトリ(つまりプロジェクトのルートディレクトリ)をコンテナ内の/appフォルダにマウントし、かつcached戦略を用いています。このcachedという修飾子は、MacOSでのDocker環境の効率を高めるうえで重要なポイントです。cachedについては別記事を書いていますので?、本記事ではこれ以上は深堀りしません。「こちらの公式ドキュメント」をご覧ください。その次の行では、/bundleの中身をbundleという名前のボリュームに保存するようコンテナに指示しています。私たちはこのようにして、gemのデータを永続化して複数の実行で使えるようにしています。--volumesを実行するまで持続します。以下の3行も、「DockerがMacだと遅い」という呪いをお祓いするために書かれています。私たちは、生成されたファイルをすべてDockerボリュームに配置することで、ホストマシンでディスク操作が重くなるのを回避しています。macOSでDockerを十分高速に動かすには、ソースファイルを:cachedでマウントし、かつ、生成されたコンテンツ(アセットやbundleなど)の保存にはボリュームを使うこと。末尾の3行では、特定のpsql設定をコンテナに追加しています。私たちはほとんどの場合、コマンド履歴をアプリのlog/.psql_historyに保存することで永続化する必要があります。dbconsoleを実行するときに内部で使われるからです。私たちの.psqlrcファイルには、履歴ファイルを環境変数経由で指定できるようにするために以下の仕掛けが施されています。履歴ファイルへのパスをPSQL_HISTFILE環境変数で指定できるようにし、利用できない場合は$HOME/.psql_historyにフォールバックします。$PSQL_HISTFILE`環境変数について説明します。WEB_CONCURRENCY=${WEB_CONCURRENCY:-1}話せばきりがありませんが、ここでは1箇所に注目したいと思います。X=${X:-smth}についてですが、これを人間の言葉で表せば「コンテナ内のXという変数については、ホストマシンにXという環境変数の値があればそれを用い、なければ別の値を用いる」ということです。railsのようにコマンドで別の環境を指定してサービスを実行できるのです。DATABASE_URL変数、REDIS_URL変数、WEBPACKER_DEV_SERVER_HOST変数は、Rubyアプリケーションを別のサービスに接続します。Recordで、WEBPACKER_DEV_SERVER_HOST変数はRailsのWebpackerでいつでもサポートされます。Cableは明示的に設定しなければなりません)。私たちはbootsnapを用いてアプリケーションの読み込みを高速化しています。bootsnapのキャッシュはBudlerのデータと同じ場所に保存しています。理由は、このキャッシュに含まれている内容のほとんどがgemのデータだからです。Rubyを別のバージョンにアップグレードするようなことがあれば、それらを一括廃棄するべきということです。HISTFILE=/app/log/.bash_historyは、開発者のUXにとって重要な設定です。この設定によって履歴が特定の場所に保管され、永続化されるようになります。credentials:editコマンドでcredentialファイルを管理するのに用います。末尾の2つの設定であるMALLOC_ARENA_MAXとWEB_CONCURRENCYは、Railsのメモリハンドリングをチェックしやすくするためのものです。他にbackendサービスで説明すべきは以下の行だけです。trueこの設定によって、サービスをインタラクティブ(TTYを提供するなどの対話的な操作)にできます。私たちの場合、たとえばRailsコンソールやBashをコンテナ内で実行するのに必要です。これは、-itオプションを付けてDockerコンテナを実行するのと同じです。webpackerwebpackerで言及しておきたいのはWEBPACKER_DEV_SERVER_HOST=0.0.0.0という設定だけです。serverに「外部から」アクセスできるようになります(デフォルトではlocalhostで実行されます)。runnerこのrunnerサービスの目的を説明するために、私がDockerを開発に用いるときの段取りについて説明させてください。私はDockerデーモンの起動で以下のようなカスタムdocker-startスクリプトを実行します。runのエイリアス)。runnerは以下のエイリアスになります。runner後はこのコンテナの中でほとんどの作業を行います(テストやマイグレーションやrakeタスクなど何でも構わない)。以上でおわかりのように、私は何かタスクを1つ実行する必要が生じるたびにいちいちコンテナを1つ立ち上げたりせず、いつも同じ設定でやっています。runnerを使っているのです。私がこれをshellと呼ばずにrunnerと呼んでいる理由はただひとつ、コンテナの中で任意のコマンドをrunするのにも使えるからです。このサービスをrunnerと呼ぶかどうかは好みの問題であり、(デフォルトのcommand(/bin/bash)は別としても)webサービスと比べて何ひとつ目新しい点はありません。/bin/bashと完全に同じです(ただし短い?)。Compose式のやり方がまだ難しいとお思いの方に、Dipというツールをご紹介します。Martiansのあるメンバーがこしらえたものです。dip.ymlは、複数のcomposeファイルを使い分ける場合や、プラットフォームに依存する複数の設定を使い分ける場合に特に便利です。dip.ymlはそれらをまとめて、Dockerでの開発環境を管理する一般的なインターフェイスを提供できるからです。dip.ymlについては別記事にて詳しく説明しようと思います。Merkushinに感謝いたします?。以下のスライドも合わせて読むことで、より理解が進むと思います。コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。チュートリアル第2版の監修および半分程度を翻訳、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れて更新翻訳中。enno.jpを運営。実は最近Go言語が好きで、Goで書かれたRubyライクなGoby言語のメンテナーでもある。仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。Storageを使ってはいけない(翻訳)#Tags#会社紹介#自社サービス#勉強会#Rails#PHP#デザイン#実績#週刊RailsウオッチCONTACTお気軽にご相談ください。(翻訳)CONTACTTechRachoでは、パートナーシップをご検討いただける方からのご連絡をお待ちしております。お気軽にご意見・ご相談ください。