📘

railsが生成するDockerfileのENTRYPOINTとCMDの設計について調べた

に公開

railsが生成するDockerfileを例にENTRYPOINTとCMDについて調べる。

現状生成されるDockerfileは下記の通り。

# Dockerfile
...
# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]

# Start server via Thruster by default, this can be overwritten at runtime
EXPOSE 80
CMD ["./bin/thrust", "./bin/rails", "server"]

コンテナイメージは次の動作をする。

  • ENTRYPOINT命令によりdocker-entrypoint.sh は常に実行される(docker-entrypoint.shでDB初期化を実施)
  • CMD命令で./bin/rails serverを指定することにより、デフォルトではrailsサーバーが起動
  • 必要に応じてdocker runコマンド実行時に、CMD命令を上書きして別のコマンドを実行可能(例. docker run -it rails ./bin/rails consoleなど)

背景

gitのHistoryとして、当初はENTRYPOINT命令のみを使用し、その後拡張性のためCMD命令でデフォルトの挙動を指定するようにしている。

https://github.com/rails/rails/pull/46762

ENTRYPOINT命令でbin/docker-entrypoint.shを指定し、内部で引数有無によりで分岐し、bin/rails COMMANDを実行している。デフォルトの挙動はbin/rails serverとなっていた。

このときのdocker runの実行方法は次の通りとなる。

docker run -it app # bin/rails serverを起動
docker run -it app console # bin/rails consoleを起動

https://github.com/rails/rails/pull/46778

その後、ENTRYPOINT命令のデフォルト引数として、CMD命令を利用し./bin/rails serverとして動作するようにしている。

これまではENTRYPOINT命令で指定するbin/docker-entrypoint.shbin/rails COMMANDとなっていたためworker用の起動コマンドを指定するために別のDockerイメージを作成する必要があった。この修正により、webとworkerで同じコンテナイメージを利用できるようになった。

docker runの実行方法は次の通りとなる。

docker run -it app # bin/rails serverを起動
docker run -it app bin/rails console # bin/rails consoleを起動
docker run -it app bundle exec sidekiq # workerを動作できるようになる
docker run -it app /bin/bash # bash起動できる

ENTRYPOINTとCMDの違いについて

https://www.docker.com/blog/docker-best-practices-choosing-between-run-cmd-and-entrypoint/

CMD命令は、コンテナイメージからコンテナを起動するときに実行するデフォルトのコマンドを指定する。

CMD ["apache2ctl", "-DFOREGROUND"]

コンテナ実行時に簡単に上書き可能。
docker run -it <image> /bin/bashでapacheを起動する代わりに、bashを起動することができる。

ENTRYPOINT命令は、コンテナのデフォルトの実行可能ファイルを指定する。docker run コマンドで指定された引数は、ENTRYPOINTコマンドに追加される。
コンテナで常に同じ基本コマンドを実行する必要があり、ユーザーが最後に追加のコマンドを追加できるようにする場合に使用する。

railsのコンテナイメージは、ENTRYPOINT命令 と CMD命令 の組み合わせにより、docker run コマンドで指定されたコマンドを柔軟に実行できる。これにより:

  1. デフォルトでは CMD命令 で指定された rails server が実行される
  2. docker run 実行時に異なるコマンドを指定することで、同じイメージから:
    • rails console
    • bundle exec sidekiq
    • その他のRailsアプリケーション関連コマンド
      を実行できる

ENTRYPOINTとCMDの違いについて調査

bin/docker-entrypointにログを出力してENTRYPOINTとCMDの違いについて調査する。

#!/bin/bash -e

echo "=== [1] Starting docker-entrypoint script"
echo "Arguments received: $@"

# Enable jemalloc for reduced memory usage and latency.
if [ -z "${LD_PRELOAD+x}" ]; then
    LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit)
    export LD_PRELOAD
fi

# If running the rails server then create or migrate existing database
if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then
  echo "=== [2][bin/rails server] Preparing database ==="
  ./bin/rails db:prepare
fi

