活用 ENTRYPOINT

ENTRYPOINT 的設計,可以保證 container 啟動執行指令的時候,都一定會包含 ENTRYPOINT 設定。因此可以藉由這個特性讓 image 用起來更靈活。

以下會介紹幾個還不錯的設計,讓讀者參考。

純執行指令類型的 image

Composer 為例:

#!/bin/sh

isCommand() {
# sh 需要特別例外,因為 composer help 會誤以為是 show 指令
if [ "$1" = "sh" ]; then
return 1
fi

composer help "$1" > /dev/null 2>&1
}

# 當第一個參數看起來像 option 的話(如:-V、--help)
if [ "${1#-}" != "$1" ]; then
set -- /sbin/tini -- composer "[email protected]"

# 當第一個參數就是 composer 的話
elif [ "$1" = 'composer' ]; then
set -- /sbin/tini -- "[email protected]"

# 當第一個參數是 composer 的子指令(如:install、update 等)
elif isCommand "$1"; then
set -- /sbin/tini -- composer "[email protected]"
fi

# 其他全部都照舊執行
exec "[email protected]"

依照上面的腳本,可以轉換出以下 Docker 指令與實際執行指令的對照表:

docker run actual
docker run composer:1.10 --help /sbin/tini -- composer --help
docker run composer:1.10 composer --help /sbin/tini -- composer --help
docker run composer:1.10 install /sbin/tini -- composer install
docker run composer:1.10 sh sh

為節省版面,把 --rm 先省略。

從這個對照表可以看得出來,平常我們能把 docker run composer 作為取代 Composer 的指令,若需要使用 sh 進入 container 也可以順利執行,因為 ENTRYPOINT 都幫我們處理好了。

我們再回頭看一下 Container 應用是怎麼設定 alias 的:

# 使用 Composer
alias composer="docker run -it --rm -v \````$PWD:/source -w /source composer:1.10"

這個 alias 設定,正是把 docker run 取代原指令,但這也必須 image 的 ENTRYPOINT 配合才行。ENTRYPOINT 設計可以有兩種方向:

  1. 額外寫 shell script 做為 ENTRYPOINT,可以同時處理原指令,與系統的指令。官方的 image 大多都有這樣設計。
  2. 直接把指令設定為 ENTRYPOINT,這個 image 就只能用來執行它的子指令,不能做其他用途,如 oryd/hydra

服務類型的 image

MySQL 為例,它的 shell script 寫得比較複雜,這裡單純截取 _main() function:

_main() {
# 當第一個參數看起來像 option 的話,就用 mysqld 執行它
if [ "${1:0:1}" = '-' ]; then
set -- mysqld "[email protected]"
fi

# 當是 mysqld 且沒有會讓 mysqld 停止的參數時,執行 setup
if [ "$1" = 'mysqld' ] && ! _mysql_want_help "[email protected]"; then
mysql_note "Entrypoint script for MySQL Server ${MYSQL_VERSION} started."

mysql_check_config "[email protected]"
# 取得 Docker ENV 以及初始化目錄
docker_setup_env "[email protected]"
docker_create_db_directories

# 若是 root 身分,則強制使用 mysql 身分啟動
if [ "$(id -u)" = "0" ]; then
mysql_note "Switching to dedicated user 'mysql'"
exec gosu mysql "$BASH_SOURCE" "[email protected]"
fi

# 如果資料庫是空的,就建一個起來吧
if [ -z "$DATABASE_ALREADY_EXISTS" ]; then
docker_verify_minimum_env

ls /docker-entrypoint-initdb.d/ > /dev/null

docker_init_database_dir "[email protected]"

mysql_note "Starting temporary server"
docker_temp_server_start "[email protected]"
mysql_note "Temporary server started."

# 初始化資料庫,這裡將會用到 MYSQL_ROOT_PASSWORD、MYSQL_DATABASE、MYSQL_USER、MYSQL_PASSWORD 等環境變數
docker_setup_db

# 初始化資料庫目錄
docker_process_init_files /docker-entrypoint-initdb.d/*

mysql_expire_root_user

mysql_note "Stopping temporary server"
docker_temp_server_stop
mysql_note "Temporary server stopped"

echo
mysql_note "MySQL init process done. Ready for start up."
echo
fi
fi

# 不是 mysqld 就直接執行了
exec "[email protected]"
}

雖然有點長,不過概念簡單來說,ENTRYPOINT 的任務主要是:

  1. 以執行 mysqld 指令優先
  2. 準備環境、切換使用者
  3. 初始化資料庫、使用者、資料表等任務

ENTRYPOINT 會寫這麼複雜,正是為了 mysqld 與環境變數(MYSQL_ROOT_PASSWORD)的搭配下能正常執行。

今日自我回顧

若有想寫 Dockerfile 的話,了解 ENTRYPOINT 會是必要的。因為 ENTRYPOINT 是啟動 container 的必經之路,善用它將可以讓 image 用起來更加靈活。