こんにちは、さるまりんです。
Spring Boot を試したいとき、こんなことを思ったことはありませんか?
- Java や Gradle のバージョンを気にしたくない
- DB も含めて「とりあえず動く環境」が欲しい
- 失敗しても、すぐ全部捨てられる実験場が欲しい
今回はそんなときのために、
Spring Boot 用のローカル devbox(開発用の実験環境)を
Docker とシェルで作ってみました。
目的は
Java / Kotlin どちらでも使える「さるまりん版 Initializr」を作る
です。
この devbox でできること
まず最初に、何ができる環境なのかをまとめます。
このテンプレートを使うと、次のことがすぐにできます。
- Java / Kotlin どちらでも Spring Boot アプリを起動できる
- ローカルに Java や Gradle をインストールしなくてOK
- PostgreSQL 付きの開発環境が
make init && make upで立ち上がる - ソースコードは通常の Spring Boot プロジェクト(
./app)として編集できる - DB・ビルド・実行環境はすべて Docker 側に隔離される
「ちょっと試したい」「検証したい」「壊してもいい環境が欲しい」
そんなときの Spring Boot 用ローカル実験場(devbox)です。
今回作る構成
最終的に、こんな構成になります。
salumarine-devbox/
├─ compose.yaml
├─ Makefile
├─ docker/
│ ├─ Dockerfile
│ └─ entrypoint.sh
├─ scripts/
│ ├─ init.sh
│ └─ doctor.sh
└─ README.md
それぞれ次のような役割があります。
scripts/init.sh→ Spring Initializr を叩いてapp/を生成compose.yaml→ Spring Boot + PostgreSQL の devboxMakefile→ 人間向けの操作窓口(覚えるコマンドを減らすためです)
前提
- Docker Desktop(docker compose が使える)
- macOS / Linux を想定
(Windows の場合は WSL2 なら近い感覚で動きます)
今回は手元のmacで動かしています。
Step 1:ディレクトリ作成
まずはディレクトリを作成します。ここにファイルが配置されていきます。
mkdir salumarine-devbox
cd salumarine-devbox
mkdir -p docker scripts
Step 2:固定テンプレートを配置
ここは「コピペでOK」ゾーンです。
compose.yaml
ポイントは2つ:
- ソースは
./appをそのままマウント - Gradle キャッシュは
/workspace/.gradleに寄せて権限トラブル回避
services:
app:
build:
context: .
dockerfile: docker/Dockerfile
args:
UID: "${UID:-1000}"
GID: "${GID:-1000}"
working_dir: /workspace/app
volumes:
- ./app:/workspace/app
- gradle-cache:/workspace/.gradle
environment:
GRADLE_USER_HOME: /workspace/.gradle
SPRING_PROFILES_ACTIVE: dev
SPRING_DATASOURCE_URL: "jdbc:postgresql://db:5432/app"
SPRING_DATASOURCE_USERNAME: "app"
SPRING_DATASOURCE_PASSWORD: "app"
JAVA_TOOL_OPTIONS: "-Dfile.encoding=UTF-8"
ports:
- "8080:8080"
depends_on:
- db
command: ["bash", "/workspace/entrypoint.sh"]
db:
image: postgres:16
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: app
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
gradle-cache:
docker/Dockerfile
ここは今回いちばん試行錯誤したところです。
- GID / UID がすでに存在していても落ちない
devユーザーを必ず作る
という安全寄りの実装にしています。
FROM eclipse-temurin:17-jdk
ARG UID=1000
ARG GID=1000
RUN apt-get update \
&& apt-get install -y --no-install-recommends bash curl unzip ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN set -eux; \
if ! getent group dev >/dev/null; then \
(groupadd -g "${GID}" dev) || groupadd dev; \
fi; \
if ! id -u dev >/dev/null 2>&1; then \
(useradd -m -u "${UID}" -g dev -s /bin/bash dev) || useradd -m -g dev -s /bin/bash dev; \
fi
WORKDIR /workspace
COPY docker/entrypoint.sh /workspace/entrypoint.sh
RUN chmod +x /workspace/entrypoint.sh
USER dev
docker/entrypoint.sh
#!/usr/bin/env bash
set -euo pipefail
cd /workspace/app
if [[ ! -f "./gradlew" ]]; then
echo "[entrypoint] ./gradlew が見つかりません。先に make init を実行してください。"
exit 1
fi
chmod +x ./gradlew || true
echo "[entrypoint] bootRun start..."
./gradlew bootRun
scripts/init.sh
Spring Initializr は POST(-d)方式で叩いています。
この方がGETで投げるより問題が少なくて安全です。
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT_DIR"
APP_DIR="$ROOT_DIR/app"
if [[ -d "$APP_DIR" ]]; then
echo "[init] app/ は既に存在します。作り直す場合は app/ を削除してください。"
exit 1
fi
TMP_ZIP="$(mktemp -t springXXXXXX.zip)"
curl -fsSL \
-o "$TMP_ZIP" \
"https://start.spring.io/starter.zip" \
-d "type=gradle-project" \
-d "language=java" \
-d "javaVersion=17" \
-d "groupId=com.salumarine" \
-d "artifactId=app" \
-d "name=app" \
-d "packageName=com.salumarine.devbox" \
-d "dependencies=web,actuator,data-jpa,postgresql,devtools"
mkdir app
unzip -q "$TMP_ZIP" -d app
rm "$TMP_ZIP"
mkdir -p app/src/main/resources
cat > app/src/main/resources/application-dev.yml <<'YAML'
spring:
jpa:
hibernate:
ddl-auto: update
management:
endpoints:
web:
exposure:
include: health,info
YAML
echo "[init] 完了!次は make up です。"
Makefile
コマンドはここにまとめました。
.PHONY: init up down logs sh
init:
./scripts/init.sh
up:
docker compose up -d --build
down:
docker compose down
logs:
docker compose logs -f app
sh:
docker compose exec app bash
README.md
何ができるかをまとめたREADMEも入れておきます。
あとから触るときや、別の環境で試すときは README を見れば思い出せるようにしています。
# salumarine-devbox
Spring Boot + PostgreSQL を Docker でサクッと起動する devbox テンプレートです。
## Requirements (要件)
- Docker Desktop(docker compose が使える)
## Quick start
```bash
chmod +x scripts/*.sh docker/*.sh
make init
make up
make logs
```
起動確認:
```bash
curl -s http://localhost:8080/actuator/health
```
コマンド
- make init : Spring Initializr で app/ を生成
- make up : コンテナ起動(build込み)
- make logs : アプリログ追尾
- make sh : app コンテナに入る
- make down : 停止
Reset
DBとキャッシュも含めて全部消す
```bash
docker compose down -v
```
### おまけ:doctor.sh について
今回の内容では使っていませんが、`scripts/doctor.sh` という
簡単な診断用スクリプトも置いています。
- Docker / docker compose のバージョン確認
- ファイル構成のチェック
- コンテナの起動状態確認
「なんか動かないな?」というときに、
状況をまとめて確認するためのスクリプトです。
Step 3:起動
ファイルの準備ができたら起動です。
スクリプトに実行権限を付与してmake init && make upで作成+起動できます。
chmod +x scripts/*.sh docker/*.sh
make init
make up
make logs
make logsでログを確認するとこんな風になってきていたら問題なく起動できています。
% make logs
docker compose logs -f app
app-1 | [entrypoint] bootRun start...
app-1 | Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
app-1 | Downloading https://services.gradle.org/distributions/gradle-9.2.1-bin.zip
app-1 | ............10%.............20%.............30%.............40%.............50%.............60%.............70%.............80%.............90%.............100%
app-1 |
app-1 | Welcome to Gradle 9.2.1!
app-1 |
app-1 | Here are the highlights of this release:
app-1 | - Windows ARM support
app-1 | - Improved publishing APIs
app-1 | - Better guidance for dependency verification failures
app-1 |
app-1 | For more details see https://docs.gradle.org/9.2.1/release-notes.html
app-1 |
app-1 | Starting a Gradle Daemon (subsequent builds will be faster)
app-1 | > Task :compileJava
app-1 | > Task :processResources
app-1 | > Task :classes
app-1 | > Task :resolveMainClassName
app-1 |
app-1 | > Task :bootRun
app-1 |
app-1 | . ____ _ __ _ _
app-1 | /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
app-1 | Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
app-1 | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
app-1 | \\/ ___)| |_)| | | | | || (_| | ) ) ) )
app-1 | ' |____| .__|_| |_|_| |_\__, | / / / /
app-1 | =========|_|==============|___/=/_/_/_/
app-1 |
app-1 | :: Spring Boot :: (v4.0.1)
app-1 |
app-1 | 2026-01-03T06:57:58.890Z INFO 401 --- [app] [ restartedMain] com.salumarine.devbox.AppApplication : Starting AppApplication using Java 17.0.17 with PID 401 (/workspace/app/build/classes/java/main started by dev in /workspace/app)
app-1 | 2026-01-03T06:57:58.895Z INFO 401 --- [app] [ restartedMain] com.salumarine.devbox.AppApplication : The following 1 profile is active: "dev"
app-1 | 2026-01-03T06:57:58.962Z INFO 401 --- [app] [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
app-1 | 2026-01-03T06:57:58.962Z INFO 401 --- [app] [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
app-1 | 2026-01-03T06:58:00.066Z INFO 401 --- [app] [ restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
app-1 | 2026-01-03T06:58:00.092Z INFO 401 --- [app] [ restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 14 ms. Found 0 JPA repository interfaces.
app-1 | 2026-01-03T06:58:00.693Z INFO 401 --- [app] [ restartedMain] o.s.boot.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
app-1 | 2026-01-03T06:58:00.706Z INFO 401 --- [app] [ restartedMain] o.apache.catalina.core.StandardService : Starting service [Tomcat]
app-1 | 2026-01-03T06:58:00.707Z INFO 401 --- [app] [ restartedMain] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/11.0.15]
app-1 | 2026-01-03T06:58:00.746Z INFO 401 --- [app] [ restartedMain] b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in 1782 ms
app-1 | 2026-01-03T06:58:01.011Z INFO 401 --- [app] [ restartedMain] org.hibernate.orm.jpa : HHH008540: Processing PersistenceUnitInfo [name: default]
app-1 | 2026-01-03T06:58:01.096Z INFO 401 --- [app] [ restartedMain] org.hibernate.orm.core : HHH000001: Hibernate ORM core version 7.2.0.Final
app-1 | 2026-01-03T06:58:01.909Z INFO 401 --- [app] [ restartedMain] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer
app-1 | 2026-01-03T06:58:01.960Z INFO 401 --- [app] [ restartedMain] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
app-1 | 2026-01-03T06:58:02.149Z INFO 401 --- [app] [ restartedMain] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@3b17b192
app-1 | 2026-01-03T06:58:02.151Z INFO 401 --- [app] [ restartedMain] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
app-1 | 2026-01-03T06:58:02.248Z INFO 401 --- [app] [ restartedMain] org.hibernate.orm.connections.pooling : HHH10001005: Database info:
app-1 | Database JDBC URL [jdbc:postgresql://db:5432/app]
app-1 | Database driver: PostgreSQL JDBC Driver
app-1 | Database dialect: PostgreSQLDialect
app-1 | Database version: 16.11
app-1 | Default catalog/schema: app/public
app-1 | Autocommit mode: undefined/unknown
app-1 | Isolation level: READ_COMMITTED [default READ_COMMITTED]
app-1 | JDBC fetch size: none
app-1 | Pool: DataSourceConnectionProvider
app-1 | Minimum pool size: undefined/unknown
app-1 | Maximum pool size: undefined/unknown
app-1 | 2026-01-03T06:58:02.831Z INFO 401 --- [app] [ restartedMain] org.hibernate.orm.core : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
app-1 | 2026-01-03T06:58:02.845Z INFO 401 --- [app] [ restartedMain] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
app-1 | 2026-01-03T06:58:02.934Z WARN 401 --- [app] [ restartedMain] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
app-1 | 2026-01-03T06:58:03.460Z INFO 401 --- [app] [ restartedMain] o.s.b.a.e.web.EndpointLinksResolver : Exposing 2 endpoints beneath base path '/actuator'
app-1 | 2026-01-03T06:58:03.541Z INFO 401 --- [app] [ restartedMain] o.s.boot.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
app-1 | 2026-01-03T06:58:03.551Z INFO 401 --- [app] [ restartedMain] com.salumarine.devbox.AppApplication : Started AppApplication in 5.305 seconds (process running for 5.807)
app-1 | 2026-01-03T06:59:27.320Z INFO 401 --- [app] [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
app-1 | 2026-01-03T06:59:27.320Z INFO 401 --- [app] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
app-1 | 2026-01-03T06:59:27.322Z INFO 401 --- [app] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
```
(警告も出ていますが、それはおいおい直していこうと思います。)
Step 4:起動確認
実際に起動できているかをcurlで叩いてみて確認します
curl -s http://localhost:8080/actuator/health
出力例:
{"groups":["liveness","readiness"],"status":"UP"}
status が UP なら成功です。
Kotlin でも使える?
はい、そのまま使えます。
例えば scripts/init.sh のこの1行:
-d "language=java"
を
-d "language=kotlin"
に変えるだけで、
- Kotlin
- Spring Boot
- PostgreSQL
の devbox が同じ手順で立ち上がります。
以降の操作(make up / make logs / curl)は 完全に同じです。
これは「シェル」についてなの?「Docker」についてなの?
どちらもちょっと違います。
- シェルは 接着剤
- make は 人間の入口
- docker compose は 実行基盤
- 主役は Spring Boot(Java / Kotlin)
この組み合わせで
「環境構築の面倒を全部 devbox に押し込めた」
というのが今回のポイントです。
今後は「対話式ジェネレータ」へ
今回は 固定テンプレートでしたが、ここからはこうしていきたいと思っています。
- Java / Kotlin / 他の JVM 言語も選べる
- PostgreSQL / MySQL / 何もなし、などを選択
- Ubuntu 以外の Linux ベースも選択
- すべて 対話式(CLI)で選ぶだけ
Spring Initializr × devbox × 対話式ジェネレータ。
もう少し遊びながら育てていく予定です。
一言で
今回は
Java / Kotlin の Spring Boot を、環境構築に悩まず “すぐ動かす” ための Docker devbox を作った話
でした。
読んでくださってありがとうございました。
それではまた!