Why
---
The dev compose (docker-compose.yml) assumes the Docker daemon can bind-mount
the host repo (and several subpaths) at runtime, providing live source for
`gradle :backend:bootRun` and Vite HMR. That works on a normal Linux/macOS
host but breaks in:
- Docker-in-Docker setups (e.g. the Hermes sandbox used for agent work)
- rootless Docker with restricted mount paths
- some CI runners
The failure mode is the daemon's mount namespace only sees compose-created
named-volume subdirs at the bind source, not the real repo files. The
backend then fails with `stat ./gradlew: no such file or directory` and
the frontend fails with `mount src=.../index.html, dst=.../index.html
... not a directory`. The image itself is empty of source — there are no
`COPY` lines in the dev Dockerfiles.
Approach
--------
Make the dev images self-sufficient by COPYing the source at build time.
The compose bind mount is kept (it's still the right thing for normal
local dev with HMR), but it's no longer load-bearing. The image works
standalone in any environment.
Add a separate `docker-compose.dev-bindless.yml` for environments where
host bind mounts can't be used (DinD, CI, restricted Docker). It uses
the same images (COPY'd source) but redefines the services with no
host bind mounts — only the named cache volumes remain, so gradle and
Vite caches persist between `up` cycles.
Compose merge semantics caveat: `volumes:` lists merge by concatenation,
not by entry replacement, so the bindless workflow can't be expressed as
a compose override on top of docker-compose.yml. A standalone file is
required.
Changes
-------
* docker/backend.Dockerfile
- Add `COPY gradlew settings.gradle build.gradle ./`
- Add `COPY gradle/ gradle/`
- Add `RUN chmod +x gradlew`
- Add `COPY backend/ backend/`
- Add `EXPOSE 8080`
- Keep ENTRYPOINT unchanged.
- New image is runnable with `docker run bilhej-backend-dev` (no bind
mount needed) and works under `docker compose up -d` on any host.
* docker/frontend.Dockerfile
- Add comments documenting the two-stage COPY pattern (deps first for
layer cache, then full source).
- Keep the existing structure — it already COPYs the source, just
wasn't being relied on. Now bind-mount failures (e.g. index.html
type mismatch in DinD) don't kill the container; the COPY'd file
is already in place.
- Add `EXPOSE 3000` (was missing).
* .dockerignore
- Expand to exclude everything that isn't strictly needed at build or
run time: docs, scripts, git, editor config, build outputs, test
results, logs, env files, docker-related metadata, etc.
- Cuts the build context from ~MBs to ~800 KB (verified).
- Image contents are now: gradlew + wrapper, build.gradle, settings,
gradle/, backend/ (for backend image); package.json, package-lock,
src/, public/, index.html, node_modules (for frontend image).
* docker-compose.dev-bindless.yml (new)
- Standalone variant of docker-compose.yml with all host bind mounts
removed. Same service definitions, same image tags, same env vars,
same named cache volumes (pgdata, gradle-cache, backend-gradle-
project, backend-build). Only differences: no `.:/app`, no
`./frontend/src:/app/src`, no `./frontend/public:/app/public`, no
`./frontend/index.html:/app/index.html`.
- Usage: `docker compose -f docker-compose.dev-bindless.yml up -d`
(no `--build` needed if images already exist; include `--build`
on first run or after pulling changes).
- Trade-off vs the default dev compose: image is "frozen" at build
time, so editing source on the host doesn't trigger HMR. Edit +
`docker compose up -d --build` (or just rebuild the relevant
service) to pick up changes. Named cache volumes still keep
gradle/npm caches warm across rebuilds.
* e2e compose (docker-compose.e2e.yml, docker/*.e2e.Dockerfile) —
unchanged. They were already self-contained and continue to work as
before. Verified by running the full 90/90 Playwright suite in 54s.
Compatibility with existing dev workflow
----------------------------------------
On a normal host where bind mounts work (the common case):
- `docker compose up -d` (the existing command) keeps working
unchanged. The bind mount on `.:/app` overlays the COPY'd source
at runtime, so HMR and `gradle :backend:bootRun` hot-reload work
exactly like before.
- Image size grows (~50 MB backend, ~50 MB frontend on top of base
image; ~200 MB including node_modules). Acceptable for dev.
- First-time `docker compose build` is slightly slower because it
has to COPY the source. Subsequent builds cache well: the COPY
layer invalidates only when source files change.
Verified
--------
- Hermes DinD sandbox: bindless dev stack (`docker-compose.dev-
bindless.yml`) brings up postgres + mailpit + backend + frontend
with no bind mounts. Spring Boot starts in ~6s, Vite dev server
in ~700ms. Backend serves real API responses
(`GET /api/vehicles/ABC123 -> 404 Inget fordon hittades`).
- Hermes DinD sandbox: e2e stack runs all 90 Playwright tests in
~54s, identical to pre-patch behavior.
- Docker image self-sufficiency: `docker run --rm bilhej-backend-dev`
and `docker run --rm bilhej-frontend-dev` both work without any
bind mounts.
Refs: project AGENTS.md (Docker section, gradle check pre-commit).
Move gradlew, gradle/wrapper, and settings.gradle from backend/ to
the repo root so build commands run from the top-level directory.
This follows the standard multi-project Gradle layout where the build
tool lives alongside docker-compose.yml and all submodules.
- Move gradlew + gradle/wrapper/* from backend/ to repo root
- Move settings.gradle to root with rootProject.name and include 'backend'
- Create root build.gradle with convenience tasks: check, up, down, reset
- check task chains frontend lint → frontend test → backend check
- Update docker-compose.yml backend volume from ./backend:/app to .:/app
- Update backend.Dockerfile entrypoint to ./gradlew :backend:bootRun
- Update AGENTS.md: document ./gradlew check, up, down, reset
- Delete backend/settings.gradle (now at root)
- Add .gradle/ and build/ to .gitignore
- Add !gradle/wrapper/gradle-wrapper.jar exception (blocked by *.jar rule)
All 38 frontend tests and 33 backend tests pass via ./gradlew check.