Docker超入門~通常レベルまで Part3

1. 本記事について

本記事では、Dockerfileの書き方、マルチビルドについて、Dockerコンテナのネットワークについて、説明します。 

2. Dockerfile

Dockerfileとは、自分好みにカスタムしたImageを作成するための設計書です。

dockerhubに公開されているイメージを持ってきて、コンテナを起動という流れになると思います。

ただ、開発をしていくと、既存で用意されているイメージでは自分の思い描くコンテナが作れないことが多々あります。そこで、Dockerfileを用いて、オリジナルのImageを作成するのです。

オリジナルと言っても、根本的なシステムやサービスは、dockerhubから持ってきます。

では、いくつか、Dockerfileを作ってみます。

ubuntu

From ubuntu:20.04
RUN apt update && apt upgrade

Imageを作成

docker build -t myubuntu:1 .

コンテナを起動して、コンテナ内へ

docker run -it myubuntu:1 bash

コンテナ内

root@0asdfasdv:/# ls
bin  boot  dev  etc  home  lib  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

無事にubuntuのコンテナが作成されていることがわかります。

COPYコマンド

Dockerfileがあるディレクトリで、hello.txtというファイルを作ってください。

hello.txtの中身は、helloとしてください。以下のコマンドでも作成できます。

echo "hello" >> hello.txt

COPYコマンドを使うために、Dockerfileを以下のように書き換えてみてください。

From ubuntu:20.04
RUN apt update && apt upgrade
# 追加
COPY ./hello.txt ./hello/hello.txt.

COPY コピーしたいファイルやフォルダ(コピー元) dockerコンテナ内のディレクトリ(コピー先)

では、先ほどと同じコマンドを使って、Imageを作成して、コンテナを起動

root@0asdfasdv:/# ls
bin  boot  dev  etc  hello  home  lib  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

コンテナ内にhelloというフォルダが追加されていることがわかります。

COPYコマンドは、コピーすることはもちろんですが、フォルダの作成もやってくれます。

root@0asdfasdv:/# cd hello
root@0asdfasdv:/hello# ls
hello.txt
root@0asdfasdv:/hello# cat hello.txt
hello

hello/hello.txtがちゃんとコピーされていることがわかり、さらに、hello.txtの中身がhelloとなっていることも確認できます。

WORKDIR

このコマンドは簡単です。dockerコンテナを起動した時、作業ディレクトリをどこにするのかということです。例えば、以下のように記述すると

From ubuntu:20.04
RUN apt update && apt upgrade
COPY ./hello.txt ./hello/hello.txt
# 追加
WORKDIR /hello

コンテナ起動して、コンテナの中に入ると、最初から、/helloディレクトリにいます。

% docker run -it myubuntu:1 bash                                                                                                                                                     ✔  00:47:40 
root@567bca7c2537:/hello# ls
hello.txt

 このような形で、Imageをカスタマイズすることが可能です。

3. マルチステージビルド

マルチステージビルドとは

公式ほうに以下のようにして記述されていました。

マルチステージビルドとは、Dockerfile の読みやすさやメンテナンスのしやすさを維持するよう、最適化に悩まされている全ての人に役立ちます。

https://docs.docker.jp/develop/develop-images/multistage-build.html

マルチステージビルドのメリット

イメージをビルドする際に取り組むことといえば、ほとんどがそのイメージサイズを小さく抑えることです。

https://matsuand.github.io/docs.docker.jp.onthefly/develop/develop-images/multistage-build/

使い方

具体的に、どのような場面で使うのか。

c言語で作成した、Hello.cというファイルをLinuxの画面で表示したい時について以下の画像を見てください。必要な処理として、

  1. Hello.cをコンパイラによりHello.outへコンパイルする
  2. Hello.outをLinuxで読み取り出力する。

これをDockerで表現すると、コンパイラ用のフェーズと、Linux用のフェーズがあります。

これをDockerfile、DockerImage、Dockerコンテナで表現すると、常にコンパイラとLinuxは存在していることになりますよね。この一連の処理に二つ必要なので。

つまり、容量も二つ分必要ということになります。

でも、よく考えてください。最後のLinuxでの処理は、Hello.outとLinuxさえあれば、良いことを。

  • 構築ステップを並列に実行できるため、構築パイプラインをより速くより効率的にできる。
  • プログラムを実行するために必要なものだけを含む、形跡が最小の最終イメージを作成できる。
https://docs.docker.jp/build/guide/multi-stage.html

二つ目の文章で、最小の最終イメージを作成できる。というのは、画像に当てはめて、説明するとコンパイラも作成されるが、最後の残るのは、Linuxのイメージである。ということです。つまり、最終的なイメージサイズが小さくなることを意味しています。

試してみよう

まずは、gccのみで構成してみましょう

.
├── Dockerfile
└── Hello.c
FROM gcc:12.2.0
WORKDIR /app
COPY ./Hello.c ./Hello.c
RUN gcc -o Hello.out Hello.c
CMD ["./Hello.out"]
#include <stdio.h>

void main() {
    printf("Hello World!");
}

Dockerfileの説明をします。

