Commit graph

1 commit

Author SHA1 Message Date
Hermes Agent
da54a67d9d chore: make dev Dockerfiles self-contained, add bindless dev override
All checks were successful
CI / Lint, type check, unit tests, coverage (pull_request) Successful in 2m26s
CI / E2E browser tests (pull_request) Successful in 4m14s
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).
2026-06-17 09:44:18 +00:00