Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

リモートデバッグ可能なNginx, Spring Boot, MySQLの開発環境をDockerで作った

前置き

最近、WebサイトをSpring Bootで開発しています。以前に投稿した記事のような開発環境が欲しくなってきましたので調査しました。前回とほぼ同じ形式で執筆します。

動作環境

   
OS macOS Catalina 10.15.5
Docker docker desktop community 2.3.0.3 (45519)
Spring Tool Suite Version: 4.5.1.RELEASE

Spring Bootプロジェクト

InteliJ + Kotlin + gradleとしたいところですが、現在携わっている環境に近い構成でやっていこうと思います。
モダンJavaだとこの組み合わせ減ってると思います:sob::sob:

  • Spring Boot 2.2
  • Java OpenJDK 11
  • STS 4
  • Maven 3.6.3

構築イメージ

まずは構築する環境を説明します。
構成の図解.png

以下のコンテナを作成します。

  • MySQL
    • ホストOSのMySQL Workbenchでアクセスできる(localhost:3306)
  • Spring Boot
    • ホストOSのブラウザでアクセスできる(localhost:8080)
    • STS(Eclipse)からリモートデバッグできる(localhost:5005)
  • Nginx
    • HTTPSでアクセスできる(https://devnokiyo.example.com)
    • ただし、GitHubにソースコードを公開する関係で自己署名SSL証明書)

もちろん、コンテナ間も通信を行います。

構成

GitHubに公開しています。併せてご覧ください。
前述の記事同様にDocker周りだけを管理したいディレクトリと永続化だけを管理したいディレクトリに分けました。
以降、粛々とファイルとその説明が続きます。「とにかくGitHubのコードを動かしたい」方はこちらまで飛んでください:airplane_departure::airplane_departure::airplane_departure:

$ tree
├── README.md
├── docker                          # DockerやDocker Compose
│   ├── containers                   # 各コンテナ(イメージ)
│   │   ├── mysql                     # MySQL 5.7
│   │   │   ├── Dockerfile             # MySQL 5.7のDockerファイル
│   │   │   ├── initialize_data.sql    # 初期セットアップでユーザとサンプルデータを作成するスプリプト
│   │   │   └── my.cnf                 # 初期セットアップで反映するmy.cnf
│   │   ├── nginx                     # Nginx 1.19.0
│   │   │   ├── Dockerfile             # NginxのDockerファイル
│   │   │   └── nginx.conf             # 初期セットアップで反映するnginx.conf
│   │   └── spring                    # OpenJDK 11 (Spring Boot構築用)
│   │       └── Dockerfile             # Spring Boot構築用のDockerファイル
│   ├── docker-compose.yml          # Docker Composeファイル
│   └── environments                # 環境変数定義 
│       ├── common.env               # 各コンテナ共通
│       └── db.env                   # DB接続用MySQL関連
└── volumes                         # 永続化するリソース
    ├── app                          # Spring Bootのプロジェクト (以下、特筆点するディレクトリ/ファイルのみコメント)
    │   ├── .m2                       # Mavenのローカルリポジトリ
    │   ├── mvnw                      # Maven Wrapper 今回はこれを使ってSpring Bootを起動する
    │   └── src                        # ソースコード
    │       └── main
    │           └── resources            # この配下にstaticディレクトリを作成しない (静的ファイルはNginxで返す)
    ├── db                           # MySQLのデータ このディレクトリは最初は無し
    └── web                          # Nginxのファイル
        ├── ssl                       # SSL証明書
        │   ├── privkey.pem            # 秘密鍵
        │   └── server.crt             # サーバー証明証
        └── static                    # 静的ファイル置き場

Composeファイル