echo "=== [3] Executing command: $@"  
exec "${@}"

Dockerfileをbuildして、コンテナイメージを作成する。

docker build -t rails-docker .

ENTRYPOINTとCMDを指定したDockerfileの場合

bin/docker-entrypointがコンテナ起動時のエントリーポイントになることを確認できる。
bin/docker-entrypointへの引数はCMD命令で指定した./bin/thrust ./bin/rails serverとなる。

docker run rails-docker
=== [1] Starting docker-entrypoint script ===
Arguments received: ./bin/thrust ./bin/rails server
=== [2][bin/rails server] Preparing database ===
=== [3] Executing command: ./bin/thrust ./bin/rails server ===

docker runコマンドで、別コマンド実行が可能。/bin/bashを指定する。

docker run -it rails-docker /bin/bash
=== [1] Starting docker-entrypoint script ===
Arguments received: /bin/bash
=== [3] Executing command: /bin/bash ===
rails@4021d75e4b1c:/rails$ 

./bin/rails consoleを指定する。

docker run -it rails-docker ./bin/rails console
=== [1] Starting docker-entrypoint script
Arguments received: ./bin/rails console
=== [3] Executing command: ./bin/rails console

CMDのみを指定したDockerfileの場合

ENTRYPOINTをコメントアウトする。

# Dockerfile
# Entrypoint prepares the database.
- ENTRYPOINT ["/rails/bin/docker-entrypoint"]
+ # ENTRYPOINT ["/rails/bin/docker-entrypoint"]

# Start server via Thruster by default, this can be overwritten at runtime
EXPOSE 80
CMD ["./bin/thrust", "./bin/rails", "server"]

bin/docker-entrypointは起動しないためログ表示はされない。

docker run rails-docker
# ログ表示なし
docker run -it rails-docker /bin/bash
# ログ表示なし
rails@79107717e0cc:/rails$ 

ENTRYPOINTのみを指定したDockerfileの場合

CMDをコメントアウトする。

# Dockerfile
Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]

# Start server via Thruster by default, this can be overwritten at runtime
- EXPOSE 80
- CMD ["./bin/thrust", "./bin/rails", "server"]
+ # EXPOSE 80
+ # CMD ["./bin/thrust", "./bin/rails", "server"]

ENTRYPOINTに対するCMD引数が未指定のため、@でパラメータがないためコンテナ起動に失敗する。

docker run rails-docker
=== [1] Starting docker-entrypoint script
Arguments received:
=== [3] Executing command:
/rails/bin/docker-entrypoint: line 14: [: ==: unary operator expected

docker run commandに引数を指定すれば起動可能

docker run -it rails-docker ./bin/rails server
=== [1] Starting docker-entrypoint script
Arguments received: ./bin/rails server
=== [2][bin/rails server] Preparing database ===
=== [3] Executing command: ./bin/rails server

まとめ

Railsでは以下のような構成を採用している:

  1. ENTRYPOINT(bin/docker-entrypoint

    • 共通の初期化処理を担当
    • サーバー起動時のみデータベース準備を実行
    • 受け取った引数に基づいて処理を分岐
  2. CMD(["./bin/thrust", "./bin/rails", "server"]

    • デフォルトではWebサーバーを起動
    • 必要に応じて他のコマンド(console、worker等)に置き換え可能

この組み合わせにより、1つのDockerイメージで複数の実行モード(Web、Worker、Console等)に対応できる。

参考

https://pocketstudio.net/2020/01/31/cmd-and-entrypoint/

https://catalog.workshops.aws/ecs-web-application-handson/ja-JP/docker/image

コンテナの概念の整理

項目 設定値
コンテナイメージ OS やアプリケーションのファイルを含むファイルシステムのようなもので、コンテナのテンプレートとなる。
コンテナ コンテナイメージを元に起動されるアプリケーションプロセス。
イメージレジストリ コンテナイメージを保管および提供するサービスで、リポジトリにコンテナイメージを格納する。
Dockerfile コンテナイメージの作成手順をコードとして記述したファイル。

Discussion