FROM gcc:12.2.0は、DockerhubにあるDockerイメージのgccかつバージョンが12.2.0を指定しています。これが、コンパイラです。

WORKDIR /appは、コンテナ内の作業ディレクトリを/appに指定しています。以後の操作は、すべて、/appで始まります。

COPY ./Hello.c ./Hello.cは、自分のローカルPC上から、Dockerコンテナ上へ指定したファイルをコピーしています。細かく説明すると、最初の./Hello.cを自分のPCのカレントディレクトリ、二つ目の./Hello.cが、Dockerコンテナ上のカレントディレクトリとなります。ここでは、意味的には、/app/Hello.cとなります。

RUN gcc -o Hello.out Hello.c
CMD [“./Hello.out”]

これは、コンパイラの実行コマンドと、出力されたファイルを、実行するコマンドです。コンパイルは、gcc Hello.cでできますが、出力のファイルが、a.outとなってしまうので、今回は、-oオプションをつけて出力ファイル名を指定しました。CMDは、実行コマンドです。

では、Dockerfileからイメージを作成して、イメージをもとにコンテナを起動してみましょう。

% docker build -t single-stage .
または
% docker image build -t single-stage .
% docker run single-stage 
または
% docker container run single-stage                                                                                                                                   ✔  09:49:53 
Hello World!%

Hello World!が出力されました。

では、マルチステージをやってみましょう。

先ほど同様にディレクトリ構成を以下のようにしてください。

.
├── Dockerfile
└── Hello.c

Dockerfileは、以下のようにしてください。

FROM gcc:12.2.0
WORKDIR /app
COPY ./Hello.c ./Hello.c
RUN gcc -o Hello.out Hello.c


FROM ubuntu:20.04
WORKDIR /app
COPY --from=0 ./app/Hello.out ./Hello.out
CMD ["./Hello.out"]

基本的には、先ほどとなにも変わりませんが、違う点は、FROMが二つある点です。

これが、マルチステージビルドです。初めに実行されるのが、gccの部分です。次に実行されるのが、ubuntuの部分です。

先ほどとさほど内容は変わりませんが、一点説明すると、COPY –from=0 ./app/Hello.out ./Hello.outこちらのコードは初めて見ると思います。先ほど、COPY ローカルディレクトリ コンテナディレクトリのように説明しましたが、–from=0とは何のことでしょうか。

これは、一つ目のFROMを指しています。その後の ./app/Hello.out ./Hello.outは、コピー元からコピー先という流れは変わりません。この–fromをつけることで、コピー元の指定を変えているのです。コピー元をローカルPCではなく、gccコンテナ内から取得を指定します。

% docker build -t multi-stage .
または
% docker image build -t multi-stage .


% docker run multi-stage 
または
% docker container run multi-stage    

これを実行すると同様にHello World!%が出力されるはずです。

ちょっとコードが増えたけど、つまりなにが言いたいの?です。。

% docker image ls                                                                                                                                                ✔  10:02:31 
REPOSITORY     TAG       IMAGE ID       CREATED          SIZE
multi-stage    latest    14f92b08c99c   7 minutes ago    65.7MB
single-stage   latest    9ccd0fceb6c6   26 minutes ago   1.18GB

以上のコマンドを実行してみてください。single-stageとmulti-stageで、SIZEが違うことが確認できると思います。single-stageは、マルチステージビルドをしていない場合のimageサイズです。このように、マルチステージビルドにするとイメージサイズが軽くなるのです。

コマンドの補足説明

CMDとRUNコマンドについて

大きな違いとしては、実行タイミングと実行回数です。

RUN

実行タイミング: Dockerビルド時

実行回数:何回でも

CMD

実行タイミング: コンテナ実行時のデフォルトを提供します 

実行回数: CMD 命令を一度だけ指定できます。複数の CMD がある場合、最も後ろの CMD のみ有効です。

参考:https://docs.docker.jp/engine/reference/builder.html

おまけ

おまけといっても、詳しくは書きませんが、マルチステージビルドの並列処理とイメージの軽量化を用いて、環境を分けることができます。

例えば、本番環境と開発環境、クライアントサイドとサーバーサイドなど。

参考:https://docs.docker.jp/build/guide/multi-stage.html

4. Dockerコンテナのポートとネットワーク

PCからサーバーのアプリケーションに対してアクセスするとき、ランダムで接続されると困るので、接続の窓口を設定する必要があります。それがポートで、80番の窓口にきた方はApp1へ案内し、8080番窓口に来た方は、App2へ案内するといったようにポートというものがあります。

ホストOSとコンテナのポートを繋げる

コンテナのポートの前に一つ余談として、以下のようなことをしたことはありませんか?

自分で開発したものを起動して、localhostでブラウザから接続し、開発を進めていく。

これも実は、ポートが指定されていて、開発したものが画面上で見えている分けなんですね。

でも、この画像はまだ不完全。

ホストマシンと4000番ポートで接続しているけど、ホストマシンの中に入ったら、実は接続先がわからないんです。4000番に接続したからといってreactへの道は用意されていません。ここでもポートが必要なんです。

