これを読めばDockerを使ったアプリ開発の流れがわかるかもしれない
検証環境
ubuntu(20.04.3 LTS)、docker(20.10.12)、docker-compose(1.26.0)
js
、go
、MariaDB
でSPAなWebサービスをdocker
で作る。アプリの仕様はMariaDB
からgo
で作ったAPI
がユーザーの一覧を取得してjson
を返す。js
で作ったフロントエンドがAPI
から取得したユーザー一覧を画面に表示する
はじめに
・使用するドメインはexample.com
とする
・web
のurl
はhttp://example.com/web
・api
のurl
はhttp://example.com/api
・docker
とdocker-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
bridge
とsample-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
サンプルプロジェクト(api
、web
、migrate
ディレクトリで分けた)
データベースのマイグレーション
データベースマイグレーションツールの sql-migrate を使用。users
テーブルを作成する
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
go
の echo フレームワークを使用
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 /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
js
の svelte フレームワークを使用
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 /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を準備
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 build
とdocker run
してたのをdocker-compose
で定義化。docker run
でオプション指定していた部分がservices
に記述できる。また、volumes
とnetworks
で作成済みのボリュームとネットワークを使えるようになる
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
コマンドのオプションに対応する
データベース
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
services:
・・・
api:
build:
context: api
# --name sample-api
container_name: api
# --network sample-nw
networks:
- sample-nw
# dbが起動したら起動する
depends_on:
- db
Web
services:
・・・
web:
build:
context: web
# --name web
container_name: web
# --network sample-nw
networks:
- sample-nw
Proxy
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ホストにproxy
、web
、api
、db
が動いている。もう1ホスト追加してweb
、api
を冗長化する。コンテナを管理するのがマネージャー、コンテナが動作するのがワーカー。
ここでは、ホスト1をマネージャー+ワーカーとして、proxy
、web
、api
、db
を動作させる。ホスト2をワーカーとして、web
、api
を動作させる。
swarmの有効化
マネージャーマシンとして追加
$ 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で実行する
ワーカーマシンとして追加
$ docker swarm join --token SWMTKN-1-3d1x0jf9w4p3tdm3dzgaica1okthygvg7pbnlswaahynfxtfs9-b32wqbhag2fzs2kzky5tl3bpn ホスト1のプライベートIP:2377
docker node ls
でswarm
に参加しているホストが表示され、MANAGER STATUS
がLeader
のホストがマネージャーであることを確認できる
$ 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
してweb
とapi
のイメージをビルドしておく
$ 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のみで起動するようにする
データベースとリバースプロキシ
services:
db:
・・・
deploy:
placement:
constraints: [node.role == manager]
・・・
proxy:
・・・
deploy:
placement:
constraints: [node.role == manager]
また、docker-compose.yml
で指定していたcontainer_name
はswarm
では指定できない。別のコンテナからはdb
やproxy
(サービス名)で名前解決できる
WebとAPIが起動するコンテナ数を決める
replicas
で起動する数を指定。どのホストで起動するかはdocker
が適当に決めてくれる
WebとAPI
services:
web:
・・・
deploy:
replicas: 2
・・・
api:
・・・
deploy:
replicas: 2
起動
アプリのイメージをビルドする
$ docker build -t sample/api -f api/Dockerfile api
$ docker build -t sample/web -f web/Dockerfile web
起動
$ 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
api
とweb
がそれぞれ2つ、db
とproxy
がそれぞれ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
アプリの更新
# イメージのビルド
$ docker build -t sample/api -f api/Dockerfile api
# サービスを更新
$ docker service update --image sample/api sample-app_api
その他
コンテナのログ
今回のアプリではコンテナを削除するとコンテナ内で発生したログも消えてしまう。この辺りの記事が参考になりそう
dockerのログ
$ docker container logs コンテナ名
$ docker service logs サービス名
環境ごとに切り替える
やり用はいくつかあるが、docker-compose.yml
のargs
の引数で変数を渡せるのでアプリ側で工夫する
api:
build:
context: api
args:
- ENV=prod
・・・
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
参考
Discussion