LoginSignup
3
3

More than 3 years have passed since last update.

Docker + Ktor + MySQL + NGINX(SSL) の環境構築

Last updated at Posted at 2020-08-12

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

システム構成

server-architecture-simple.png

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
  • db: MySQLコンテナ関連のファイルを格納
    • Dockerfile: mysql_hostコンテナのビルド用Dockerfile
    • my.cnf: MySQL設定ファイル

Ktor

それでは各コンテナのDockerfileとdocker-composeのサービス定義をみていきます。
まずktorのコンテナです。基本的には公式のクイックスタートに沿って構築します。KtorのコンテナをビルドするDockerfileは次のようになっています。

app/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になっていましたので、参考にさせていただきました。

wait-for-mysql.sh
#!/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のサービスの箇所をみていきましょう。

docker-compose.yml
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です。

db/Dockerfile
FROM mysql:8.0
RUN mkdir /var/log/mysql
RUN chown mysql:mysql /var/log/mysql

イメージをダウンロードして、ログ出力先ディレクトリを作成し権限を設定しています。
次に、docker-composeのサービス定義の箇所です。

docker-compose.yml
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_USERrootで指定してたんですが、そうすると、MySQLコンテナが起動したときにrootというユーザーを作成しようとして起動に失敗してしまうんですね...。
参考: https://stackoverflow.com/a/45086773
Ktorからrootユーザーではなく別のユーザーでアクセスしたい方は適宜セットしてください。

設定値の読み込みは.envファイルを使用しています。

.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のサービス定義のみです。

docker-compose.yml
  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:roroは read only モードとのことですが、このコンテナからは証明書を読み込むだけで書き込みは必要ないのでつけています。
  • NGINX_PROXY_CONTAINER=nginx_proxylabelは、Let's Encrypt companionコンテナがnginxプロキシコンテナを識別するために使用するもののようです

上記で3つのvolumesを定義しています。それらはSSL証明書発行のために使用されるので、後述のLet's Encrypt companionコンテナと共有しています。それぞれのボリュームについて解説します。

docker-volumes.png
※ 一部の矢印が点線なのは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証明書を発行します。

docker-compose.yml
  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

今度はできました!以上です。

そのほかの参考記事

Docker、ボリューム(Volume)について真面目に調べた

Docker network 概論

docker コマンドの --build-arg で設定した値を Dockerfile の CMD で利用する方法

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3