では、ここからコンテナを交えて説明していきます。

画像のように、コンテナをブラウザやコンテナ外部から接続する場合には、ポートを設定する必要があります。

実は、part2のDockerfileで、ポートができてきているんです😁

docker container run -p 3030:80 --name phpcontainer01 phpimage

ここで大事なのは、-p 3030:80の部分です。-pは、portオプションで紐付けを意味します。3030:80は、ホストOS側のポートが3030、コンテナ側のポートが80という意味です。

docker container run -p {ホスト側のポート}:{コンテナ側のポート} {イメージ}

Dockerfileを作成しなくても、dockerhubからイメージを指定すれば、コンテナを起動することができるので、色々コマンドを叩いてみましょう

nginx

docker container run -p 3000:80 nginx

nginxのイメージ:https://hub.docker.com/_/nginx

apache

ホストOS側のポートがかぶるのでapacheはホストOSを4000番ポートで指定します。

docker container run -p 4000:80 httpd

公式によるとhttps://hub.docker.com/_/httpd。apacheは、httpdというイメージですね。

試しに、以下のコマンドを実行してみてください。

% docker container ls

実行結果
CONTAINER ID   IMAGE     COMMAND                   CREATED              STATUS              PORTS                  NAMES
10e9f02d414c   httpd     "httpd-foreground"        4 seconds ago        Up 3 seconds        0.0.0.0:4000->80/tcp   strange_gauss
f017d19d0882   nginx     "/docker-entrypoint.…"   About a minute ago   Up About a minute   0.0.0.0:3000->80/tcp   mystifying_edison

このように実行結果が返ってくると思います。

PORTを見るとIPアドレス:ポート->ポートとなっていることがわかると思います。先ほどの図と同じように接続できました。

コンテナネットワークについて

次にコンテナのネットワークについて説明します。

dockerには、ネットワークという概念が存在し、同一ネットワーク内なら簡単に通信することができます。逆に同一ネットワークではない場合、通信することができません。設定を行えばできます。

networkと言われても何のことかわからないと思いますので、一度、コマンドを実行してみましょう。

% docker network ls
NETWORK ID     NAME              DRIVER    SCOPE
02c9bdce3dda   bridge            bridge    local
0f73ade7d877   docker_gwbridge   bridge    local
749cf2e94a82   host              host      local
knlv1ytebgyb   ingress           overlay   swarm
850297abe447   none              null      local

docker networkについての公式ドキュメントhttps://docs.docker.jp/engine/userguide/networking/dockernetworks.html

ちなみに、ネットワークを指定しない場合は、bridgeというネットワークにグルーピングされます。

同じネットワーク上のコンテナ同士は通信が可能です。

ブリッジネットワークについて知りたい方は、公式ドキュメントを読んでみてください。

以下のコマンドを実行するとbridgeネットワークの詳細が表示されます。

docker network inspect bridge

では、bridgeネットワーク間で通信をしてみましょう。

まずは、二つのコンテナを起動します。以下のコマンドを実行してみてください。

docker run -itd --name=container1 busybox
docker run -itd --name=container2 busybox

2つのコンテナが起動したことを確認したら、以下のコマンドを実行してください。

docker network inspect bridge

以下のように、Containersという項目に、container1とcontainer2が追加されていることが確認できます。

"Containers": {
            "6ec9de58b2b2abb20acbf4de226946a6f43f8fb9fea2ce4efe59d021ec9a2060": {
                "Name": "container2",
                "EndpointID": "431eb3cce07b4cb87b3464012d4c9b15a7cae1b7d35a248d047ef7397b39a4ca",
                "MacAddress": "02:42:ac:11:00:03",
                "IPv4Address": "172.17.0.3/16",
                "IPv6Address": ""
            },
            "c8ccfa54bcf9c736b18292e67e497406079e815dedbb18f1f3ac4b7b7e9b607f": {
                "Name": "container1",
                "EndpointID": "99cfe88f80868ebebd6bed11cc51a78bc56b7b84a0ee7726f409b8e863f0529d",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },

Container1へアタッチしてそこからpingコマンドを用いてContainer2へ接続してみると画像のように返ってきます。

docker attach container1

ping 172.17.0.3
または
ping -w3 172.17.0.3

また、以下のコマンドを実行してみてください

しっかりとContainer2への接続ができていることが確認できます

/ # cat /etc/hosts
127.0.0.1	localhost
::1	localhost ip6-localhost ip6-loopback
fe00::0	ip6-localnet
ff00::0	ip6-mcastprefix
ff02::1	ip6-allnodes
ff02::2	ip6-allrouters
172.17.0.2	c8ccfa54bcf9

ネットワーク分野は知識を要するので説明はここまでとします。

興味のある方は、pingコマンドやtcp / ip、ポート、ネットワークとはなど調べてみるのもいいかもしれません。

5. 終わりに

part3はここまでになります。

次回は、いよいよ超入門〜普通編ラストpart4となります。

part4では、docker composeを使って、複数のコンテナを作成する方法について説明します。

コメントを送信

You May Have Missed