🐳

これを読めばDockerを使ったアプリ開発の流れがわかるかもしれない

2022/03/08に公開

検証環境
ubuntu(20.04.3 LTS)、docker(20.10.12)、docker-compose(1.26.0)

jsgoMariaDBでSPAなWebサービスをdockerで作る。アプリの仕様はMariaDBからgoで作ったAPIがユーザーの一覧を取得してjsonを返す。jsで作ったフロントエンドがAPIから取得したユーザー一覧を画面に表示する

はじめに

・使用するドメインはexample.comとする
weburlhttp://example.com/web
apiurlhttp://example.com/api
dockerdocker-composeはインストール済みとする
※インストール手順はページ下の参考リンクを参照

ネットワークとボリュームの作成

ネットワーク作成

sample-nwというネットワークを作成して、ここにアプリを作成していく。デフォルトでもbridgeという名前のネットワークがあるのでこれを使うことも可

$ docker network create sample-nw

$ docker network ls
NETWORK ID     NAME        DRIVER    SCOPE
c69b97aecb26   bridge      bridge    local
・・・
7a1e5b08da56   sample-nw   bridge    local

bridgesample-nwのネットワークが異なることがわかる

$ docker network inspect sample-nw
・・・
"Subnet": "172.28.0.0/16",
"Gateway": "172.28.0.1"

$ docker network inspect bridge
・・・
"Subnet": "172.17.0.0/16",
"Gateway": "172.17.0.1"

ボリューム作成

MariaDBのデータを保存するためのvolumeを作成。sample-db-volというvolumを作成して、ここにMariaDBのデータを作成していく。コンテナを作り直すとデータが消えるのでコンテナの外にデータを保存するイメージ

$ docker volume create sample-db-vol

$ docker volume ls
DRIVER VOLUME NAME
local  sample-db-vol

volumeのパスを確認

$ docker volume inspect sample-db-vol
・・・
"Mountpoint": "/var/lib/docker/volumes/sample-db-vol/_data",
"Name": "sample-db-vol",

この場合、/var/lib/docker/volumes/sample-db-vol/_data以下にMariaDBのデータを保存していくことになる

データベース作成

作成したネットワークとボリュームを指定してアプリ用のMariaDBを作成。--mount type=volumeオプションでボリューム、--networkオプションでネットワークを指定

$ docker run -d \
    --name db \
    --env MARIADB_ROOT_PASSWORD=password \
    --mount type=volume,src=sample-db-vol,dst=/var/lib/mysql \
    --network sample-nw \
    mariadb:10.7.1 

$ docker container ls
CONTAINER ID IMAGE           ・・・  NAMES
8f685e421f12 mariadb:10.7.1         sample-db

作成したMariaDBにログインしてデータベースを作成する

# MariaDBを作成したコンテナに入る
$ docker exec -it db bash

# MariaDBにログインしてデータベースを作成
$ mariadb -uroot -ppassword
> create database sample;

アプリのデプロイ

開発したアプリをデプロイしていく

まずは開発したアプリをgit clone

$ git clone https://github.com/nrikiji/docker-spa-example

サンプルプロジェクト(apiwebmigrateディレクトリで分けた)
https://github.com/nrikiji/docker-spa-example

データベースのマイグレーション

データベースマイグレーションツールの sql-migrate を使用。usersテーブルを作成する

Dockerfileを準備

migrate/Dockerfile
FROM golang:1.17.7-alpine3.15 AS build

ARG CGO_ENABLED=0

WORKDIR /go/src/app
RUN go get github.com/rubenv/sql-migrate/...

COPY ./migrate .
CMD ["sql-migrate", "up", "-env", "production"]

イメージをビルドしてマイグレーション実行

$ docker build \
	-f migrate/Dockerfile \
	-t sample/migrate \
	migrate

$ docker run \
	--rm -it \
	--network sample-nw \
	sample/migrate

確認とデータ登録

$ docker exec -it db bash
$ mysql -uroot -ppassword

> use samples;

> desc users;
MariaDB [sample]> desc users;
+-------+-------------+------+-----+---------+-------+
| Field | Type        | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id    | int(11)     | YES  |     | NULL    |       |
| name  | varchar(64) | YES  |     | NULL    |       |
+-------+-------------+------+-----+---------+-------+

