Spring Boot を Docker devbox で即起動!

こんにちは、さるまりんです。

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 の devbox
  • Makefile → 人間向けの操作窓口(覚えるコマンドを減らすためです)

前提

  • 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"}

statusUP なら成功です。

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 を作った話

でした。

読んでくださってありがとうございました。
それではまた!