Three isolated Spring Boot environments, one repository.
Coursework, personal experiments and the graded project β each in its own container, on its own ports, running side by side without ever clashing.
English Β· PortuguΓͺs π§π·
- Why three environments
- Repository layout
- Ports
- Requirements
- Quick start
- Everyday commands
- Debugging
- Testing and code quality
- Production image
- How the Docker setup works
- Troubleshooting
- FAQ
Study code, assignments and a graded project have different needs. Mixing them in one workspace means the graded project drowns in scratch files, and a broken experiment can take down the thing being submitted.
So each lives in its own container:
| Folder | What goes here | Tooling | |
|---|---|---|---|
| π | class-content/ |
Lecture notes and assigned exercises | minimal |
| π§ͺ | playground/ |
Personal research, outside coursework | full |
| π | projects/ |
The graded college project | full |
Minimal is JDK 21, Maven, git and curl β nothing more, so nothing distracts from the exercise.
Full adds database clients (psql, mysql, redis-cli), network tools
(netcat, dig, ping), jq, httpie, ripgrep, tree, graphviz and
editors.
learning-spring-boot/
β
βββ π class-content/ Minimal environment
β βββ theory/ Lecture notes and reference material
β βββ exercises/ One Maven project per assignment
β βββ challange-01/ Games API β see its own README
β
βββ π§ͺ playground/ Full environment β throwaway experiments
β
βββ π projects/ Full environment β the graded project
β
βββ π³ docker/
β βββ Dockerfile Multi-stage: base Β· full Β· build Β· production
β βββ docker-compose.yml The three dev services + the production one
β βββ docker-compose.override.yml Optional NVIDIA GPU reservation
β
βββ βοΈ .devcontainer/
βββ class-content/ One devcontainer definition per environment
βββ playground/
βββ projects/
Every folder also carries a CLAUDE.md with instructions specific to it.
| Environment | App | Debugger | Docker stage | |
|---|---|---|---|---|
| π | class-content | 8082 | 5005 | base |
| π | projects | 8083 | 5007 | full |
| π¦ | projects (production image) | 8084 | β | production |
| π§ͺ | playground | 8085 | 5006 | full |
β οΈ Host ports vs. container portsThose are ports on your machine. Inside every container the app always listens on
8081and the debugger on5005. Nothing in the Java code or inapplication.propertiesmentions 8082/8083/8085 β the remap exists purely so all three can run at once, and is defined only indocker-compose.yml.The one place the host port matters is a front-end: the browser runs on your machine, so
fetchcalls and the API's CORS config must use the host port, not 8081.
- π³ Docker β Docker Desktop on Windows or macOS, Docker Engine on Linux
- π» VS Code with the Dev Containers extension, for the devcontainer workflow
Nothing else. Java and Maven live inside the containers β no local JDK needed.
- Open the repository in VS Code
- Run Dev Containers: Reopen in Container from the command palette (F1)
- Pick the environment you want to work in
- In the container terminal, start the app:
mvn spring-boot:runThe Java extensions and the Maven cache come preconfigured.
# Start all three dev environments
docker compose -f docker/docker-compose.yml up -d
# Open a shell in one of them
docker exec -it learning-spring-boot-projects bash
# Then, inside:
mvn spring-boot:runπ‘ Nothing starts by itself
Containers idle on purpose β they do not launch an app on their own, so you decide when something starts. They carry no healthcheck and simply show
Up; only the production service is health-checked.Closing a devcontainer window also leaves the containers running, so the other environments are not disturbed. Stop everything with
docker compose -f docker/docker-compose.yml down.
Run these inside a container, from the folder holding the pom.xml:
| Command | What it does |
|---|---|
mvn spring-boot:run |
Start the app on port 8081 (container-side) |
mvn test |
Run unit tests |
mvn verify |
Tests plus configured lint and coverage |
mvn clean package |
Build the jar into target/ |
mvn dependency:tree |
Inspect the dependency graph |
jshell |
Java REPL β quick experiments with no project |
From your machine:
| Command | What it does |
|---|---|
docker compose -f docker/docker-compose.yml up -d |
Start all environments |
docker compose -f docker/docker-compose.yml down |
Stop them |
docker compose -f docker/docker-compose.yml build --no-cache |
Rebuild images from scratch |
docker compose -f docker/docker-compose.yml logs -f projects |
Follow one service's logs |
docker ps |
See status and health of every container |
No installation needed β the JDWP agent ships with the JVM, so remote
debugging works in every environment, including the minimal one. This is unlike
Python, where you would pip install debugpy first.
Start the app with the debug flag:
mvn spring-boot:run -Dspring-boot.run.jvmArguments="$JAVA_DEBUG_OPTS"$JAVA_DEBUG_OPTS is preset in the image and opens port 5005 inside the
container. Then attach your IDE to the host debug port for that folder:
Also available inside any container, straight from the JDK:
| Tool | Use |
|---|---|
jdb |
Command-line debugger |
jstack <pid> |
Thread dump β find deadlocks |
jmap <pid> |
Heap dump β chase memory leaks |
jcmd <pid> Thread.print |
Live JVM commands |
jfr |
Java Flight Recorder β profiling |
Java has no system-installed equivalent of pytest or pylint. The tools
are Maven dependencies and plugins declared per project, which means adding
them never requires rebuilding the Docker image.
| Role | Java tool | Python analogue | Declared in |
|---|---|---|---|
| Test runner | JUnit 5 | pytest | <dependencies> |
| Assertions / mocks | AssertJ, Mockito | β | bundled with the starter |
| Controller tests | MockMvc | β | bundled with the starter |
| Real DBs in tests | Testcontainers | β | <dependencies> |
| Style | Checkstyle | pylint | <plugins> |
| Static analysis | SpotBugs, PMD | pylint | <plugins> |
| Coverage | JaCoCo | coverage.py | <plugins> |
JUnit, AssertJ, Mockito and MockMvc all arrive together through a single dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>mvn test # unit tests only
mvn verify # tests + lint + coverageprojects/ can also be built as a lean runtime image, for when the graded
project has to be delivered or deployed:
- πͺΆ JRE only, Alpine based β no Maven, no git, no compiler
- π Non-root user
- β‘ Starts the jar directly via
ENTRYPOINT, no build step at runtime - π§
-XX:MaxRAMPercentage=75.0, so the JVM respects the container's memory limit
It compiles during the image build, so it needs a working pom.xml in
projects/ first. Until then it stays behind a Compose profile and is skipped
by a plain up:
docker compose -f docker/docker-compose.yml --profile prod up projects-prodServed on host port 8084, and it is the only service that becomes healthy
without you starting anything.
One multi-stage Dockerfile produces every image. The stages inherit from each
other, so the JDK is installed once and shared:
base βββββββββββΊ full βββββββββββΊ build βββββββββββΊ production
JDK 21 + db clients copies source JRE only
Maven, git + net tools runs the build non-root
curl + jq, httpie⦠(internal) runs the jar
β² β² β²
β β β
class-content playground, projects projects-prod
Two details worth knowing before editing anything:
- π The build context is the project root, not
docker/.COPYpaths in the Dockerfile are relative to the root. - π
docker-compose.override.ymlis merged automatically by that exact filename. It only reserves an NVIDIA GPU and is inert without one. Skip it withdocker compose -f docker/docker-compose.yml up.
| Symptom | Cause | Fix |
|---|---|---|
port is already allocated |
Something on the host already uses 8082β8085 | Free the port, or change the host side in docker-compose.yml |
| Debugger will not attach | App started without the debug flag | Restart with -Dspring-boot.run.jvmArguments="$JAVA_DEBUG_OPTS" |
| Maven re-downloads everything | The maven-cache volume was removed |
Normal on first run; it repopulates |
| Front-end gets a CORS error | CORS allows 8081 instead of the host port | Allow http://localhost:8083 (or the relevant host port) |
Why different host ports instead of 8081 everywhere?
Two processes cannot bind the same host port. With all three mapped to
8081:8081, only one could run at a time and the others would fail with port
is already allocated. Splitting the host side lets you run an exercise and
your own version of it side by side β while inside the containers everything
stays on the canonical 8081.
Why one Dockerfile instead of one per folder?
All three environments are Java 21 + Spring Boot; they differ only in how much tooling is installed. Multi-stage handles exactly that: the JDK layer is built once and shared, and there is no risk of three files drifting to different Java versions. Splitting only makes sense when the runtimes genuinely diverge.
Why isn't Checkstyle or JUnit installed in the image?
In Java they cannot be. There is no apt install junit that a build would use
β they are artifacts Maven resolves from a pom.xml. Keeping them there means
adding a tool is a one-line edit with no image rebuild.
Why does class-content get a debug port if it is the "minimal" environment?
Because debugging costs nothing to include: JDWP is part of the JVM, not an add-on. And coursework exercises are exactly where stepping through a breakpoint teaches the most.
Do I need Java installed on my machine?
No. The JDK, Maven and every tool live inside the containers. Docker is the only requirement.