> insert into users(name) values ("user1"), ("user2"), ("user3");

API

goecho フレームワークを使用

Dockerfileを準備

api/Dockerfile
FROM golang:1.17.7-alpine3.15 AS build

WORKDIR /go/src/app

ADD ./go.mod .
ADD ./go.sum .
RUN go mod download

COPY . .
RUN go build -o server .

FROM alpine:3.15.0
WORKDIR /go/src/app

COPY .env .
COPY --from=build /go/src/app/server .

EXPOSE 1323
CMD ["./server"]

ビルドして起動

$ docker build \
	-f api/Dockerfile \
	-t sample/api \
	api

$ docker run -t -d \
	--network sample-nw \
	--name api \
	sample/api

sample/apiというイメージを作成して、apiというコンテナ名で起動している。別のコンテナからはコンテナ名のapiで名前解決できる

動作確認

$ docker run --rm \
	--network sample-nw \
	curlimages/curl:7.81.0 -L -v http://api:1323
・・・
[{"id":1,"name":"user1"},{"id":2,"name":"user2"},{"id":3,"name":"user3"}]

Web

jssvelte フレームワークを使用

Dockerfileを準備

web/Dockerfile
FROM node:16.14.0 as build

WORKDIR /srv/app
ADD ./package.json .
ADD ./package-lock.json .
RUN npm install

COPY . .
RUN npm run build

FROM nginx:1.20.2
COPY --from=build /srv/app/public /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]

ビルドして起動

$ docker build \
	-f web/Dockerfile \
	-t sample/web \
	web

$ docker run -t -d \
	--network sample-nw \
	--name web \
	sample/web

sample/webというイメージを作成して、webというコンテナ名で起動している。別のコンテナからはコンテナ名のwebで名前解決できる

動作確認

$ docker run --rm \
	--network sample-nw 
	curlimages/curl:7.81.0 -L -v http://web
・・・
<html lang="en">
・・・
</html>

リバースプロキシ

WebとAPIに外部からアクセスできるようにnginxでリバースプロキシ。ホストの80番ポートへのリクエストをこのコンテナの80番ポートに転送。また、http://example.com/webにアクセスされたらwebコンテナへ、http://example.com/apiにアクセスされたらapiコンテナへ転送する

nginx.confを準備

proxy/nginx.conf
events {
    worker_connections  16;
}

http {
    server {
        listen 80;
        server_name example.com;
        location /web {
            proxy_pass http://web:80/;
            proxy_redirect off;
        }
        location /api {
            proxy_pass http://api:1323/;
            proxy_redirect off;
        }
    }
}

起動

$ docker run -t -d \
	--network sample-nw \
	-p 80:80 \
	--name proxy \
	--mount type=bind,src=/home/ubuntu/docker-example/proxy/nginx.conf,dst=/etc/nginx/nginx.conf \
	nginx

--mount type=bindでホストとコンテナの共有ファイルを指定している

ここまでできたらブラウザで確認可

docker-composeで設定をファイル化

1つずつdocker builddocker runしてたのをdocker-composeで定義化。docker runでオプション指定していた部分がservicesに記述できる。また、volumesnetworksで作成済みのボリュームとネットワークを使えるようになる

docker-compose.ymlの準備

docker-compose.yml
version: '3.8'

services:
  db:
    ・・・
  api:
    ・・・
  web:
    ・・・
  proxy:
    ・・・

volumes:
  sample-db-vol:
    external: true

networks:
  sample-nw:
    external: true

servicesでは各コンテナを起動するときに指定したdocker runコマンドのオプションに対応する

データベース

docker-compose.yml
services:
  db:
    image: mariadb:10.7.1
    # --name db
    container_name: db
    # --env MARIADB_ROOT_PASSWORD=password
    environment:
      MARIADB_ROOT_PASSWORD: password
    # --mount type=volume,src=sample-db-vol,dst=/var/lib/mysql
    volumes:
      - sample-db-vol:/var/lib/mysql
    # --network sample-nw
    networks:
      - sample-nw

API

docker-compose.yml
services:
  ・・・
  api:
    build:
      context: api
    # --name sample-api
    container_name: api
    # --network sample-nw
    networks:
      - sample-nw
    # dbが起動したら起動する
    depends_on:
      - db

