前置き
最近、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だとこの組み合わせ減ってると思います
- Spring Boot 2.2
- Java OpenJDK 11
- STS 4
- Maven 3.6.3
構築イメージ
以下のコンテナを作成します。
- 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のコードを動かしたい」方はこちらまで飛んでください
$ 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ファイル
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ファイル
# 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から設定される環境変数
TZ=Asia/Tokyo
MYSQL_ROOT_PASSWORD=root
MYSQL_USER=spring
MYSQL_PASSWORD=spring
Docker Comopseから設定される永続化
ホスト | コンテナ | 概要 |
---|---|---|
volumes/db/data | /var/lib/mysql | MySQLのデータ |
app(Spring Boot)コンテナ
Dockerファイル
FROM openjdk:11
# 永続化でマウントされるパスをワーキングディレクトリに指定
WORKDIR /app
Docker Comopseから設定される環境変数
TZ=Asia/Tokyo
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 とする必要があります。ポート番号しか書いていないネット情報が多いのでご注意ください![]() ![]() |
web(Nginx)コンテナ
Dockerファイル
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証明書を利用しています。
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から設定される環境変数
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で起動
だいぶ簡単でしたが各ファイルの説明が終わりましたので実行して確認します。
-
GitHubから一式ダウンロードします。
-
dockerディレクトリへ移動します。
$ cd docker
-
ビルドします。(初回なので起動でも構いません)
$ 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
-
起動します。
$ 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の例ですが表示自体はされています。



前述のとおり筆者専用環境ではLet's EncryptのSSL証明書を利用しています。ご参考までに正常な証明書扱いの画像も載せておきます。独自ドメインを所有しておりhoge.devnokiyo.comで取得した例になります。
Tomcatへアクセス
Nginxの設定の誤りで動作不良になることもあると思いますので、Tomcatへの直接アクセスも許可しています。念のためアクセスできるか確認します。
http://localhost:8080
にアクセスします。レイアウトが崩れています
しかし、静的ファイルはNginxから返されるように設定しているので問題ありません
ホストOSからMySQLへ接続を確認
ホストOSのMySQL Workbenchから以下にアクセスします。
ホスト名 | 127.0.0.1 |
ポート | 3306 |
ユーザー | spring |
パスワード | spring |
TomcatからMySQLへ接続を確認
Docker Composeで定義したコンテナ間でMySQLへ接続できるか確認します。
https://devnokiyo.example.com/form
へアクセスしてお問い合わせしてみます。
送信ボタンを押下します。
再度MySQL Workbenchでテーブルを参照するとレコードが1行増えていますね
リモートデバッグする
Spring BootをDockerでコンテナ化しましたが、ブレークポイントを利用できないと不便です。STS(Eclipse)からTomcatのコンテナをアタッチしてデバッグできるようにします。
準備
STS(Eclipse)のメニューからRun
-> Debug Configurations...
を選択します。Remote Java Applicaion
でdemo
プロジェクトをDebugします。
ブレークポイントを設定
RootControllerの29行目にブレークポイントを設定しました。
http://devnokiyo.example.com/form
へアクセスするとブレークポイントで処理が止まります。
自動リロード
ソースコードを修正したときに毎回ビルドするのは面倒なので、Spring Boot Devtoolsの自動リロードを利用します。前述のとおりJava -jar
による起動は開発中のコードが更新されたときの自動リロードが無効になってしまいますが、Maven WrapperでSpring Bootを起動しているので自動リロードできます。
ソースコード修正
以下のアクションメソッドを追加してみます。コントローラークラスのファイルを保存するとリロードされます。
@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周りの情報が少ない印象です。(点々とした情報はあるのですが、線になっている情報は少ない雰囲気です。)
ひととおりの手順を習得できたので、どのくらい開発環境として使っていけるか運用してみようと思います。