dockerfileはdockerHubからプルしてきたベースのイメージに対してdockerfileで記述した変更・修正を加えて新しくイメージをビルドするのに用いる
dockerイメージをビルドしよう
dockerfileは端的にいえば、ベースイメージに対する変更点を記述したファイルのことです。
コンテナの作成方法として一般的なのは、
・必要最低限なLinuxだけが入ったイメージファイル
に対して
・必要なアプリケーションを追加
していくことで作成します。
元になるベースイメージに対して、追加していく設定はdockerfileに記述します。
コンテナは容量が小さいほど運用しやすく、コンテナの特性に適っています。
従って、アプリケーションを動かす土台のLinuxイメージは小さいほど運用面のメリットがあります。
また、不要なコンポーネントが少ないほど脆弱性となる標的が少なくなるのでセキュリティ上も好ましいです。
気を付けるポイントとしては
・不要なアプリケーション等がなるべく含まれない(シェルでさえバイナリ実行だけなら不要)
・必要なライブラリを含むか
・メンテされているか
例えば軽量で有名なAlpineは定番ですがmusl libcの互換性の問題があるため、googleが管理しているgcr.ioの軽量なdebianイメージなどを利用するのをお勧めします。
dockerコンテナイメージを作成する上での注意点
軽量化
dockerイメージは軽量であるほど
1.セキュリティ上好ましい(Attack surface(攻撃対象面)が減少する)
2.モビリティやデプロイの高速化
などのメリットがある。
対策としては
・軽量なベースイメージを利用する→alpineやgcr.ioのdistrolessイメージを利用
・バイナリのみを載せる
・不要なソフトウェアは入れない
buildの高速化
・Multi Stage buildを実行
・キャッシュ(apt-get updateなどの処理は最初に持ってくる
バージョンを指定する
コンテナ作成ごとに中身が変わるような設定は好ましくありません。
例えばベースイメージのバージョン指定をlatestにしているとタイミングによってバージョンが変わってしまい、最悪動かなくなるという可能性もあります。
指定可能であればバージョンは固定しておくのが良いです。
イメージの作成方法
dockerfileで作成するか、設定済みのコンテナからイメージを生成する方法があります。
IaC的な観点からはdockerfileからの生成が好ましいと思います。動いてるコンテナをパッとイメージ化したいみたいな場合はコンテナから生成しても良いでしょう。
ただし、イメージの受け渡しになるため転送もたいへんです。dockerfileはただのテキストなので可搬性が良く、イメージの機能もdockerfileを見ればわかるし、変更も加えやすいです。
dockerfileの作成
必要な機能を入れられるbaseイメージの選択を済ませたらdockerfileを記述していきます。
goのWebサーバ実行用のコンテナを作成するdockerfileを作成します。
FROM golang:1.16-alpine
# Set destination for COPY
WORKDIR /app
# Download Go modules
COPY go.mod .
COPY go.sum .
RUN go mod download
# Copy the source code. Note the slash at the end, as explained in
# https://docs.docker.com/engine/reference/builder/#copy
COPY *.go ./
# Build
RUN go build -o /docker-gs-ping
# This is for documentation purposes only.
# To actually open the port, runtime parameters
# must be supplied to the docker command.
EXPOSE 8080
# (Optional) environment variable that our dockerised
# application can make use of. The value of environment
# variables can also be set via parameters supplied
# to the docker command on the command line.
#ENV HTTP_PORT=8081
# Run
CMD [ "/docker-gs-ping" ]
Dockerfileの構成
Dockerfileの構成を詳しく見ていく
Dockerfileは複数のセクションに分かれている。上記のDockerfileには下記の項目がある。
- FROM
- WORKDIR
- COPY
- RUN
- EXPOSE
- CMD
これらはレイヤと呼ばれる。
FROMなどの後には命令が続く。(このレイヤの構成によってビルドの速さが変わるがここでは説明しない)
FROM
FROMはベースイメージを指定する。上の例ではgolang:1.16-alpineのイメージを利用する
FROM golang:1.16-alpine
イメージの詳細
指定してるイメージを単体でPullもできる
$ docker pull golang:1.16-alpine
$ docker run -dit --name golang-container golang:1.16-alpine
$ docker ps
8ffc4b3fcb79 golang:1.16-alpine "/bin/sh" 2 minutes ago Up 2 minutes golang-container
## ↑のgolang-containerが起動している
## シェルを起動してコンテナの内部に入ることができる
$ docker exec -it golaong-container /bin/sh
# ls
/bin /src
## go コンテナ内でgoスクリプトを実行する
# cat hello.go
package main
import "fmt"
func main() {
fmt.Printf("Hello World\n")
}
/go/src # go run hello.go
Hello World
/go/src # which go
/usr/local/go/bin/go
WORKDIR
コンテナ内の作業ディレクトリを指定する。WORKDIR指定した以降の操作は指定したディレクトリで行われる。FROMで指定したイメージ内にディレクトリがなければ作成される。
WORKDIR /app
/appがなければ作成される。これ以降の作業は/appのディレクトリで行われる
COPY
COPYはDockerfileがあるディレクトリのファイルを指定した場所にコピーするコマンドです。
COPY “コピーするファイル名” “コピー先のディレクトリ”
下記のDockerfileの例を見てみましょう。
dockerfileの場所にはgo.mod, go.sumのファイルがある。 WORKDIRで/appで指定しています。go.modとgo.sumのファイルをコンテナイメージ内のカレントディレクトリ(/app)にコピーします。
WORKDIR /app
COPY go.mod .
COPY go.sum .
RUN
RUNはdockerイメージを作成する段階で実行される処理を書きます。例えば 必要なパッケージのダウンロードやアップデートの処理を書きます。
RUN go mod download
RUN go build -o /docker-gs-ping
あとで登場するCMDも似ていますが、CMDはコンテナ起動時に実行される処理を書きます。
EXPOSE
EXPOSEで指定したポートをListenできる。
EXPOSE 8080
CMD & ENTRYPINT
イメージからコンテナ生成・起動時にdocker runでコマンドを実行できます。docker run image-name command argument
コンテナ起動時にコマンドやバイナリを複数実行したい場合に複数行書く方法
1.コマンドをまとめたシェルスクリプトを実行
2. &&でコマンドをまとめる
3.ヒアドキュメントで書く
$ docker run container-image /bin/bash << EOF
cd /var/log
pwd
ls
cat auth.log
EOF
CMDとENTRYPOINTは共にコンテナ起動時に実行される処理を書きます。
例えば、サービスの自動起動設定やアプリケーションの実行などを書きます。
Dockerfile内にはCMDまたはENTRYPOINTが1つは必要ですが、それぞれ1つだけしか書けず、複数書いた場合は最後の行だけが評価されます。
ENTRYPOINTの内容はdocker run 実行時に–entrypoint オプションで指定すれば上書きできます。
ENTRYPOINTはコンテナをバイナリ実行のみに利用したい場合に使うべきで、
CMDはENTRYPOINTで定義したコマンドのデフォルト引数を設定したい場合に用います。
CMDの設定コマンドは起動時コマンド実行のデフォルト設定→別コマンドの実行も想定(docker runで指定したコマンドが優先)
ENTRYPOINTは–entrypoint指定なければ書きかわらない→起動時実行必須のものを設定
dockerfileでCMD,ENTRYPOINTを指定しなかった場合は、ベースイメージの設定を引き継ぎます。
CMDの書式と活用
CMDコマンドの書式にはシェル形式とexec形式の2種類あります。
シェル形式を使うとPID=1がシェル(/bin/sh -c “command here”)になり、コンテナ停止のSIGTERM送信してもPID=1のシェルが受け取り、コンテナが終了しないことがあるため、exec形式が推奨されています。
##---シェル形式の記法
CMD echo "test cmd" # /bin/sh -c 'echo test cmd' が動く
##---exec形式の記法
CMD ["echo", "test cmd"]
$HOMEなどの環境変数を展開してechoしたい場合はシェル形式で実行します。exec形式では変数展開されません。
exec形式ではCMDがENTRYPOINTの引数になる場合がある
exec形式でENTRYPOINTが存在する状態でexec形式のCMDを設定するとCMDの内容がENTRYPOINTの引数になります。
ENTRYPOINT ["ls", "-a"]
CMD ["-l", "/var/log"]
## CMDの-lはコマンドではなく、ENTRYPOINTの引数になる ls -a -l /var/log
## さらにdocker runの引数も追加できるが、CMD変数は上書きされる
$ docker run image_name /etc/ ## → ls -a /etc/
CMDには用途や目的によって変動する引数(/var/log以外も見たければ変更する)を指定することで使い勝手が良くなります。
ENTRYPOINTがシェル形式の場合、CMDは無視されます。
ENTRYPOINTの書式
CMDと同じくシェル形式とexec形式がありますが、exec形式が推奨です。
イメージの作成
dockerfileができたらこのファイルを元にイメージを作成します。
イメージ作成はdocekr buildコマンドを使います。
$ docker build -t goimage ./
-tでイメージのタグを指定します。
./ はDockerfileのディレクトリを指定します。
イメージの作成を確認
$ docker images | grep goimage
goimage latest da68c087c427 17 minutes ago 534MB
ビルド時のキャッシュ
dockerfileの項目を実行する際にdockerはキャッシュをして、再度ビルド時に同じ内容であればキャッシュを利用するのでビルド時間が短縮されます。
dockerfileの命令内容が変わらないとキャッシュが使われてしまうため、これを防ぐためには、–no-cacheオプションを利用します。
イメージからコンテナを起動
イメージからコンテナを作成・起動するにはdocker runを使用します。
$ docker run --publish 8080:8080 goimage
114019d4048ab8f853bac612949db944fe73f90b02a706e80f3b72c434f0e0b3
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
114019d4048a goimage "/docker-gs-ping" 3 seconds ago Up 2 seconds 8080/tcp relaxed_ritchie
http://127.0.0.1:8080にアクセスすると文字が表示されます。
軌道だけなdocker run goimageだけで良いですが、コンテナに外部からアクセスするには–pubishする必要があります。
(補足) golangの公式イメージを利用してコンテナを作成する
go langのDocker Official Imageが提供されているのでこれを使ってコンテナを作成します。
golang – Official Image | Docker Hub
コンテナの挙動を確認するため、golangで簡単なWebサーバを作成します。
コンテナ作成の流れ
- dockerfileを作成
- コンテナを作成
参考にするチュートリアルは下記
What will you learn in this module?
webサーバ作成の手順は下記を参考
リポジトリ
https://github.com/olliefr/docker-gs-ping
適当なディレクトリを作成・移動してリポジトリをクローンする
$ mkdir develop/golang && cd develop/golang && pwd
$ git clone <https://github.com/olliefr/docker-gs-ping>
$ cd docker-gs-ping && ls
Dockerfile LICENSE go.mod main.go
Dockerfile.multistage README.md go.sum main_test.go
まずはmain.goをローカル環境で直接実行する
・main.goの内容
package main
import (
"net/http"
"os"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.GET("/", func(c echo.Context) error {
return c.HTML(http.StatusOK, "Hello, Docker! <3")
})
e.GET("/ping", func(c echo.Context) error {
return c.JSON(http.StatusOK, struct{ Status string }{Status: "OK"})
})
httpPort := os.Getenv("HTTP_PORT")
if httpPort == "" {
httpPort = "8080"
}
e.Logger.Fatal(e.Start(":" + httpPort))
}
このmain.goをコンテナを使用せずに実行する(goの実行環境が必要)
$ go run main.go
## httpPort = 8080と設定しているので、ローカルホストの8080にアクセスすると"Hello, Docker! <3"”と表示される
## URL* 127.0.0.1:8080にアクセスするかcurlして確かめる
$ curl -iL 127.0.0.1:8080
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Date: Sat, 07 Jan 2023 13:14:09 GMT
Content-Length: 17
Hello, Docker! <3
このmain.goをコンテナ内で実行する環境を作成するためにdockerfileで作成する(内容は前内容と同じ)
公式チュートリアルの続き