Web

docker-compose.yml
services:
  ・・・
  web:
    build:
      context: web
    # --name web
    container_name: web
    # --network sample-nw
    networks:
      - sample-nw

Proxy

docker-compose.yml
services:
  ・・・
  proxy:
    image: nginx
    # --name proxy
    container_name: proxy
    # --mount type=bind,src=/home/ubuntu/docker-example/proxy/nginx.conf,dst=/etc/nginx/nginx.conf
    volumes:
      - ./proxy/nginx.conf:/etc/nginx/nginx.conf
    # --network sample-nw
    networks:
      - sample-nw
    # -p 80:80
    ports:
      - 80:80
    # webとapiが起動したら起動
    depends_on:
      - web
      - api

起動

# 起動
$ docker-compose -f docker-compose.yml up -d

# コンテナが起動していることを確認
$ docker container ls
CONTAINER ID   IMAGE                    ・・・ STATUS          PORTS                               NAMES
113f87517c5e   nginx                    ・・・ Up 49 seconds   0.0.0.0:80->80/tcp, :::80->80/tcp   proxy
8ab2afe73e16   docker-spa-example_api   ・・・ Up 49 seconds   1323/tcp                            api
50916fcb685b   mariadb:10.7.1           ・・・ Up 49 seconds   3306/tcp                            db
c48757f47d8c   docker-spa-example_web   ・・・ Up 49 seconds   80/tcp                              web

停止

$ docker-compose -f docker-compose.yml down

アプリのコンテナを更新

$ docker-compose -f docker-compose.yml up -d --build api

-f docker-compose.ymlの指定はdocker-compose.ymlと同じディレクトリで実行すれば不要

docker-swarmで冗長化

ここまでで1ホストにproxywebapidbが動いている。もう1ホスト追加してwebapiを冗長化する。コンテナを管理するのがマネージャー、コンテナが動作するのがワーカー。

ここでは、ホスト1をマネージャー+ワーカーとして、proxywebapidbを動作させる。ホスト2をワーカーとして、webapiを動作させる。

swarmの有効化

マネージャーマシンとして追加

ホスト1
$ docker swarm init --advertise-addr ホスト1のプライベートIP
  ・・・
  docker swarm join --token SWMTKN-1-3d1x0jf9w4p3tdm3dzgaica1okthygvg7pbnlswaahynfxtfs9-b32wqbhag2fzs2kzky5tl3bpn ホスト1のプライベートIP:2377

docker swarm initでマネージャーマシンを追加するとワーカーを追加するためのdocker swarm joinコマンドが表示されるのでホスト2で実行する

ワーカーマシンとして追加

ホスト2
$ docker swarm join --token SWMTKN-1-3d1x0jf9w4p3tdm3dzgaica1okthygvg7pbnlswaahynfxtfs9-b32wqbhag2fzs2kzky5tl3bpn ホスト1のプライベートIP:2377

docker node lsswarmに参加しているホストが表示され、MANAGER STATUSLeaderのホストがマネージャーであることを確認できる

ホスト1
$ docker node ls
ID                            HOSTNAME          STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
p7o6qv9k7ltbb88gzwlctb7ib     ip-172-31-4-219   Ready     Active                          20.10.12
s03k08890tzjb93kjqo438tuc *   ip-172-31-7-251   Ready     Active         Leader           20.10.12

ホスト2でもgit cloneしてwebapiのイメージをビルドしておく

ホスト2
$ git clone https://github.com/nrikiji/docker-spa-example
$ docker build -f web/Dockerfile -t sample/web ./web
$ docker build -f api/Dockerfile -t sample/api ./api

データベースとリバースプロキシが起動するホストを固定する

docker-compose.ymlをコピーしてdocker-stack.ymlを作る。以下、docker-compose.ymlとの差分を追記

データベースは作成済みのボリュームsample-db-volがあるホスト1で、リバースプロキシはドメインが割り当てられているホスト1のみで起動するようにする

データベースとリバースプロキシ

docker-stack.yml
services:
  db:
    ・・・
    deploy:
      placement:
        constraints: [node.role == manager]
  ・・・
  proxy:
    ・・・
    deploy:
      placement:
        constraints: [node.role == manager]

