Docker環境で、kotlinのWebフレームワークであるKtorを動かす方法をまとめます。
動作環境
GCP の Container-Optimized OS で確認しました。また、docker-composeを使って構築しました。
Docker
version 19.03.6
docker-compose
version 1.24.0
Compose file version 3
Ktor
version 1.3.2
システム構成
internet
以外は、docker-composeのDockerコンテナ名(container_name
)と紐付いています。
ディレクトリ構成は以下になっています。
docker-compose.yml
app/
Dockerfile
wait-for-mysql.sh
src/
... Ktorのgradleプロジェクトのファイル群 ...
db/
Dockerfile
my.cnf
-
app
: Ktorのソースなどを格納- Dockerfile:
ktor
コンテナのビルド用Dockerfile
- Dockerfile:
-
db
: MySQLコンテナ関連のファイルを格納- Dockerfile:
mysql_host
コンテナのビルド用Dockerfile - my.cnf: MySQL設定ファイル
- Dockerfile:
Ktor
それでは各コンテナのDockerfileとdocker-composeのサービス定義をみていきます。
まずktor
のコンテナです。基本的には公式のクイックスタートに沿って構築します。KtorのコンテナをビルドするDockerfileは次のようになっています。
FROM openjdk:8-jre-alpine
# ktor
ENV APPLICATION_USER ktor
ENV KTOR_ENV production
RUN adduser -D -g '' $APPLICATION_USER
RUN mkdir /app
RUN chown -R $APPLICATION_USER /app
USER $APPLICATION_USER
# あらかじめビルドしておいたjarファイルをコンテナにコピー
# ここではktorapp-0.0.1.jarというjarファイルをパッケージング済みである前提です
COPY ./build/libs/ktorapp-0.0.1.jar /app/ktorapp.jar
COPY ./wait-for-mysql.sh /app/wait-for-mysql.sh
WORKDIR /app
# mysqlクライアントをインストール
USER root
RUN apk add mysql-client
RUN chmod 755 /usr/bin/mysql
# ktorユーザーに戻す
USER $APPLICATION_USER
# DBパスワードは設定ファイルのものを使用する
ARG DB_ROOT_PASS
ENV DB_PASS=${DB_ROOT_PASS}
CMD ["sh", "-c", "./wait-for-mysql.sh mysql_host root ${DB_PASS} java -server -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:InitialRAMFraction=2 -XX:MinRAMFraction=2 -XX:MaxRAMFraction=2 -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+UseStringDeduplication -jar ktorapp.jar"]
クイックスタートと異なるのは、MySQLコンテナが起動完了するのを待つスクリプトを追加している点です。
DBユーザーはrootユーザ固定にしてしまっていますが、パスワードは.envファイルから読み込んだものを使用しています。
また、CMD
実行時にDBパスワードの変数を展開するために、こちらの回答を参考にして変更しています。変更しないと、変数名DB_PASS
がそのまま文字列として実行されてしまっていました。
CMD ["java", "-server", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseCGroupMemoryLimitForHeap", "-XX:InitialRAMFraction=2", ...]
↓
CMD ["sh", "-c", "./wait-for-mysql.sh mysql_host root ${DB_PASS} java -server -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:InitialRAMFraction=2 ..."]
MySQLコンテナの起動完了を待つスクリプトは以下です。
Dockerの公式にあったサンプルはPostgresになっていますが、こちらの記事はMySQLになっていましたので、参考にさせていただきました。
#!/bin/sh
# wait-for-mysql.sh
set -e
host="$1"
shift
user="$1"
shift
password="$1"
shift
cmd="$@"
until /usr/bin/mysql -h"$host" -u"$user" -p"$password" &> /dev/null
do
>&2 echo "MySQL is unavailable - sleeping"
sleep 1
done
>&2 echo "MySQL is up - executing command"
exec $cmd
ここまでがktor
コンテナをビルド・起動するためのスクリプトです。次にktor
コンテナを使用しているdocker-composeのサービスの箇所をみていきましょう。
version: "3"
services:
# Ktorアプリケーション
app:
container_name: ktor
expose:
- 8080
environment:
- VIRTUAL_HOST=virtual.host.example
- VIRTUAL_PORT=8080
- LETSENCRYPT_HOST=virtual.host.example
- LETSENCRYPT_EMAIL=mailaddress@mail.example
build:
context: ./app
args:
DB_ROOT_PASS: $DB_ROOT_PASS
depends_on:
- db
networks:
- reverse-proxy
# ... 省略 ...
networks:
reverse-proxy:
external: true
build
で構築するDockerfileがあるapp
ディレクトリを指定し、引数としてDBのパスワードを渡しています。それをDockerfileのなかで解釈し、MySQL起動完了待ちスクリプトに渡していました。
後述するDBコンテナやnginxのリバースプロキシ、Let's Encrypt companionなどもすべて同一のネットワークを使用するので、ここではrevese-proxy
という既存のネットワークを使うようにしています。(ひとつのdocker-compose.yml
に全てのコンテナ定義を書く場合はデフォルトの同一ネットワークが適用されるので不要かもしれませんが...)
DB
mysql_host
のコンテナです。
docker-composeのサービス定義はこちらを参考にしました。ただ、Container Optimized OSだとMySQLコンテナがログ(/var/log/mysql
)への書き出し権限がないため起動に失敗しましたが、その問題はこちらを参考に解決しました。ありがとうございます。
まず、コンテナをビルドするDockerfileです。
FROM mysql:8.0
RUN mkdir /var/log/mysql
RUN chown mysql:mysql /var/log/mysql
イメージをダウンロードして、ログ出力先ディレクトリを作成し権限を設定しています。
次に、docker-composeのサービス定義の箇所です。
services:
# MySQL
db:
build:
context: ./db
container_name: mysql_host
volumes:
- db-store:/var/lib/mysql
- db-logs:/var/log/mysql
- ./db/my.cnf:/etc/mysql/conf.d/my.cnf
environment:
- MYSQL_DATABASE=${DB_NAME}
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASS}
- TZ=${TZ}
ports:
- ${DB_PORT}:3306
networks:
- reverse-proxy
# ... 省略 ...
volumes:
db-store:
db-logs:
今回はDBユーザーはrootユーザーのみ使用していますので、DB情報の環境変数は以下のみセットしています。
-
MYSQL_DATABASE
: DB名 -
MYSQL_ROOT_PASSWORD
: rootユーザーのパスワード
実は最初はDBユーザーの環境変数MYSQL_USER
にroot
で指定してたんですが、そうすると、MySQLコンテナが起動したときにroot
というユーザーを作成しようとして起動に失敗してしまうんですね...。
参考: https://stackoverflow.com/a/45086773
Ktorからrootユーザーではなく別のユーザーでアクセスしたい方は適宜セットしてください。
設定値の読み込みは.env
ファイルを使用しています。
DB_NAME=<db name>
DB_ROOT_PASS=<password>
DB_PORT=3306
TZ=Asia/Tokyo
リバースプロキシ
nginx_proxy
コンテナです。Ktorへリクエストを転送するリバースプロキシになります。このコンテナとLet's Encryptを使ってSSL証明書を発行するコンテナ(letsencrypt_companion
)については、KtorクイックスタートのSSL通信にするためのガイドを参考に構築しました。
jwilder/nginx-proxyという素晴らしいイメージをそのまま使うので、docker-composeのサービス定義のみです。
nginx:
image: jwilder/nginx-proxy
container_name: nginx_proxy
environment:
- NGINX_PROXY_CONTAINER=nginx_proxy
ports:
- "80:80"
- "443:443"
volumes:
- certs:/etc/nginx/certs:ro
- vhost:/etc/nginx/vhost.d
- nginx-html:/usr/share/nginx/html
- /var/run/docker.sock:/tmp/docker.sock:ro
restart: always
labels:
- "com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy=true"
networks:
- reverse-proxy
# ...
volumes:
certs:
vhost:
nginx-html:
-
certs:/etc/nginx/certs:ro
のro
は read only モードとのことですが、このコンテナからは証明書を読み込むだけで書き込みは必要ないのでつけています。 -
NGINX_PROXY_CONTAINER=nginx_proxy
とlabel
は、Let's Encrypt companionコンテナがnginxプロキシコンテナを識別するために使用するもののようです
上記で3つのvolumes
を定義しています。それらはSSL証明書発行のために使用されるので、後述のLet's Encrypt companionコンテナと共有しています。それぞれのボリュームについて解説します。
※ 一部の矢印が点線なのはread onlyにしているのを表現したかったからです...
certs
ボリューム
発行されたSSL証明書が格納されます。nginx_proxy
コンテナからは読み込むだけなのでro
(read only)にします。
vhost
ボリューム
nginxのlocationなど設定ファイルが格納されるボリュームです。Let's EncryptでSSL証明書を発行する際に、Let's Encryptから検証のためのリクエストが送信されるので、そのリクエストを受け付けるlocation設定がこちらのボリュームに格納されるようです。
ためしに、コンテナを作成したあとにボリュームのなかを見てみると...
$ docker volume inspect ktorapp_vhost
[
{
"CreatedAt": "2020-08-10T06:43:59Z",
"Driver": "local",
"Labels": {
"com.docker.compose.project": "ktorapp",
"com.docker.compose.version": "1.24.0",
"com.docker.compose.volume": "vhost"
},
"Mountpoint": "/var/lib/docker/volumes/ktorapp_vhost/_data",
"Name": "ktorapp_vhost",
"Options": null,
"Scope": "local"
}
]
/var/lib/docker/volumes/ktorapp_vhost/_data
というところにデータが格納されているようです。
$ sudo ls -la /var/lib/docker/volumes/ktorapp_vhost/_data
total 12
drwxr-xr-x 2 root root 4096 Aug 10 06:43 .
drwxr-xr-x 3 root root 4096 Aug 8 13:37 ..
-rw-r--r-- 1 root root 278 Aug 10 06:43 default
default
というファイルがありました。みてみると、
$ sudo cat /var/lib/docker/volumes/ktorapp_vhost/_data/default
## Start of configuration add by letsencrypt container
location ^~ /.well-known/acme-challenge/ {
auth_basic off;
auth_request off;
allow all;
root /usr/share/nginx/html;
try_files $uri =404;
break;
}
## End of configuration add by letsencrypt container
URLパスのドメイン/.well-known/acme-challenge/
へのアクセスは/usr/share/nginx/html
配下を見にいくようなlocation設定を追加しているようです。これはコンテナのnginx-html
ボリュームのマウントポイント=/usr/share/nginx/html
と一致しています。
nginx-html
ボリューム
Let's Encryptによる検証のためのファイルが格納されます。こちらも見てみましょう。
$ sudo ls -la /var/lib/docker/volumes/ktorapp_nginx-html/_data/
total 20
drwxr-xr-x 3 root root 4096 Aug 10 06:43 .
drwxr-xr-x 3 root root 4096 Aug 8 13:37 ..
drwxr-xr-x 3 root root 4096 Aug 8 14:01 .well-known
-rw-r--r-- 1 root root 494 Nov 19 2019 50x.html
-rw-r--r-- 1 root root 612 Nov 19 2019 index.html
.well-known
がありました。さらにこの下に acme-challenge
ディレクトリがあり、そこに検証用ファイルが生成されるみたいです。
Let’s Encrypt companion
letsencrypt_companion
コンテナです。こちらもjrcs/letsencrypt-nginx-proxy-companionという素晴らしいDockerイメージをそのまま使うだけです。Let's Encryptを使ってSSL証明書を自動で発行してくれます。さきほど紹介したcerts
vhost
nginx-html
ボリュームを使用して検証のためのlocation設定やファイル作成を行い、SSL証明書を発行します。
letsencrypt:
image: jrcs/letsencrypt-nginx-proxy-companion
container_name: letsencrypt_companion
depends_on:
- nginx
volumes:
- certs:/etc/nginx/certs:rw
- vhost:/etc/nginx/vhost.d
- nginx-html:/usr/share/nginx/html
- /var/run/docker.sock:/var/run/docker.sock:ro
restart: always
networks:
- reverse-proxy
さきほどのリバースプロキシのコンテナとは異なり、certs
へのアクセス権限が書き込み可(rw
)になっていることに注意してください。
コンテナ起動
実際にコンテナの生成/起動を行ってみます。
前提
Container Optimized OS では、docker-composeコマンドはエイリアス設定を行わないとできないみたいですので、チュートリアルを参考に設定しました。
Running Docker Compose with Docker
手順
まず、とても微妙なのですが...ローカル環境でKtorアプリケーションをビルド・パッケージングします...。ここもサーバー側でやりたいのでいずれ更新します・・・。
> ./gradlew build
パッケージングしたjarファイルをGCPサーバーにアップロードします。ここではプロジェクトパス/app/build/libs
配下におきます。そしてdocker-composeでコンテナ群を起動します。
$ docker-compose up
Network reverse-proxy declared as external, but could not be found. Please create the network manually using `docker network create reverse-proxy` and try again.
reverse-proxy
ネットワークがないと怒られました。
$ docker network create reverse-proxy
fdf7cedf66ea285984eabbc77b49a1b27720b06e774d3d39975fd46066fa59c7
もう一回チャレンジしてみます。
$ docker-compose up
... 省略 ...
ktorapp | MySQL is unavailable - sleeping
ktorapp | MySQL is up - executing command
ktorapp | 2020-08-11 07:32:29.390 [main] TRACE Application
今度はできました!以上です。