docker/docker-compose.yml
version: '3'
services:                                          # 各コンテナ(サービス)
  db:                                               # MySQLのコンテナ 「db」と命名
    build: containers/mysql                          # Dockerファイルのパス
    env_file:                                        # 環境変数
      - ./environments/common.env                     # 各コンテナ共通
      - ./environments/db.env                         # DB接続用MySQL関連
    volumes:                                         # 永続化
      - ../volumes/db/data:/var/lib/mysql             # MySQLのデータ
    ports:                                           # 開放ポート
      - 3306:3306                                     # ホストOSのWorkBenchでDBを参照する目的
  app:                                              # Spring Bootのコンテナ 「app」と命名
    build: containers/spring                         # Dockerファイルのパス
    env_file:                                        # 環境変数
      - ./environments/common.env                     # 各コンテナ共通
      - ./environments/db.env                         # DB接続用MySQL関連
    # 実行するコマンド(後述)
    command: ./mvnw clean spring-boot:run -Dspring-boot.run.jvmArguments="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005"
    volumes:                                         # 永続化
      - ../volumes/app:/app                           # Spring Bootのプロジェクト
      - ../volumes/app/.m2:/root/.m2                  # Mavenのローカルリポジトリ
    ports:                                           # 開放ポート
      - 8080:8080                                     # ホストOSからNginxを通さずTomcatを直接参照する目的
      - 5005:5005                                     # IDEでリモートデバッグする目的
    depends_on:                                      # 起動する順番
      - db                                            # 「db」の後で起動
  web:                                              # Nginxのコンテナ 「web」と命名
    build: containers/nginx                          # Dockerファイルのパス
    env_file:                                       # 環境変数
      - ./environments/common.env                    # 各コンテナ共通
    volumes:                                        # 永続化
      - ../volumes/web/static:/usr/share/nginx/www/  # 静的ファイル (Spring Bootプロジェクトのresouce/staticには配置しない)
      - ../volumes/web/ssl:/etc/nginx/cert/          # SSL証明書
      - ../volumes/web/log:/var/log/nginx/           # Nginxのログ
    ports:                                          # 開放ポート
      - 443:443                                      # HTTPS この開発環境はHTTPSのみ想定
    depends_on:                                     # 起動する順番
      - app                                          # 「app」の後で起動

db(MySQL)コンテナ

Dockerファイル

docker/containers/mysql/Dockerfile
# MySQL 5.7
FROM mysql:5.7

# 初期セットアップで利用するmy.cnfをイメージへコピー
# アーカイブを展開する必要などが無ければADDで無くてCOPYで良い。
COPY my.cnf /etc/mysql/conf.d

# 初期セットアップで実行したいスクリプトをイメージへコピー
COPY initialize_data.sql /docker-entrypoint-initdb.d

コピーするファイルの概要

ホスト イメージ 概要
docker/containers/mysql/my.cnf /etc/mysql/conf.d/ 文字コード
docker/containers/mysql/initialize_data.sql /docker-entrypoint-initdb.d/ Spring Bootアプリ向けDBアカウント/サンプルデータ

Docker Comopseから設定される環境変数

docker/environments/common.env
TZ=Asia/Tokyo
docker/environments/db.env
MYSQL_ROOT_PASSWORD=root
MYSQL_USER=spring
MYSQL_PASSWORD=spring

Docker Comopseから設定される永続化

ホスト コンテナ 概要
volumes/db/data /var/lib/mysql MySQLのデータ

app(Spring Boot)コンテナ

Dockerファイル

docker/containers/spring/Dockerfile
FROM openjdk:11

# 永続化でマウントされるパスをワーキングディレクトリに指定
WORKDIR /app

Docker Comopseから設定される環境変数

docker/environments/common.env
TZ=Asia/Tokyo
docker/environments/db.env
MYSQL_ROOT_PASSWORD=root
MYSQL_USER=spring
MYSQL_PASSWORD=spring

Docker Comopseから設定される永続化

ホスト コンテナ 概要
volumes/app/ /app Spring Bootのプロジェクト
GitHubに公開しているものは予めサンプルアプリが入っています)
volumes/app/.m2 /root/.m2 Mavenのローカルリポジトリ
./mvnw cleanを実行するとプラグイン/ライブラリなどを毎回ダウンロードしてしまうことを防ぎます。

