chore: make dev Dockerfiles self-contained, add bindless dev override
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).
This commit is contained in:
parent
aa2cb7c4a0
commit
da54a67d9d
4 changed files with 187 additions and 3 deletions
|
|
@ -1,6 +1,63 @@
|
||||||
|
# Exclude everything that isn't strictly needed to build or run the dev images.
|
||||||
|
# The dev Dockerfiles COPY . /app, so without this the image would bloat with
|
||||||
|
# docs, scripts, git history, etc.
|
||||||
|
|
||||||
|
# Build artifacts and caches (mounted as named volumes at runtime)
|
||||||
.gradle
|
.gradle
|
||||||
.env
|
|
||||||
.git
|
|
||||||
frontend/node_modules
|
|
||||||
backend/build
|
backend/build
|
||||||
|
frontend/dist
|
||||||
|
frontend/coverage
|
||||||
|
frontend/node_modules
|
||||||
|
backend/.gradle
|
||||||
|
|
||||||
|
# Test outputs
|
||||||
|
**/build/test-results
|
||||||
|
**/build/reports
|
||||||
|
**/coverage
|
||||||
|
**/.pytest_cache
|
||||||
|
frontend/playwright-report
|
||||||
|
frontend/test-results
|
||||||
|
|
||||||
|
# Local config and secrets
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
**/application-local.yml
|
||||||
|
|
||||||
|
# VCS and editor state
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
.github
|
||||||
|
.forgejo
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.iml
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Documentation (not needed at runtime)
|
||||||
|
README.md
|
||||||
|
REQUIREMENTS.md
|
||||||
|
AGENTS.md
|
||||||
|
CODING_GUIDELINES.md
|
||||||
|
docs/
|
||||||
|
|
||||||
|
# Ops scripts (not needed at runtime)
|
||||||
|
scripts/
|
||||||
|
|
||||||
|
# Test source dirs that aren't built into runtime artifacts
|
||||||
frontend/src/__tests__
|
frontend/src/__tests__
|
||||||
|
backend/src/test
|
||||||
|
|
||||||
|
# Docker-related metadata (not needed inside the running image)
|
||||||
|
Dockerfile*
|
||||||
|
.dockerignore
|
||||||
|
docker-compose*.yml
|
||||||
|
docker/
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
tmp/
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
106
docker-compose.dev-bindless.yml
Normal file
106
docker-compose.dev-bindless.yml
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
# Bindless dev stack — standalone variant of docker-compose.yml.
|
||||||
|
#
|
||||||
|
# Why this exists as a standalone file (not an override):
|
||||||
|
# Docker Compose merges `volumes:` by list concatenation, not by entry
|
||||||
|
# replacement, so an override can't drop the bind mounts from the base file —
|
||||||
|
# only append to them. A standalone file lets us redefine services with only
|
||||||
|
# the volumes we want.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker compose -f docker-compose.dev-bindless.yml up -d --build
|
||||||
|
#
|
||||||
|
# Use this when the Docker daemon can't bind-mount the host repo correctly:
|
||||||
|
# - Docker-in-Docker setups (e.g. this Hermes sandbox)
|
||||||
|
# - rootless Docker with restricted mount paths
|
||||||
|
# - Some CI runners
|
||||||
|
#
|
||||||
|
# For normal local dev, use docker-compose.yml — it bind-mounts the repo for
|
||||||
|
# Vite HMR and gradle bootRun hot reload.
|
||||||
|
#
|
||||||
|
# Trade-off vs. the bind-mounted dev compose:
|
||||||
|
# - The image is "frozen" at build time. Editing source on the host does not
|
||||||
|
# affect the running container. Edit + rebuild + restart, or run
|
||||||
|
# `docker compose up -d --build` after changes.
|
||||||
|
# - All source lives inside the image (docker/backend.Dockerfile and
|
||||||
|
# docker/frontend.Dockerfile COPY it in at build time).
|
||||||
|
#
|
||||||
|
# What you still get:
|
||||||
|
# - Gradle caches in named volumes (.gradle, backend/build, gradle-cache)
|
||||||
|
# so dependency downloads persist between `up` cycles.
|
||||||
|
# - Postgres data persists across `down` (via the pgdata volume).
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16
|
||||||
|
container_name: bilhej-postgres
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
mailpit:
|
||||||
|
image: ghcr.io/axllent/mailpit:v1.28
|
||||||
|
container_name: bilhej-mailpit
|
||||||
|
ports:
|
||||||
|
- "1025:1025"
|
||||||
|
- "8025:8025"
|
||||||
|
|
||||||
|
backend:
|
||||||
|
image: bilhej-backend-dev
|
||||||
|
build:
|
||||||
|
dockerfile: docker/backend.Dockerfile
|
||||||
|
context: .
|
||||||
|
container_name: bilhej-backend
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
SPRING_PROFILES_ACTIVE: docker
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
|
SWISH_NUMBER: ${SWISH_NUMBER}
|
||||||
|
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
|
||||||
|
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET}
|
||||||
|
STRIPE_PRICE_ID: ${STRIPE_PRICE_ID}
|
||||||
|
APP_PUBLIC_BASE_URL: ${APP_PUBLIC_BASE_URL:-http://localhost:3000}
|
||||||
|
MAIL_HOST: mailpit
|
||||||
|
MAIL_PORT: "1025"
|
||||||
|
MAIL_USERNAME: ""
|
||||||
|
MAIL_PASSWORD: ""
|
||||||
|
MAIL_FROM: ${MAIL_FROM:-noreply@bilhej.se}
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
mailpit:
|
||||||
|
condition: service_started
|
||||||
|
volumes:
|
||||||
|
- backend-gradle-project:/app/.gradle
|
||||||
|
- backend-build:/app/backend/build
|
||||||
|
- gradle-cache:/root/.gradle
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
image: bilhej-frontend-dev
|
||||||
|
build:
|
||||||
|
dockerfile: docker/frontend.Dockerfile
|
||||||
|
context: .
|
||||||
|
container_name: bilhej-frontend
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
|
gradle-cache:
|
||||||
|
backend-gradle-project:
|
||||||
|
backend-build:
|
||||||
|
|
@ -1,3 +1,15 @@
|
||||||
FROM eclipse-temurin:21-jdk
|
FROM eclipse-temurin:21-jdk
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy build configuration and wrapper first so this layer caches well.
|
||||||
|
COPY gradlew settings.gradle build.gradle ./
|
||||||
|
COPY gradle/ gradle/
|
||||||
|
RUN chmod +x gradlew
|
||||||
|
|
||||||
|
# Copy backend module. The dev compose overlays this with a host bind mount
|
||||||
|
# for live source changes; if the bind mount is absent (DinD, CI, k8s) the
|
||||||
|
# image is still self-contained and `gradlew :backend:bootRun` will work.
|
||||||
|
COPY backend/ backend/
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
ENTRYPOINT ["./gradlew", ":backend:bootRun", "--no-daemon"]
|
ENTRYPOINT ["./gradlew", ":backend:bootRun", "--no-daemon"]
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,16 @@
|
||||||
FROM node:24-alpine
|
FROM node:24-alpine
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies first so this layer caches independently of source changes.
|
||||||
COPY frontend/package.json frontend/package-lock.json ./
|
COPY frontend/package.json frontend/package-lock.json ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
|
# Copy the rest of the frontend. The dev compose overlays individual paths
|
||||||
|
# (./frontend/src, ./frontend/public, ./frontend/index.html) with host bind
|
||||||
|
# mounts for live reload; if those bind mounts are absent (DinD, CI, k8s)
|
||||||
|
# the image is still self-contained and `npm run dev` will serve from the
|
||||||
|
# COPY'd files.
|
||||||
COPY frontend/ .
|
COPY frontend/ .
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue