この記事について
Rails開発環境用のDockerfileのみを題材に、私自身がとてもつまずきましたので、学習の中で深掘りして理解したことを、この記事にて整理しました。
復習も兼ねていますが、同じようにDockerfileでつまずいている方の参考になれば嬉しいと思いました。
初学者が学習しながら整理した内容のため、誤りや不正確な表現があるかもしれません。
もし間違いがあれば、ご指摘いただけると嬉しいです。
はじめに
先日、初めて1からDocker環境でRails開発環境を構築してみようとした際に、以下のようなコードを見たとき
RUN apt-get update -qq \
&& apt-get install -y ca-certificates curl gnupg \
&& mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
&& apt-get update -qq \
&& apt-get install -y build-essential libpq-dev nodejs
初学者の私にとって、Dockerfileの中身は完全に 呪文 でした...
ただ、一つずつ調べていくと、Dockerfile特有の構文というより、Linuxのインストール手順をDockerfile向けに整理して書いているだけだと、少しずつですが理解することができました。
今回作成したDockerfile
最終的に作成したDockerfileは以下のとおりです。
FROM ruby:3.2.3
ENV LANG=C.UTF-8
ENV TZ=Asia/Tokyo
RUN apt-get update -qq \
&& apt-get install -y ca-certificates curl gnupg \
&& mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
&& apt-get update -qq \
&& apt-get install -y build-essential libpq-dev nodejs
RUN npm install -g yarn@1.22.22
WORKDIR /myapp
COPY Gemfile Gemfile.lock ./
RUN bundle install
COPY . .
この記事では、このDockerfileを理解するまでに、自分がどのように調べていったのかをまとめます。
まずDockerfileの命令をざっくり整理する
Dockerfileには、インストラクションと呼ばれる命令を書いていきます。
今回使った命令は以下です。
| 命令 | ざっくりした意味 |
|---|---|
FROM |
ベースになるDockerイメージを指定する |
ENV |
環境変数を設定する |
RUN |
ビルド時にコマンドを実行する |
WORKDIR |
作業ディレクトリを指定する |
COPY |
ホスト側(私のPC)のファイルをコンテナ内にコピーする |
それぞれの命令の意味を調べると、なんとなく役割はわかりましたが、正直なところ、次は何するの?状態でした。
例えば、RUN が「コマンドを実行する命令」だとわかっても
RUN apt-get update -qq \
&& apt-get install -y build-essential libpq-dev nodejs
この中で何をしているのかは、よくわかりませんでした...
参考にしたDocker公式ドキュメント
まず、調べて最初に参考にしたのは、Docker公式ドキュメントにあるRailsとPostgreSQLのクイックスタートでした。
そこでは、以下のようなDockerfileが紹介されていました。
FROM ruby:2.3.3
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs
RUN mkdir /myapp
WORKDIR /myapp
ADD Gemfile /myapp/Gemfile
ADD Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
ADD . /myapp
正直、この時点で理解できてたのは
FROM ruby:2.3.3
で、Rubyのイメージを指定している、ということくらいでした。
それ以外の部分は、何をしているのかほとんどわかりません。
特に気になったのが、次の行です。
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs
この時点では、apt-get が何なのかもよくわかっていませんでしたので、まずは apt-get が何をするものなのかを調べました。
apt-getとは何か
apt-get は、Debian系Linuxで使われるパッケージ管理コマンドとのこと
ざっくり言うと、必要なソフトウェアをインストールしたり、パッケージ情報を更新したりするために使います。
ここで初めて、DebianやLinuxという言葉が出てきました。
最初はRubyのDockerfileを見ているつもりだったので、DebianやLinuxの話が出てくることに少し違和感がありました。
ただ、DockerのRuby公式イメージは、Rubyだけでできているわけではなく、Rubyを動かすための土台として、Linux環境も含まれています。
そして、DebianとはLinuxの種類の1つということを理解。
なので、今回使っている ruby:3.2.3 のようなイメージでは、その土台にDebian系のLinuxが使われています。
そのため、Dockerfileの中でパッケージをインストールするときに、Debian系Linuxで使われる apt-get が出てくる、ということでした。
自分なりに整理すると、以下のようなイメージです。
RubyのDockerイメージを使っている
↓
でも中にはLinux環境も含まれている
↓
そのLinuxがDebian系
↓
だから apt-get が使える
ここまで整理して、ようやく 少し先に進めるなという感じでした。
apt-get update と apt-get install
apt-get がDebian系Linuxで使うコマンドだとわかったので、次に以下の部分を分けて見ていきました。
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs
apt-get update
apt-get update -qq
これは、インストールできるパッケージの一覧情報を更新しています。
最初は「何をアップデートしているのか」がよくわかりませんでした。
自分なりには、
どのパッケージが、どこから、どのバージョンでインストールできるかの情報を更新している
という理解をしました。
ソフトウェア本体を更新しているというより、パッケージ一覧を最新にしているイメージです。
-qq はquiet quietの省略でログの出力を少なくするためのオプションとのこと。
Docker build中のログを少し見やすくするために使われています。
apt-get install
apt-get install -y build-essential libpq-dev nodejs
これは、指定したパッケージをインストールしています。
最初は build-essential libpq-dev nodejs がひとまとまりの言葉に見えていました。
しかし、実際には以下のように、複数のパッケージ名をスペース区切りで並べているだけでした(お恥ずかしい)
apt-get install -y パッケージ1 パッケージ2 パッケージ3
今回の場合は、以下の3つをインストールしています。
| パッケージ | 役割 |
|---|---|
build-essential |
C/C++などで書かれたプログラムをコンパイル(機械語へ変換)するための基本的なツール一式 |
libpq-dev |
RailsからPostgreSQLを利用するために必要な部品が入ったパッケージで、 pg gem を使うために必要 |
nodejs |
JavaScript関連の処理で使うNode.js本体 |
また、-y はインストール時の確認に自動で yes と答えるためのオプションです。
Docker build中に確認待ちで止まらないようにするために付けています。
公式例から変更したところ
Docker公式ドキュメントの例をそのまま使うのではなく、今回のDockerfileでは自分なりに調べて、少し書き方を変えています。
主に変えたのは以下です。
-
RUN mkdir /myappを省略した -
ADDではなくCOPYを使った -
GemfileとGemfile.lockを先にコピーしてbundle installした
RUN mkdir /myapp は省略した
公式ドキュメントの例では、以下のように書かれていました。
RUN mkdir /myapp
WORKDIR /myapp
ただ、現在のDockerfileでは、WORKDIR で指定したディレクトリが存在しない場合、自動で作成されます。
そのため、今回のように /myapp を作業ディレクトリにしたいだけであれば、以下のように書けば大丈夫です。
WORKDIR /myapp
もちろん、RUN mkdir を使う場面がないわけではありません。
例えば、複数のディレクトリをまとめて作りたい場合や、ユーザー・権限を細かく設定したい場合などは RUN mkdir を使うこともあるとのことです。
ただ、今回のDockerfileでは不要だと判断しました。
ADDではなくCOPYを使った
公式ドキュメントの例では、以下のように ADD が使われていました。
ADD Gemfile /myapp/Gemfile
ADD Gemfile.lock /myapp/Gemfile.lock
ただ、今回は以下のように COPY を使いました。
COPY Gemfile Gemfile.lock ./
ADD と COPY はどちらも、ホスト側のファイルをDockerイメージ内にコピーする命令です。
ただし、ADD にはURLからファイルを取得できるなど、追加の機能があるらしいですが、単純にファイルをコピーするだけなら、COPY の方が意図がわかりやすいと思ったので、今回は COPY を使っています。
Gemfileを先にCOPYする理由
次に気になったのが、この部分です。
COPY Gemfile Gemfile.lock ./
RUN bundle install
COPY . .
最初は、なぜ先に Gemfile と Gemfile.lock だけコピーするのかがわかりませんでした。
また、 Gemfile と Gemfile.lock を先に準備しておかないといけないとは思っていませんでした。
Gemfile には、Railsアプリで使うgemを記述します。
source 'https://rubygems.org'
gem 'rails', '~> 7.2'
そして、Gemfile.lock には実際にインストールされたgemのバージョンが記録されます。
ざっくり言うと、以下のような違いだと思います。
Gemfile :必要なgemを書くファイル
Gemfile.lock :実際に入ったgemのバージョンを記録するファイル
Dockerfileで以下の順番にしているのは、Dockerのキャッシュを活かしやすくするためでもあります。
COPY Gemfile Gemfile.lock ./
RUN bundle install
COPY . .
先に Gemfile と Gemfile.lock だけをコピーして bundle install しておくことで、アプリのコードだけを変更した場合に、毎回gemのインストールをやり直さなくて済むことがあります。
ここは最初かなり疑問でしたが、Dockerfileではよく見る書き方だとわかりました。
一度できそうなところまで書いてみる
ここまで調べて、基本的な命令や Gemfile まわりの意味は少しわかってきました。
そこで、一度できそうなところまでDockerfileを書いてみました。
FROM ruby:3.2.3
ENV LANG=C.UTF-8
ENV TZ=Asia/Tokyo
WORKDIR /myapp
COPY Gemfile Gemfile.lock ./
RUN bundle install
COPY . .
ただ、この時点ではまだパッケージのインストール部分を書けていませんでした...
特に迷ったのが、Node.jsのインストール方法です。
Node.jsのインストールで迷ったところ
最初は、以下のように書けばNode.jsもインストールできると思っていました。
RUN apt-get update -qq \
&& apt-get install -y build-essential libpq-dev nodejs
実際、Debian標準のリポジトリから nodejs をインストールすることはできます。
ただし、この場合はDebian側で用意されているバージョンのNode.jsが入ります。
RailsでImport Mapsを使うだけなら、必ずしもNode.jsが必要ない場合もありますが、esbuild、webpack、jsbundling-rails などを使う場合は、Node.jsが必要になることがあります。
また、フロントエンド系のツールでは比較的新しいNode.jsを前提としていることもあります。
そのため、今回は学習も兼ねてNode.js 20系を使うことで進めていきました
Node.jsのインストールはGitHubに公式手順があった
ここが、個人的にはかなり大きな気づきでした。
技術記事や他の方のDockerfileを見ていると、以下のような記述がありました。
RUN apt-get update -qq \
&& apt-get install -y ca-certificates curl gnupg \
&& mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
&& apt-get update -qq \
&& apt-get install -y build-essential libpq-dev nodejs
最初は、curl -fsSL や gpg --dearmor、tee など、知らないものが多くてかなり難しく感じました。
特に、自分の中では
apt-get install -y build-essential libpq-dev nodejs
のように書けばパッケージをインストールできる、というところまでは少し理解できていました。
しかし、その前に書かれている
mkdir -p /etc/apt/keyrings
curl -fsSL ...
gpg --dearmor ...
tee /etc/apt/sources.list.d/...
の部分が何をしているのか、最初はまったくわかりませんでした。
そこで調べていくと、NodeSourceの公式リポジトリがGitHubにあることを知りました。
NodeSourceのGitHubリポジトリには、Debian系LinuxにNode.jsをインストールするための手順が載っています。
最初は、Node.jsのインストール手順というと公式サイトのどこかにあるものだと思っていたので、GitHubのREADMEやWikiに公式の手順がまとまっていること自体、あまりイメージできていませんでした。
自分が見たのは、NodeSourceの公式リポジトリにあるManual Installationの手順です。
そこには、Debian Systems向けの手順として、ざっくり以下のような流れが書かれていました。
# Update local package index
sudo apt-get update
# Install necessary packages for downloading and verifying new repository information
sudo apt-get install -y ca-certificates curl gnupg
# Create a directory for the new repository's keyring, if it doesn't exist
sudo mkdir -p /etc/apt/keyrings
# Download the new repository's GPG key and save it in the keyring directory
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
# Add the new repository's source list with its GPG key for package verification
echo "Types: deb
URIs: https://deb.nodesource.com/node_${NODE_MAJOR}.x/
Suites: nodistro
Components: main
Signed-By: /etc/apt/keyrings/nodesource.gpg" | sudo tee /etc/apt/sources.list.d/nodesource.sources
この手順を見てから、技術記事やDockerfileに書かれていた長い RUN 命令とつながりました。
つまり、Dockerfileに書かれていた長い処理は、誰かが適当に書いた謎のコマンドではなく、NodeSource公式のインストール手順をDockerfile向けに書き換えたものだったということです。
ここに気づいてから、私はかなり理解しやすくなりました。
公式手順とDockerfileの違い
NodeSource公式の手順は、ターミナルで手動実行する前提で書かれています。
そのため、以下のように sudo が付いています。
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
sudo mkdir -p /etc/apt/keyrings
一方、Dockerfileのビルド中は基本的に管理者権限を持つrootユーザーで実行されるため、sudo は不要です。
また、公式手順では複数のコマンドが分かれて書かれていますが、Dockerfileでは1つの RUN 命令にまとめることがあります。
そのため、以下のように書き換えられています。
RUN apt-get update -qq \
&& apt-get install -y ca-certificates curl gnupg \
&& mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
&& apt-get update -qq \
&& apt-get install -y build-essential libpq-dev nodejs
整理すると、変わっているのは主に以下です。
| 公式手順 | Dockerfile |
|---|---|
sudo を使う |
sudo を外す |
| コマンドを1つずつ実行する |
&& でつなげて1つの RUN にまとめる |
| ターミナルで手動実行する | Docker build時に自動で実行される |
| 複数行で手順として書かれている |
\ で改行して読みやすくする |
このように見ると、Dockerfileの長い RUN 命令は、Docker専用の特殊な書き方というより、公式のLinux向けインストール手順をDockerfileの形に整えているものだとわかりました。
NodeSourceの手順で実際にやっていること
大きく分けると、やっていることは以下です。
- パッケージ一覧を更新する
- 必要なツールをインストールする
- GPGキーを保存するディレクトリを作る
- NodeSourceのGPGキーを取得して保存する
- APTにNodeSourceのリポジトリを追加する
- もう一度パッケージ一覧を更新する
- Node.jsなどをインストールする
一つずつ見ると、特別なことをしているというより、Node.jsを安全にインストールするための準備を順番に行っているだけでした。
必要なツールを入れる
apt-get install -y ca-certificates curl gnupg
ここでは、NodeSourceのリポジトリを追加するために必要なツールを入れています。
| パッケージ | 役割 |
|---|---|
ca-certificates |
HTTPS通信で使う証明書 |
curl |
URLからファイルを取得するツール |
gnupg |
GPGキーを扱うためのツール |
GPGキーを保存する場所を作る
mkdir -p /etc/apt/keyrings
GPGキーを保存するためのディレクトリを作成しています。
-p を付けることで、すでにディレクトリが存在していてもエラーになりません。
NodeSourceのGPGキーを取得して保存する
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
ここでは、NodeSourceのGPGキーを取得して、APTが扱いやすい形式で保存しています。
curl -fsSL は、指定したURLからファイルを取得するための書き方です
また、-fsSL は、細かく分けると、エラー時は失敗として扱う(-f)、余計な表示は減らす(-s,-S)、リダイレクトがあれば追いかける(-L)、といったオプションが付いています。
| はcurlで取得したデータをgpgに渡すという意味です。
gpg --dearmorは詳細割愛しますが、ASCII形式のGPGキーをAPTが扱いやすいバイナリ形式へ変換するものです。
そして、-o は出力先を指定しています。
最初はかなり難しく見えましたが、ざっくり言うと
NodeSourceのGPGキーを取得(curl)する
↓
APTが使える形式に変換する
↓
/etc/apt/keyrings/nodesource.gpg に保存する
という流れだと考えています。
NodeSourceのリポジトリをAPTに追加する
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
ここでは、Node.jsをどこから取得するかをAPTに登録しています。
今回の場合は、Node.js 20系を取得するために、以下のリポジトリを追加しています。
https://deb.nodesource.com/node_20.x
また、signed-by=/etc/apt/keyrings/nodesource.gpg によって、先ほど保存したGPGキーで署名されたパッケージを信頼するように指定しています。
その内容を tee で以下のファイルに保存しています。
/etc/apt/sources.list.d/nodesource.list
もう一度 apt-get update する
apt-get update -qq
NodeSourceのリポジトリを追加したので、もう一度パッケージ一覧を更新しています。
これをしないと、APTがNodeSourceから取得できるNode.jsを認識できません。
最後に必要なパッケージをインストールする
apt-get install -y build-essential libpq-dev nodejs
最後に、Rails開発に必要なパッケージをインストールしています。
この時点ではNodeSourceのリポジトリを追加済みなので、nodejs はNodeSource側からインストールされます。
なぜyarnはNode.jsの後に書くのか
yarnは、JavaScriptのパッケージを管理するためのツールです。
Railsアプリでは、JavaScriptやCSSをビルドするときに、Node.jsまわりのパッケージを使うことがあります。
そのため、Railsの開発環境ではNode.jsと一緒にyarnを入れておくことがあります。
Node.jsのインストール後に、今回のDockerfileでは以下のようにyarnをインストールしています。
RUN npm install -g yarn@1.22.22
最初は、この行も少し疑問でした。
なぜなら、ここだけ apt-get install ではなく、npm install を使っているからです。
npm は、Node.jsと一緒に入るパッケージ管理ツールであるとのこと。
そのため、先にNode.jsをインストールしていないと、そもそも npm コマンドを使うことができません。
つまり、順番としては以下になります。
Node.jsをインストールする
↓
Node.jsと一緒にnpmが使えるようになる
↓
npmを使ってyarnをインストールする
今回のDockerfileでは、NodeSourceからNode.jsをインストールした後に、npmを使ってyarnを入れています。
RUN npm install -g yarn@1.22.22
-g は、グローバルにインストールするという意味です。
コンテナ内で yarn コマンドを使えるようにするために付けています。
また、今回は以下のようにバージョンを指定しています。
yarn@1.22.22
バージョンを固定しておくことで、環境によってyarnのバージョンが変わることを防ぎやすくなるらしいので、ここは参考にさせてもらっています。
自分なりに整理すると、この部分は以下のような意味でした。
Node.jsを入れる
↓
npmが使えるようになる
↓
npmでyarnを入れる
↓
RailsのJavaScriptまわりでyarnを使えるようにする
そのため、RUN npm install -g yarn@1.22.22 は、Node.jsをインストールした後に書く必要があります。
改めてDockerfileを見る
ここまで調べて、ようやく最初のDockerfileの意味が少しつながりました。
FROM ruby:3.2.3
ENV LANG=C.UTF-8
ENV TZ=Asia/Tokyo
RUN apt-get update -qq \
&& apt-get install -y ca-certificates curl gnupg \
&& mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
&& apt-get update -qq \
&& apt-get install -y build-essential libpq-dev nodejs
RUN npm install -g yarn@1.22.22
WORKDIR /myapp
COPY Gemfile Gemfile.lock ./
RUN bundle install
COPY . .
最初はDocker専用の難しい構文に見えていました。
しかし、実際には
- Debian系Linuxの上にRubyが入っている
- Debian系なので
apt-getが使える - Node.js 20系を使うためにNodeSourceのリポジトリを追加している
- Node.jsを入れた後、npmを使ってyarnを入れている
- 最後にRailsアプリのgemをインストールし、アプリ本体をコピーしている
という流れでした。
まとめ
今回Dockerfileを書いてみて、最初はほとんど意味がわかりませんでした。
特に長い RUN 命令は、完全に呪文です。
ただ、一つずつ分解してみると、
- RubyのDockerイメージの中には、Debian系Linuxの環境も含まれている
- そのため、Debian系Linuxで使われる
apt-getが出てくる -
apt-get installは必要なパッケージをインストールしている -
build-essentialやlibpq-devはRailsで使うgemのビルドに関係している - NodeSourceの公式手順はGitHubにあり、それをDockerfile向けに整理している
- NodeSourceの部分はNode.js 20系をインストールするための準備
-
npm install -g yarn@1.22.22は、Node.jsを入れた後にnpmでyarnを入れている -
COPY Gemfile Gemfile.lock ./は先にgemをインストールするため - 長い
RUN命令は、Linuxのインストール手順をDockerfile向けに整理したもの
ということと考えて、自分の中に落とし込むことができました。
Dockerfileは、Docker専用の特別な構文だけでできているわけではなく、Linuxのコマンドを順番に実行して環境を作っている部分が多いのだと感じました。
まだ完全に理解できたわけではありませんが、同じようにDockerfileでつまずいている初学者の方の参考になれば嬉しいです。
おまけ
Rails 7.1以降ではDockerfileが自動生成される
Rails 7.1以降では rails new 実行時にDockerfileなどのDocker関連ファイルが自動生成されます。
また、Rails 7.2では生成されるDockerfileに jemalloc が追加されるなど、内容も改善されているようです。
今回は学習のために自分でDockerfileを読み解きながら作成しましたが、自動生成されるDockerfileを理解するうえでも、今回のように一度中身を分解してみたことは意味があったと感じました。
ARGを使ったRubyバージョンの指定
今回のDockerfileではシンプルにするためにFROM ruby:3.2.3と直接書きましたが、他のDockerfileでは以下のように ARG を使ってRubyのバージョンを指定していることが多いです。
ARG RUBY_VERSION=3.2.3
FROM ruby:${RUBY_VERSION}-slim
ARG を使うと、Rubyのバージョンを変数として管理できます。
apt と apt-get の違い
apt と apt-get は、どちらもDebian系Linuxでパッケージを扱うためのコマンドです。
apt は人がターミナルで使いやすいように作られた比較的新しいコマンドで、apt-get は昔から使われているコマンドです。
apt-get は古くから使われている分、スクリプトやDockerfileなど、自動で実行する環境構築の手順でもよく使われているとのこと。
そのため、今回のDockerfileでも apt-get を使っています。