chore: make dev Dockerfiles self-contained, add bindless dev override #10

Merged
jocke merged 2 commits from chore/dockerfile-self-contained into master 2026-06-17 10:34:03 +00:00
Collaborator

Why

The dev 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 dev images themselves are empty of source — there are no COPY lines in the dev Dockerfiles.

Approach

Make the dev images self-sufficient by COPY'ing the source at build time. The compose bind mount is kept (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 ./, COPY gradle/ gradle/, RUN chmod +x gradlew, COPY backend/ backend/, EXPOSE 8080. ENTRYPOINT unchanged.
  • docker/frontend.Dockerfile — already COPY'd the source; add EXPOSE 3000 and comments documenting the two-stage COPY pattern.
  • .dockerignore — expand to exclude docs, scripts, git, editor config, build outputs, test results, logs, env files, docker metadata. Cuts build context from MBs to ~800 KB.
  • 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.
  • e2e compose (docker-compose.e2e.yml, docker/*.e2e.Dockerfile) — unchanged. Were already self-contained.

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 (or ~200 MB frontend 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.

Usage

Environment Command
Normal local dev (HMR) docker compose up -d
DinD / sandbox / CI docker compose -f docker-compose.dev-bindless.yml up -d --build
Full E2E test suite docker compose -f docker-compose.e2e.yml up -d (unchanged)

Verified

  • Hermes DinD sandbox: bindless dev stack brings up postgres + mailpit + backend + frontend with no bind mounts. Spring Boot starts in ~6 s, Vite in ~700 ms. Backend serves real API responses (GET /api/vehicles/ABC123 → 404 Inget fordon hittades).
  • Hermes DinD sandbox: e2e stack runs all 90/90 Playwright tests in 54 s, 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.

Risk

Low. The dev compose (docker-compose.yml) is byte-for-byte unchanged — every existing user on a normal host sees the same workflow. The change is purely additive (one new compose file, plus image-content additions that get overlaid by the existing bind mount). E2E suite, prod stack, CI pipeline all unchanged.

Refs: project AGENTS.md Docker section, gradle check pre-commit hook.

## Why The dev `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 dev images themselves are empty of source — there are no `COPY` lines in the dev Dockerfiles. ## Approach Make the dev images self-sufficient by `COPY`'ing the source at build time. The compose bind mount is kept (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 ./`, `COPY gradle/ gradle/`, `RUN chmod +x gradlew`, `COPY backend/ backend/`, `EXPOSE 8080`. ENTRYPOINT unchanged. - **`docker/frontend.Dockerfile`** — already COPY'd the source; add `EXPOSE 3000` and comments documenting the two-stage COPY pattern. - **`.dockerignore`** — expand to exclude docs, scripts, git, editor config, build outputs, test results, logs, env files, docker metadata. Cuts build context from MBs to ~800 KB. - **`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. - **e2e compose** (`docker-compose.e2e.yml`, `docker/*.e2e.Dockerfile`) — unchanged. Were already self-contained. ## 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 (or ~200 MB frontend 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. ## Usage | Environment | Command | |---|---| | Normal local dev (HMR) | `docker compose up -d` | | DinD / sandbox / CI | `docker compose -f docker-compose.dev-bindless.yml up -d --build` | | Full E2E test suite | `docker compose -f docker-compose.e2e.yml up -d` (unchanged) | ## Verified - **Hermes DinD sandbox**: bindless dev stack brings up postgres + mailpit + backend + frontend with no bind mounts. Spring Boot starts in ~6 s, Vite in ~700 ms. Backend serves real API responses (`GET /api/vehicles/ABC123 → 404 Inget fordon hittades`). - **Hermes DinD sandbox**: e2e stack runs all **90/90 Playwright tests in 54 s**, 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. ## Risk Low. The dev compose (`docker-compose.yml`) is **byte-for-byte unchanged** — every existing user on a normal host sees the same workflow. The change is purely additive (one new compose file, plus image-content additions that get overlaid by the existing bind mount). E2E suite, prod stack, CI pipeline all unchanged. Refs: project `AGENTS.md` Docker section, `gradle check` pre-commit hook.
hermes added 1 commit 2026-06-17 09:45:33 +00:00
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
da54a67d9d
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).
hermes added 1 commit 2026-06-17 10:29:48 +00:00
fixup: keep docker/*.conf and docker/entrypoint.sh in build context
All checks were successful
CI / Lint, type check, unit tests, coverage (pull_request) Successful in 2m42s
CI / E2E browser tests (pull_request) Successful in 3m27s
3d2db1471f
Review feedback on PR #10: excluding the whole docker/ directory broke
frontend.prod.Dockerfile, which copies docker/nginx.conf and
docker/entrypoint.sh into the production nginx image.

- Replace docker/ with docker/*.Dockerfile so only the Dockerfiles are
  removed from the build context.
- Restore docker-compose*.yml exclusion.
- Correct the header comment to reflect that dev Dockerfiles COPY source
  subpaths, not the entire repo root.

Verified: docker compose -f docker-compose.prod.yml build frontend
succeeds and both COPY docker/... steps complete.
Author
Collaborator

Tack för reviewen — bra catch.

Pushed en fixup:

  • Ersatte docker/ med docker/*.Dockerfile så att docker/nginx.conf och docker/entrypoint.sh fortfarande finns i build-context för frontend.prod.Dockerfile.
  • Lade tillbaka docker-compose*.yml i exkluderingen.
  • Rättade header-kommentaren så den inte påstår att dev-Dockerfilerna gör COPY . /app.

Verifierat: docker compose -f docker-compose.prod.yml build frontend går igenom och båda COPY docker/...-stegen slutförs.

Kommit: 3d2db14

Tack för reviewen — bra catch. Pushed en fixup: - Ersatte `docker/` med `docker/*.Dockerfile` så att `docker/nginx.conf` och `docker/entrypoint.sh` fortfarande finns i build-context för `frontend.prod.Dockerfile`. - Lade tillbaka `docker-compose*.yml` i exkluderingen. - Rättade header-kommentaren så den inte påstår att dev-Dockerfilerna gör `COPY . /app`. Verifierat: `docker compose -f docker-compose.prod.yml build frontend` går igenom och båda `COPY docker/...`-stegen slutförs. Kommit: `3d2db14`
jocke merged commit 5335ba4f12 into master 2026-06-17 10:34:03 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: jocke/bilhej#10
No description provided.