また、docker-compose.ymlで指定していたcontainer_nameswarmでは指定できない。別のコンテナからはdbproxy(サービス名)で名前解決できる

WebとAPIが起動するコンテナ数を決める

replicasで起動する数を指定。どのホストで起動するかはdockerが適当に決めてくれる

WebとAPI

docker-stack.yml
services:
  web:
    ・・・
    deploy:
      replicas: 2
  ・・・
  api:
    ・・・
    deploy:
      replicas: 2

起動

アプリのイメージをビルドする

ホスト1
$ docker build -t sample/api -f api/Dockerfile api
$ docker build -t sample/web -f web/Dockerfile web

起動

ホスト1
$ docker stack deploy --compose-file docker-stack.yml sample-app

$ docker service ls
ID             NAME               MODE         REPLICAS   IMAGE               PORTS
fjmp0qiro886   sample-app_api     replicated   2/2        sample/api:latest   
w3pk118zri3w   sample-app_db      replicated   1/1        mariadb:10.7.1      
c98ndsva3duy   sample-app_proxy   replicated   1/1        nginx:latest        *:80->80/tcp
kdpcm5at6v6v   sample-app_web     replicated   2/2        sample/web:latest   

apiwebがそれぞれ2つ、dbproxyがそれぞれ1つずつ起動されていることが確認できる

コンテナが起動しているホストを確認

ホスト1
$ docker stack ps sample-app
ID             NAME                     IMAGE               NODE              DESIRED STATE   CURRENT STATE            ERROR                       PORTS
y1rnfqr1uiyb   sample-app_api.1         sample/api:latest   ip-172-31-4-219   Running         Running 16 minutes ago                               
he664lhewzic   sample-app_api.2         sample/api:latest   ip-172-31-7-251   Running         Running 16 minutes ago                               
wqlj7ormtnos   sample-app_db.1          mariadb:10.7.1      ip-172-31-7-251   Running         Running 16 minutes ago                               
ux6olwoluhv8   sample-app_proxy.1       nginx:latest        ip-172-31-7-251   Running         Running 16 minutes ago                               
s49my5cso1d0   sample-app_web.1         sample/web:latest   ip-172-31-4-219   Running         Running 16 minutes ago                               
8hv6dlsqrojp   sample-app_web.2         sample/web:latest   ip-172-31-7-251   Running         Running 16 minutes ago                               

アプリの更新

ホスト1
# イメージのビルド
$ docker build -t sample/api -f api/Dockerfile api

# サービスを更新
$ docker service update --image sample/api sample-app_api

その他

コンテナのログ

https://qiita.com/Esfahan/items/5e5a9ae7882bb0eaaf5f
今回のアプリではコンテナを削除するとコンテナ内で発生したログも消えてしまう。この辺りの記事が参考になりそう

dockerのログ

$ docker container logs コンテナ名
$ docker service logs サービス名

環境ごとに切り替える

やり用はいくつかあるが、docker-compose.ymlargsの引数で変数を渡せるのでアプリ側で工夫する

dokcer-compose.yml
api:
  build:
    context: api
    args:
      - ENV=prod
api/Dockerfile
・・・
ARG ENV=prod
COPY ./.env.${ENV} ./.env
・・・

docker swarmで使用するポート

TCP ポート 2377 はクラスタ管理通信用
TCP・UDP ポート 7946 はノード間の通信
TCP・UDP ポート 4789 はオーバレイ・ネットワークの通信

ドキュメントの引用。ファイアウォールなどでポートを制限している場合は許可する必要がある

コンテナの再起動ポリシー

ドキュメントを見て用途に合わせて設定する。restart=alwaysを設定すれば、コンテナが停止したら再起動を試みる

よく使うコマンドなど

コンテナに入る

$ docker exec -it コンテナ名 sh

curlで動作確認

$ docker run --rm --network ネットワーク名 curlimages/curl:7.81.0 -L -v URL

起動中のコンテナをリアルタイムで確認

$ watch docker ps

未使用なイメージやコンテナを削除

$ docker image prune
$ docker container prune

参考

https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-20-04-ja

https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-compose-on-ubuntu-20-04-ja

Discussion