Docker Comopseのcommand

こちらのコマンドについて少し補足します。

./mvnw clean spring-boot:run -Dspring-boot.run.jvmArguments="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005"
コマンド 概要
./mvnw Java -jarによる起動は開発中のコードが更新されたときの自動リロードが無効になってしまうので、Mavenで起動することにしました。Maven WrapperがあればMavenをインストールする必要がないので、mvnwを採用しました。
address=*:5005 JDWPのポートを指定しますが、 ホストOSからコンテナへアクセスするには*:5005とする必要があります。ポート番号しか書いていないネット情報が多いのでご注意ください:skull_crossbones::skull_crossbones:

web(Nginx)コンテナ

Dockerファイル

docker/containers/nginx/Dockerfile
FROM nginx:1.19.0

# 初期セットアップで反映するファイルをコピー
COPY nginx.conf /etc/nginx/conf.d/app.conf
CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf

コピーするファイルの概要

ホスト イメージ 概要
docker/containers/nginx/nginx.conf /etc/nginx/conf.d/app.conf 後述

WEBサーバーの設定

トリッキーな設定は無いと思いますが簡単に補足します。
GitHubに公開しているものはSSL証明書については「*.example.com」の自己署名SSL証明書(所謂「オレオレ証明書」)を利用しています。筆者の環境ではLet's EncryptのSSL証明書を利用しています。

docker/containers/nginx/nginx.conf
server {
  listen 443 ssl;
  server_name devnokiyo.example.com;

  ssl_certificate     /etc/nginx/cert/server.crt;
  ssl_certificate_key /etc/nginx/cert/privkey.pem;
  ssl_prefer_server_ciphers on;

  access_log /var/log/nginx/access.log;
  error_log  /var/log/nginx/error.log;

  root /app/public;
  try_files $uri /;
  client_max_body_size 10m;
  error_page 404             /404.html;
  error_page 505 502 503 504 /500.html;

  location / {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_set_header X-CSRF-Token $http_x_csrf_token;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_pass http://app:8080;
  }

  # Spring Bootの静的ファイルはNginxで返す。
  location ~* .*\.(jpg|gif|png|css|js|ico|svg) {
      root /usr/share/nginx/www;
      access_log off;
  }
}

Docker Comopseから設定される環境変数

docker/environments/common.env
TZ=Asia/Tokyo

Docker Comopseから設定される永続化

ホスト コンテナ 概要
volumes/web/static/ /usr/share/nginx/www/ 静的ファイル
volumes/web/ssl/ /etc/nginx/cert/ SSL証明書
volumes/web/log/ /var/log/nginx/ ログ

実行してみましょう!

Docker Composeで起動

