docker-composeでFlask+MySQL環境構築してハマった話

はじめに

docker-composeでFlask+MySQLの環境構築を行いました。

主に以下の記事を参考にしました。
PythonのFlaskでMySQLを利用したRESTfulなAPIをDocker環境で実装する - Qiita

Dockerは触っていましたが今回はじめてdocker-composeを触りまして、盛大にハマったのでメモします。

結論

私がハマった穴を踏まえ、うまくいかないときに見ると良いかもしれないポイントです。

基本

docker-compose logsでエラーログを確認する

MySQL

  • 初期データ投入のSQLファイルに構文エラーがないか
  • MYSQL_ROOT_PASSWOORDは設定されているか
  • MySQLバージョンに合わせた設定がmy.cnfに記載されているか
  • データ永続化用ディレクトリの中身は空か

Flask

  • tty: true オプションはついているか
  • volumes:のマウントはできているか
  • pip installは適切な設定か

やりたかったことと発生した事象

Flask用のコンテナと、MySQL用のコンテナを作って、Flask->MySQLに接続できるようにしたい、というのがやりたかったことです。

症状としては、docker-compose up -dしてもどちらのコンテナもExit 0, Exit 1となり立ち上がらない、というものでした。

$ docker-compose up -d
Starting to-buy-list_mysql_1 ... done
Starting to-buy-list_appserver_1 ... done

$ docker-compose ps
         Name                       Command             State    Ports
----------------------------------------------------------------------
to-buy-list_appserver_1   python3                       Exit 0        
sto-buy-list_mysql_1       docker-entrypoint.sh mysqld   Exit 1

最初の設定は以下でした。

フォルダ構成

.
├── app
│   ├── Dockerfile
│   ├── requirements.txt
│   └── src
├── docker-compose.yml
├── mysql
│   ├── Dockerfile
│   ├── logs
│   ├── my.cnf
│   ├── mysql_data
│   └── sqls
│       └── initialize.sql
.

docker-compose.yml

version: '3'
services:
  db:
    build: ./mysql/
    volumes:
      - ./mysql/mysql_data:/var/lib/mysql # データの永続化
      - ./mysql/sqls:/docker-entrypoint-initdb.d # 初期データ投入
      - ./mysql/logs:/var/log/mysql
    environment:
      - MYSQL_ROOR_PASSWORD=password
  appserver:
    build: ./app/
    links:
      - db

mysql/Dockerfile

FROM mysql:8.0
EXPOSE 3306

ADD ./my.cnf /etc/mysql/conf.d/my.cnf

CMD ["mysqld"]

app/Dockerfile

# ベースイメージ作成
FROM python:3.7

# requirements.txtをコピー
COPY requirements.txt .

# pipアップグレード
RUN pip install --upgrade pip
RUN pip install --user -r requirements.txt

# 作業ディレクトリ指定
WORKDIR /workdir

# 公開ポート指定
EXPOSE 8080

MySQLコンテナの解決

初期データ投入のSQLファイルに構文エラーがないか

初期データ投入用に、mysql/sqls/initialize.sqlを用意し、docker-compose.ymlでそれを読み込むように設定していました。

      - ./mysql/sqls:/docker-entrypoint-initdb.d # 初期データ投入

が、そのinitialize.sqlを改めて見てみると、コピペしたときに不要な文字列が紛れ込んでおり、構文エラーを引き起こしていることがわかりました。

MySQLバージョンに合わせた設定がmy.cnfに記載されているか

前項の修正をしてもいまだ立ち上がらないので、$ docker-compose logsでエラーログを出してみます。

$ docker-compose logs
Attaching to to-buy-list_db_1, to-buy-list_appserver_1
appserver_1  | Python 3.7.8 (default, Jul 22 2020, 12:48:54) 
appserver_1  | [GCC 8.3.0] on linux
appserver_1  | Type "help", "copyright", "credits" or "license" for more information.
appserver_1  | >>> db_1         | 2020-10-31 16:15:24+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.22-1debian10 started.
db_1         | 2020-10-31 16:15:24+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'
db_1         | 2020-10-31 16:15:24+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.22-1debian10 started.
db_1         | 2020-10-31 16:15:24+00:00 [ERROR] [Entrypoint]: Database is uninitialized and password option is not specified
db_1         |  You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD
You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD

とありますね。「MYSQL_ROOT_PASSWORD/MYSQL_ALLOW_EMPTY_PASSWORD/MYSQL_RANDOM_ROOT_PASSWORDのいずれかが設定されている必要がある」ようです。MYSQL_ROOT_PASSWORDを設定していたはずですが…と改めてdocker-compose.ymlを見てみると。

    environment:
      - MYSQL_ROOR_PASSWORD=password

普通にタイポしてました。辛い…

MySQLバージョンに合わせた設定がmy.cnfに記載されているか

それでもまだ動かないので、MySQLバージョンを8.0にしているのが理由かもと、試しにバージョンを5.7に書き換えてやってみると、うまくいきました。

MySQL8.0以降は、認証周りの初期設定が異なるためか、my.cnfの設定も8.0以降に合わせて書く必要があったのでした。

MySQL8.0を使いたかったため、以下の記事を参考に、my.cnfを書き換えました。
docker-compose MySQL8.0 のDBコンテナを作成する - Qiita

mysql/my.cnf

[mysqld]
# 文字コード/照合順序の設定
character-set-server = utf8mb4
collation-server = utf8mb4_bin

# タイムゾーンの設定
default-time-zone = SYSTEM
log_timestamps = SYSTEM

# デフォルト認証プラグインの設定
default-authentication-plugin = mysql_native_password

# エラーログの設定
log-error = /var/log/mysql/mysql-error.log

# スロークエリログの設定
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 5.0
log_queries_not_using_indexes = 0

# 実行ログの設定
general_log = 1
general_log_file = /var/log/mysql/mysql-query.log

# mysqlオプションの設定
[mysql]
# 文字コードの設定
default-character-set = utf8mb4

# mysqlクライアントツールの設定
[client]
# 文字コードの設定
default-character-set = utf8mb4

データ永続化用ディレクトリの中身は空か

my.cnfを8.0用に書き換えて再度ビルドしてみるものの、うまくいきません。
ログを見てみると、

db_1         | 2020-10-31T16:32:41.524601Z 1 [ERROR] [MY-012526] [InnoDB] Upgrade after a crash is not supported. This redo log was created with MySQL 5.7.32. Please follow the instructions at http://dev.mysql.com/doc/refman/8.0/en/upgrading.html
db_1         | 2020-10-31T16:32:41.524983Z 1 [ERROR] [MY-012930] [InnoDB] Plugin initialization aborted with error Generic error.
db_1         | 2020-10-31T16:32:41.972222Z 1 [ERROR] [MY-011013] [Server] Failed to initialize DD Storage Engine.
db_1         | 2020-10-31T16:32:41.976898Z 0 [ERROR] [MY-010020] [Server] Data Dictionary initialization failed.
db_1         | 2020-10-31T16:32:41.978094Z 0 [ERROR] [MY-010119] [Server] Aborting
db_1         | 2020-10-31T16:32:41.990226Z 0 [System] [MY-010910] [Server] /usr/sbin/mysqld: Shutdown complete (mysqld 8.0.22)  MySQL Community Server - GPL.
This redo log was created with MySQL 5.7.32. Please follow the instructions at http://dev.mysql.com/doc/refman/8.0/en/upgrading.html

あたりのメッセージを読むに、さきほど一度検証用にMySQL 5.7にしてビルドしたことが影響しているように見えます。

docker-compose.yml

    volumes:
      - ./mysql/mysql_data:/var/lib/mysql # データの永続化

↑でデータの保存先としてローカルフォルダをマウントしているため、これまで何度もビルドを繰り返してきたログやその他のファイルが格納されていました。
mysql/mysql_data/の中身をすべて削除すると、うまくいきました。

これでようやくDBコンテナのStatusがUpになり、接続して確認ができる状態となりました。

# DBコンテナに接続して確認
$ docker-compose exec db mysql -u root -p

Flaskコンテナの解決

tty: true オプションはついているか

オプションとして、tty: trueを付けることで、コンテナが起動時、すぐに終了してしまうのを防ぐことができます。

    tty: true

volumes:のマウントはできているか

この状態だと、まだボリュームのマウントができていません。

コンテナ単独で立ち上げる時の、以下のコマンドに代替する設定をdocker-composeに書いておく必要があります。

$ docker run -it -p 8080:5000 -v $(pwd)/src:/workdir --name flask-to-buy-list flask-app /bin/bash

合わせてポートのマッピングも設定しておきます。

他にも、
– コンテナ起動するとFlaskが動くようにcommand設定
– FlaskをCLIで動かすための環境変数など設定
なども入れています。

docker-compose.yml

(省略)
  appserver:
    build: ./app/
    ports:
      - 8080:5000
    volumes:
      - ./app/src:/workdir
    environment:
      TZ: Asia/Tokyo
      FLASK_APP: run.py
      FLASK_ENV: development
    command: flask run -h 0.0.0.0
    tty: true
    links:
      - db

pip installは適切な設定か

ここまでやると、appserverのコンテナのビルド、起動、ログインまでできるようになりました。

# コンテナにログイン(docker-composeコマンドで入る場合)
$ docker-compose exec appserver bash

# コンテナにログイン(dockerコマンドで入る場合)
$ docker exec -it to-buy-list_appserver_1 bash

しかし、コンテナにログインした後、flask db initなどのflaskコマンドがcommand not foundとなり実行できない問題が発生しました。

command not foundになる理由は、「コンテナにFlaskがインストールできていないから」が一番に考えられるものの、

$ pip install Flask

を実行しても、すでに入っているとのメッセージが出たのと、また

$ python run.py

でFlaskの起動自体はできていたため、どうしたものかと途方にくれていました。

再度コンテナのビルドをやり直したときに、以下のようなエラーが出ていることに気付きました。

WARNING: The script flask is installed in '/root/.local/bin' which is not on PATH.

つまり、「インストールしているけど、PATHが通っていないよ」ということでした。

appserverコンテナのDockerfileを以下のように修正しました

RUN pip install --user -r requirements.txt
↓
RUN pip install -r requirements.txt

--userオプションは、パッケージのインストール先を、ユーザーのホームディレクトリ配下に指定できるのですが、Dockerコンテナの場合、具体的には以下にインストールすることになります。

>>> import site
>>> site.getusersitepackages()
'/root/.local/lib/python3.7/site-packages'

コンテナの$PATHを確認してみます。

root@c4f97f85d99c:/workdir# echo $PATH
/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

インストール先のディレクトリが、$PATHに含まれていないよ、とWarningが出ているのでした。

なぜ他のパッケージは問題なくいけて、Flaskだけできないのかは不明ですが、この件については--userオプションをDockerfileの記述から削除することで解決しました。

ここまで乗り越え、ようやくFlaskとMySQL両方のコンテナが無事起動し、動いている状態にすることができました。

終わりに

Docker, docker-compose, MySQL, Pythonそれぞれの要素で少しずつハマり、結果数時間をコンテナ起動に溶かしてしまいました…
同じようなエラーに出くわした方のなにかの参考になれば幸いです。

参考