First-time host nginx setup needs HTTP-only vhost before certbot can
issue certs; the full bilhej.nginx.conf 443 block fails nginx -t until
those files exist.
- Add docker/bilhej.nginx.http.conf for ACME phase
- Reorder README one-time setup: HTTP vhost, certbot, then full config
The production backend Dockerfile was looking for Gradle files in a
backend/ subdirectory that doesn't exist in the repo structure:
- gradlew lives at repo root, not backend/gradlew
- gradle/ wrapper dir lives at repo root, not backend/gradle/
- settings.gradle lives at repo root, not backend/settings.gradle
Fixed by copying root-level Gradle files and placing backend-specific
files in the backend/ subdirectory. Also added :backend: subproject
prefix to Gradle tasks and corrected the output JAR path.
This fixes the deploy pipeline failure:
failed to calculate checksum: /backend/settings.gradle: not found
Add a manually-triggered deploy workflow that builds production Docker
images and starts the stack on the srvr.nu server.
- : workflow_dispatch with version input,
writes production .env from Forgejo secrets, builds and starts the
docker-compose.prod.yml stack, runs health checks via temporary curl
containers on the bilhej_default Docker network, tags the git commit.
- : nginx server block for bilhej.se.
Handles HTTP→HTTPS redirect, SSL termination with Let's Encrypt certs,
and proxies all traffic to the bilhej-frontend-prod container on the
Docker 'web' network. The frontend container handles /api/ proxying
to the backend internally.
To deploy:
1. Add production secrets to Forgejo (Settings → Actions → Secrets)
2. Trigger deploy from Actions → Deploy to Production
3. Run certbot for bilhej.se SSL (one-time setup)
4. Add docker/bilhej.nginx.conf to srvr.nu nginx container
5. Point bilhej.se DNS A record to srvr.nu IP
Three problems caused E2E browser tests to fail in Forgejo CI:
1. TypeScript build errors in (frontend.e2e.Dockerfile):
- used parameter property which violates
. Replaced with explicit property declaration.
- included in type-checking, causing
mock Response type mismatches. Added .
- mock Order was missing field.
2. Nginx SSL crash:
- copied production
which references SSL certs that don't exist in the e2e image.
- Replaced nginx entirely with (simpler, no SSL needed).
- Added to so routes to backend.
3. Docker context hygiene:
- excludes so test files don't
bloat the build context or trigger type errors in the container.
All other files untouched.
Implement per-job Docker-in-Docker (DinD) for E2E tests, giving each
job a completely isolated Docker daemon and network. This prevents
leakage to the host Docker or other containers.
The previous E2E approach failed because:
1. The Forgejo runner's container.docker_host was not set, causing
the runner itself to try unix:///var/run/docker.sock and crash-loop.
2. The host DinD daemon had isolated networking — job containers
running docker compose could not resolve 'dind' hostname or access
host filesystem bind mounts (e.g. .:/app).
New approach — zero bind mounts, all COPY-based images:
- docker/backend.e2e.Dockerfile: multi-stage build from repo root.
Copies gradlew + settings.gradle + backend/build.gradle to download
dependencies in a cacheable layer, then copies backend/src and builds
the bootJar. Runs the JAR directly on startup.
- docker/frontend.e2e.Dockerfile: multi-stage Node build → nginx.
Reuses existing docker/nginx.conf for /api proxy to backend service.
No volume mounts, fully self-contained.
- docker/playwright.e2e.Dockerfile: extends official Playwright image.
Installs deps from package-lock.json, copies e2e tests + config.
- docker-compose.e2e.yml: zero bind mounts. Services depend on each
other in order: postgres (healthy) → backend → frontend → playwright.
Playwright waits for backend and frontend via curl loops before
running tests.
- .forgejo/workflows/ci.yml: E2E job adds a 'dind' service container
(docker:28-dind, privileged, no TLS). The job sets DOCKER_HOST to
tcp://dind:2375 so the docker CLI inside the job talks to the
per-job DinD daemon. The compose file is docker-compose.e2e.yml.
- Runner fix on tocke: added container.docker_host: 'tcp://dind:2375'
to runner-config.yaml so the runner's own Docker client connects to
the host DinD container, stopping the crash loop.
Key properties:
- Network isolation: each E2E job gets its own DinD with its own
container network. No host container visibility.
- No bind mount leakage: all images use COPY instead of volume mounts.
The per-job DinD has its own filesystem and can't see host paths.
- Deterministic: builds start from clean state every time. Image cache
exists only within the per-job DinD lifetime.
- Lint-and-test job is untouched and remains green.
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.