だいぶ簡単でしたが各ファイルの説明が終わりましたので実行して確認します。

  1. GitHubから一式ダウンロードします。
  2. dockerディレクトリへ移動します。

    $ cd docker
    
  3. ビルドします。(初回なので起動でも構いません)

    $ docker-compose build --no-cache
    Building db
    Step 1/3 : FROM mysql:5.7
    ---> 9cfcce23593a
    Step 2/3 : COPY my.cnf /etc/mysql/conf.d
    ---> 4af29808e20c
        :
        :
    Successfully built b074e2d8831d
    Successfully tagged docker_web:latest
    
  4. 起動します。

    $ docker-compose up
    Creating network "docker_default" with the default driver
    Creating docker_db_1 ... done
    Creating docker_app_1 ... done
    Creating docker_web_1 ... done
    Attaching to docker_db_1, docker_app_1, docker_web_1
    db_1   | 2020-06-22 23:04:49+09:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 5.7.30-1debian10 started.
        :
        :
    db_1   | Version: '5.7.30'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)
        :
        :
    app_1  | [INFO] Scanning for projects...
    app_1  | Downloading from central: https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-starter-parent/2.2.8.RELEASE/spring-boot-starter-parent-2.2.8.RELEASE.pom
    app_1  | Downloading from central: https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-dependencies/2.2.8.RELEASE/spring-boot-dependencies-2.2.8.RELEASE.pom
        :
        :
    app_1  | [INFO] Attaching agents: []
    app_1  | Listening for transport dt_socket at address: 5005
    app_1  | 
    app_1  |   .   ____          _            __ _ _
    app_1  |  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
    app_1  | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
    app_1  |  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
    app_1  |   '  |____| .__|_| |_|_| |_\__, | / / / /
    app_1  |  =========|_|==============|___/=/_/_/_/
    app_1  |  :: Spring Boot ::        (v2.2.8.RELEASE)
    app_1  | 
    app_1  | 2020-06-22 23:07:31.526  INFO 74 --- [  restartedMain] com.example.demo.DemoApplication         : Starting DemoApplication on e5dd9a4954d8 with PID 74 (/app/target/classes started by root in /app)
        :
        :
    app_1  | 2020-06-22 23:07:37.683  INFO 74 --- [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
    app_1  | 2020-06-22 23:07:37.686  INFO 74 --- [  restartedMain] com.example.demo.DemoApplication         : Started DemoApplication in 6.781 seconds (JVM running for 8.41)
    

ブラウザからアクセス

hostsファイルにdevnokiyo.example.comをループバックIPで設定してhttps://devnokiyo.example.com にアクセスします。不正な証明書ということでエラーが出ると思いますが便宜上例外を許可してください。以下はChromeの例ですが表示自体はされています。

ブラウザの警告.png
nginxへアクセス.png
Nginxを経由でWebサイトにアクセスできています:thumbsup:
なお、ポポデザイン様のテンプレートを利用させて頂きました。感謝申し上げます。

前述のとおり筆者専用環境ではLet's EncryptのSSL証明書を利用しています。ご参考までに正常な証明書扱いの画像も載せておきます。独自ドメインを所有しておりhoge.devnokiyo.comで取得した例になります。
スクリーンショット 2020-06-23 1.32.32.png

Tomcatへアクセス

Nginxの設定の誤りで動作不良になることもあると思いますので、Tomcatへの直接アクセスも許可しています。念のためアクセスできるか確認します。
http://localhost:8080 にアクセスします。レイアウトが崩れています:thermometer_face::thermometer_face::thermometer_face:
しかし、静的ファイルはNginxから返されるように設定しているので問題ありません:bangbang:
Tomcatにアクセス.png

ホストOSからMySQLへ接続を確認

ホストOSのMySQL Workbenchから以下にアクセスします。

ホスト名 127.0.0.1
ポート 3306
ユーザー spring
パスワード spring

無事できていますね:thumbsup:
DB_確認_1.png

TomcatからMySQLへ接続を確認

Docker Composeで定義したコンテナ間でMySQLへ接続できるか確認します。
https://devnokiyo.example.com/formへアクセスしてお問い合わせしてみます。
お問い合わせ.png
送信ボタンを押下します。
お問い合わせ完了.png
再度MySQL Workbenchでテーブルを参照するとレコードが1行増えていますね:thumbsup:
DB_確認_4.png

リモートデバッグする

Spring BootをDockerでコンテナ化しましたが、ブレークポイントを利用できないと不便です。STS(Eclipse)からTomcatのコンテナをアタッチしてデバッグできるようにします。

準備

STS(Eclipse)のメニューからRun -> Debug Configurations...を選択します。Remote Java ApplicaiondemoプロジェクトをDebugします。
リモートデバッグの準備.png

ブレークポイントを設定

RootControllerの29行目にブレークポイントを設定しました。
ブレークポイントを設定
http://devnokiyo.example.com/formへアクセスするとブレークポイントで処理が止まります。
ブレークポイントで停止中.png

自動リロード

ソースコードを修正したときに毎回ビルドするのは面倒なので、Spring Boot Devtoolsの自動リロードを利用します。前述のとおりJava -jarによる起動は開発中のコードが更新されたときの自動リロードが無効になってしまいますが、Maven WrapperでSpring Bootを起動しているので自動リロードできます。

ソースコード修正

以下のアクションメソッドを追加してみます。コントローラークラスのファイルを保存するとリロードされます。

RootController.java
@GetMapping("/hoge")
public String hoge() {
    return "root/index";
}

ソースコード修正

自動リロード中

コンソールをみるとリロードの状況がわかります。1行目が初回起動時の日時で2行目以降が自動リロードの挙動になります。

app_1  | 2020-06-23 00:09:00.911  INFO 40 --- [  restartedMain] com.example.demo.DemoApplication         : Started DemoApplication in 6.459 seconds (JVM running for 8.092)
app_1  | 2020-06-23 00:21:51.379  INFO 40 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
app_1  | 2020-06-23 00:21:51.379  INFO 40 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
app_1  | 2020-06-23 00:21:51.389  INFO 40 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 9 ms
app_1  | 2020-06-23 00:36:13.585  INFO 40 --- [       Thread-5] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'
app_1  | 2020-06-23 00:36:13.588  INFO 40 --- [       Thread-5] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
app_1  | 2020-06-23 00:36:13.651  INFO 40 --- [       Thread-5] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
app_1  | 2020-06-23 00:36:13.755  INFO 40 --- [       Thread-5] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
app_1  | 
app_1  |   .   ____          _            __ _ _
app_1  |  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
app_1  | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
app_1  |  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
app_1  |   '  |____| .__|_| |_|_| |_\__, | / / / /
app_1  |  =========|_|==============|___/=/_/_/_/
app_1  |  :: Spring Boot ::        (v2.2.8.RELEASE)
app_1  | 
app_1  | 2020-06-23 00:36:14.036  INFO 40 --- [  restartedMain] com.example.demo.DemoApplication         : Starting DemoApplication on e5dd9a4954d8 with PID 40 (/app/target/classes started by root in /app)
app_1  | 2020-06-23 00:36:14.037  INFO 40 --- [  restartedMain] com.example.demo.DemoApplication         : No active profile set, falling back to default profiles: default
app_1  | 2020-06-23 00:36:14.500  INFO 40 --- [  restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
        :
        :
app_1  | 2020-06-23 00:36:15.884  INFO 40 --- [  restartedMain] o.s.b.d.a.OptionalLiveReloadServer       : LiveReload server is running on port 35729
app_1  | 2020-06-23 00:36:15.907  INFO 40 --- [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
app_1  | 2020-06-23 00:36:15.908  INFO 40 --- [  restartedMain] com.example.demo.DemoApplication         : Started DemoApplication in 1.965 seconds (JVM running for 1644.96)
app_1  | 2020-06-23 00:36:15.911  INFO 40 --- [  restartedMain] .ConditionEvaluationDeltaLoggingListener : Condition evaluation unchanged
app_1  | 2020-06-23 00:36:30.177  INFO 40 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
app_1  | 2020-06-23 00:36:30.178  INFO 40 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
app_1  | 2020-06-23 00:36:30.183  INFO 40 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 5 ms

修正後の確認

https://devnokiyo.example.com/hogeにアクセスしてトップページと同じページが表示されることを確認します。
自動リロード済み

終わりに

前回同様に基礎的なコマンドの説明よりは「筆者はこうやった」的なことをメインに記事を書いてみました。JVM系はRubyやGoと比較するとDocker周りの情報が少ない印象です。(点々とした情報はあるのですが、線になっている情報は少ない雰囲気です。)
ひととおりの手順を習得できたので、どのくらい開発環境として使っていけるか運用してみようと思います。

devnokiyo
SESからゲーム業界の東証1部上場企業に転職した正社員エンジニアです。Webサイトや稀にモバイルアプリを自社開発してます。少し採用業務も始めました。教材を執筆したりAWSスクールのお手伝いもしてます。犬とデジモノガジェットが好きです。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away