Troubleshooting — MotorPH Enterprise Payroll System
Self-service reference for Docker Compose and runtime issues. Written so a new dev can get unstuck without asking anyone: start with the triage flow, then jump to your symptom in the index.
Start here: triage in order
When anything is wrong with the stack, run these three commands before guessing — between them they diagnose nearly every issue on this page:
# 1. What state is everything in? Backend must show "(healthy)".
docker compose ps
# 2. If a container is restarting/unhealthy/exited — read its logs. The
# backend logs the exact reason it refuses to boot (JWT, Flyway, ...).
docker compose logs motorph_payroll_backend --tail=50
# 3. What config is the stack ACTUALLY running with? (.env + any local
# override file merged) — catches stale env values and override surprises.
docker compose config | less
Then find your symptom below.
Symptom index
| Symptom | Jump to |
|---|---|
permission denied ... docker.sock or Cannot connect to the Docker daemon | Docker daemon unreachable |
bind: address already in use on 5173/8081/5434 | Port already in use |
network <hash> not found | Stale network |
| Backend container keeps restarting | Backend keeps restarting |
JWT_SECRET is still set to the insecure placeholder | JWT error |
Detected applied migration not resolved locally | Flyway history mismatch |
Found more than one migration with version N | Duplicate migration version |
| Blank page / API errors right after startup | Frontend before backend |
| Healthcheck never turns healthy | Healthcheck failing |
no space left on device during build | Disk full |
| Edited frontend code, browser shows old version | Static build |
| CORS errors in the browser console | CORS |
| 429 / account locked while testing logins | Rate limit & lockout |
| Can't log in at all — which accounts exist? | reference/demo-users.md |
| App "sent" an email but nothing arrived | Emails go to Mailpit |
| E2E fails with HTTP 428 at login | ALTCHA |
Playwright: Executable doesn't exist | Playwright browsers |
| E2E failures that make no sense | Concurrent runs |
| Flyway ignores/skips migrations mysteriously | Local override file |
Monitoring containers survive docker compose down | Monitoring overlay |
| Grafana login rejected / dashboards missing | Monitoring overlay |
| Nav items missing for a role after an RBAC migration | RBAC sync |
Getting the stack up
Docker daemon unreachable or permission denied
permission denied while trying to connect to the Docker daemon socket
Cannot connect to the Docker daemon at unix:///var/run/docker.sock
Cause: either the Docker daemon isn't running, or your user isn't allowed to talk to it.
Fix:
# Daemon not running (Linux):
sudo systemctl start docker
# Permission denied — add yourself to the docker group, then log out/in
# (or run `newgrp docker` for the current shell):
sudo usermod -aG docker $USER
On Docker Desktop (macOS/Windows), just make sure the Desktop app is running.
Port already in use
Error ... bind: address already in use
Cause: something on your machine already listens on 5173, 8081, 5434, 8025, or (with the monitoring overlay) 3000/9090/9093.
Fix: find the squatter, or move ours — all host ports come from .env:
# who owns the port?
ss -ltnp | grep 5173
# or move the stack's ports in .env:
FRONTEND_PORT=5174
BACKEND_PORT=8082
POSTGRES_PORT=5435
If you change FRONTEND_PORT, add the new origin to CORS_ALLOWED_ORIGINS in
.env too, then docker compose up -d.
network <hash> not found
Error response from daemon: failed to set up container networking:
network 74f42806d... not found
Cause: Docker stored a network by its auto-generated hash. After a daemon restart or unclean shutdown the hash is gone but containers still reference it.
Fix:
docker compose down --remove-orphans
docker compose up -d --build
If the error persists, prune dangling networks:
docker network prune
docker compose up -d --build
Backend keeps restarting (restarting state)
Check the logs first:
docker compose logs motorph_payroll_backend --tail=50
Then consult the specific error below.
Backend crash-loops right after up with a JWT error
IllegalStateException: JWT_SECRET is still set to the insecure placeholder default
Cause: .env still has JWT_SECRET=change-me-in-production. The backend
refuses to start with the placeholder, a blank value, or anything under 32
characters (JwtProperties.validateSecret).
Fix:
sed -i "s/^JWT_SECRET=.*/JWT_SECRET=$(openssl rand -hex 64)/" .env
docker compose up -d
Flyway: Detected applied migration not resolved locally: 20
Validate failed: Migrations have failed validation
Detected applied migration not resolved locally: 20.
If you removed this migration intentionally, run repair to mark the migration as deleted.
Cause: The database has a record of migration V20 in flyway_schema_history, but there is no V20 file in the backend's migration folder (e.g., it was renamed to V25). Flyway refuses to start until the history matches the local files.
Fix A — Remove the stale history row (preserves data):
docker compose exec motorph_payroll_db psql -U motorph -d motorph -c \
"DELETE FROM flyway_schema_history WHERE version = '20';"
docker compose restart motorph_payroll_backend
Fix B — Full clean start (wipes all data):
docker compose down --volumes
docker compose up -d --build
Use Fix B when you don't need the existing data (fresh dev environment).
Flyway: Found more than one migration with version 20
Found more than one migration with version 20
Offenders:
→ /demo-seed/V20__rich_seed_employees.sql
→ .../db/migration/V20__message_cursor_index.sql
Cause: Running the demo compose (docker-compose.demo.yml) mounts extra seed files that conflict with a migration of the same version in the backend.
Fix: Check the version numbers of all demo seed files (demo/seed/V*.sql) and ensure no application migration shares the same version number. Rename the conflicting application migration to the next available version.
Frontend starts before backend is ready
Symptom: Blank page or API errors on first load right after docker compose up.
Cause: The old compose didn't wait for Spring Boot to finish starting before nginx launched.
Status: Fixed in docker-compose.yml. The frontend now uses depends_on: condition: service_healthy and waits for the backend healthcheck to pass (~90 seconds on first boot due to Flyway migrations).
If you still see this, check backend health:
docker compose ps
# motorph_payroll_backend should show "(healthy)" before frontend starts
Backend healthcheck always failing
docker inspect motorph_payroll_backend | grep -A5 Health
If the health status is unhealthy, check if the container can reach its own port:
docker compose exec motorph_payroll_backend \
wget -qS -O /dev/null http://localhost:8080/ 2>&1 | head -3
Expected output: a line beginning with HTTP/1.1 (any status code is fine — even 401 means the server is up).
no space left on device during docker build
Cause: repeated --build runs accumulate old images, build cache, and
dangling layers; Docker's disk fills up long before yours does.
Fix:
docker system df # see what's eating the space
docker system prune # removes stopped containers, dangling images, build cache
docker builder prune # bigger hammer for build cache alone
docker system prune --volumes also deletes unused volumes — your running
stack's data volume is in use and survives, but stopped stacks' data does not.
Run it only if you don't need any stopped stack's data.
Runtime issues
Frontend change doesn't show up in the browser
Cause: The frontend image is a static build — nginx serves the dist/
bundle produced at docker build time. There is no hot reload in the Docker
stack, and VITE_API_URL is baked in at build time too.
Fix: rebuild the image (docker compose up -d --build motorph_payroll_frontend),
or use the Vite dev-server loop for iteration (onboarding.md §5).
CORS errors in the browser console (host-dev only)
Cause: You're running the Vite dev server (or changed FRONTEND_PORT), so
the browser origin is no longer in the backend's CORS_ALLOWED_ORIGINS. The
dockerized nginx setup never hits CORS because everything is same-origin.
Fix: add your dev origin to CORS_ALLOWED_ORIGINS in .env and
docker compose up -d motorph_payroll_backend.
429 Too Many Requests / account locked (423) while testing logins
Cause: The login rate limiter (Bucket4j, 10 requests/min per IP) or the exponential account lockout (5 failures → 15 min, doubling). Both are deliberate (security/authentication.md).
Fix: wait it out, or — since both are in-memory — restart the backend:
docker compose restart motorph_payroll_backend. The e2e helpers already
cache logins per worker to stay under the limit.
Emails never arrive
Symptom: the app says it sent a signup code / welcome mail / payslip notification, but no inbox ever receives it.
Cause: that's by design in dev — the stack sends all mail to the local Mailpit catcher, never to the internet.
Fix: open http://localhost:8025 — every email is there. If Mailpit shows
nothing either, check that MAIL_ENABLED=true in .env and that the backend
log doesn't show a send error. Details: backend/email.md.
Flyway behaves differently than the migration folder suggests
Cause: A local, gitignored docker-compose.override.yml can inject
Flyway settings (e.g. SPRING_FLYWAY_IGNORE_MIGRATION_PATTERNS) without
anything in the repo showing it — Docker Compose merges it automatically.
Fix: run docker compose config | grep -i flyway to see what the backend
actually receives; delete or adjust the override file.
Test-suite issues
E2E suite fails at login with HTTP 428
Cause: ALTCHA is enabled; the Playwright helpers post credentials directly
to /api/auth/login without a proof-of-work payload.
Fix: keep ALTCHA_ENABLED=false (the .env.example default) on any stack
you run the e2e suite against. See e2e/README.md.
Playwright: Executable doesn't exist
browserType.launch: Executable doesn't exist at ~/.cache/ms-playwright/...
Cause: the Playwright npm package is installed but the browser binaries aren't — they're a separate download.
Fix (from the repo root):
npx playwright install --with-deps chromium
E2E failures that make no sense
Symptom: tests that pass in isolation fail in a full run — wrong row counts, entities that "shouldn't exist", flaky assertions.
Likely causes, in order:
- Someone else's run is hitting the same stack. The suite runs against a
shared live database; two concurrent runs race each other. Check
ps aux | grep playwrightbefore starting a full run. - Seed data has drifted. The specs assume the seed baseline documented in
e2e/README.md; a stack where you've been creating
entities by hand will fail assertions. Reset with
docker compose down --volumes && docker compose up -d --build. - Your code actually broke something. Only conclude this after ruling out 1 and 2.
Monitoring overlay issues
The observability stack (deployment/monitoring.md) is an overlay — most confusion comes from forgetting that it's a second compose file layered on the first.
-
Monitoring containers survive
docker compose down: any command that should affect the overlay needs both-fflags —docker compose -f docker-compose.yml -f docker-compose.monitoring.yml downA single-file
downconsiders the monitoring containers orphans and leaves them running (add--remove-orphansto sweep them up). -
Grafana login rejected: the default is
admin/admin, overridden byGRAFANA_ADMIN_USER/GRAFANA_ADMIN_PASSWORDin.env. The admin password is set on first boot of the volume — changing.envlater doesn't change an existing Grafana's password (reset by removing thegrafana_datavolume, or change it inside Grafana). -
Grafana/Prometheus unreachable from another machine: intentional. All monitoring UIs bind to
127.0.0.1because Docker's port publishing bypasses UFW. Use an SSH tunnel (ssh -L 3000:localhost:3000 user@host) rather than changingMONITORING_BIND. -
Dashboard edits disappear: dashboards are provisioned read-only from
infra/monitoring/grafana/dashboards/and re-read every 30 s; UI edits are refused because the next reload would discard them. Edit the JSON on disk, or use Save As in Grafana for scratch work.
Useful Commands
# Bring everything up (normal workflow)
docker compose up -d --build
# View logs for all services
docker compose logs -f
# View logs for one service
docker compose logs -f motorph_payroll_backend
# Check container health and status
docker compose ps
# Restart only the backend (e.g., after config change)
docker compose restart motorph_payroll_backend
# Stop all containers (keeps volumes/data)
docker compose down
# Stop and remove orphan containers from old compose runs
docker compose down --remove-orphans
# Full reset — removes containers AND all data volumes
docker compose down --volumes
# See the effective merged config (env + overrides) the stack runs with
docker compose config
# Connect to the database directly
docker compose exec motorph_payroll_db psql -U motorph -d motorph
# View Flyway migration history in the DB
docker compose exec motorph_payroll_db psql -U motorph -d motorph \
-c "SELECT version, description, success, installed_on FROM flyway_schema_history ORDER BY installed_rank;"
Nav items missing after an RBAC migration
Symptom (original case, 2026-06-27): Logged in as Warehouse Manager, the Inventory sidebar does not show Transfers, Serial Numbers, or Lot Numbers, even though the backend migrations (V21__serial_lot_rbac.sql, V24__warehouse_transfer_rbac.sql) correctly insert the permissions and assign them to the role.
Root cause: The frontend resolves permissions entirely from the static ROLE_PERMISSIONS map in frontend/src/constants/role-permissions.ts — it does not fetch them from the API or JWT at runtime. When INVENTORY_SERIALS_VIEW, INVENTORY_SERIALS_MANAGE, INVENTORY_TRANSFERS_VIEW, and INVENTORY_TRANSFERS_MANAGE were introduced in later migrations, they were never added to the 'Warehouse Manager' (and 'System Administrator') entries in that file. As a result, hasPermission() returned false and the nav items were filtered out.
Fix: Added the four missing constants to both roles in frontend/src/constants/role-permissions.ts:
// added after INVENTORY_RECEIVING_MANAGE for both 'Warehouse Manager' and 'System Administrator'
PermissionConstants.INVENTORY_SERIALS_VIEW,
PermissionConstants.INVENTORY_SERIALS_MANAGE,
PermissionConstants.INVENTORY_TRANSFERS_VIEW,
PermissionConstants.INVENTORY_TRANSFERS_MANAGE,
Prevention: Whenever a new Flyway RBAC migration adds permissions to a role, the matching constants must also be added to frontend/src/constants/role-permissions.ts. The two sources must stay in sync manually — there is no runtime sync between them.
Where the architecture details went
The request-routing and startup-order diagrams that used to live here moved to
deployment/docker.md, next to the rest of the compose
documentation. Short version: the browser only ever talks to nginx on :5173,
which proxies /api/ and /ws to the backend over the internal Docker
network; startup order is db → backend (healthy after ~90s on first boot) →
frontend, gated by healthchecks.