# ============================================================================= # feature-container-runtime-contract — dev environment override # # Merge with base: # docker compose -f docker-compose.yml -f docker-compose.dev.yml up # # Dev intent: # - Relaxes read-only filesystem (writable, no tmpfs constraint) so developers # can hot-swap files without image rebuilds. # - Increases memory allowance for dev/debugging workloads. # - Disables restart-unless-stopped so crash loops don't mask startup errors. # - Enables JMX remote port (local only, never in prod). # - Mounts a local source volume for fast iteration (optional; mount when IDE # attaches to the running container). # ============================================================================= services: app: # Relax read-only constraint for local development. read_only: false # `!override`, not a plain empty list. An empty sequence merges with the base sequence rather # than replacing it, so the base's /var/tmp/heap tmpfs survived and collided with the bind mount # below — Compose refuses to have the same target twice and will not silently pick one. That is # the right refusal: a heap dump written into a tmpfs dies with the container that produced it, # which is the one moment somebody wants the file. # # `!override` needs Compose >= 2.24.4. Whether the collision is actually gone is checked in the # merged model rather than assumed from this line. tmpfs: !override [] # More memory for dev profiling / heap dumps. mem_limit: 1g memswap_limit: 1g environment: # Explicit, not inherited. A Compose profile selects services; it says nothing about which # environment the application believes it is in, and the two drifting is how a dev stack ends # up running local's settings. SPRING_PROFILES_ACTIVE: "dev" # The datasource address, owned here like the local and prod-smoke overlays own theirs. It was # the only one of the three missing, and the gap was invisible while the qualification wrapper # supplied a URL to every lane: the dev stack ran on a value that came from the test harness # rather than from the file that describes the dev environment. With the wrapper no longer # setting it — it was overriding prod's sslmode=verify-full URL — dev had none at all and # Flyway was handed the literal string "${APP_DATASOURCE_URL}". APP_DATASOURCE_URL: "jdbc:postgresql://db:5432/${POSTGRES_DB:-ca_skeleton}" TZ: "UTC" LANG: "C.UTF-8" LC_ALL: "C.UTF-8" # Use a shorter drain timeout in dev so restarts are faster. APP_SERVER_SHUTDOWN_TIMEOUT: "5s" APP_SERVER_SHUTDOWN: "graceful" # Remote JMX — bind to localhost only; NEVER expose in production. JAVA_TOOL_OPTIONS: >- -XX:MaxRAMPercentage=75 -XX:+UseContainerSupport -XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/tmp/heap -Dserver.tomcat.basedir=/tmp -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9999 -Dcom.sun.management.jmxremote.rmi.port=9999 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 ports: - "8080:8080" - "9001:9001" - "127.0.0.1:9999:9999" # JMX — localhost only # No health check constraint in dev; let the app start at its own pace. healthcheck: disable: true # Do not restart automatically so crash loops stay visible. restart: "no" # Optional: mount heap dump directory to host for dev analysis. # The base declares /var/tmp/heap as a tmpfs, which is right for an ephemeral runtime and wrong # for dev: a heap dump written into a tmpfs dies with the container that produced it, which is # the one moment somebody wants the file. Compose refuses to have both, and correctly — it will # not silently pick one — so the tmpfs list is replaced rather than appended to. # # `!override` needs Compose >= 2.24.4. An empty sequence is not assumed to delete the base # sequence by itself; scripts/verify-compose-profile-contracts.sh checks mount-target uniqueness # in the merged model, which is what actually proves the collision is gone. volumes: - type: bind source: ./tmp/heap-dumps target: /var/tmp/heap bind: create_host_path: true # The same network the shared infrastructure lives on. The local overlay joins it and the dev # overlay did not, so a dev lane that started PostgreSQL beside the application put the two on # different networks: `UnknownHostException: db`, from a container that was running and healthy # a metre away. Compose puts a service with no `networks:` on `default`, which is a network of # its own making — so the omission reads as a working stack until something has to resolve a # name across it. networks: - caskeleton-infra networks: # Defined in docker-compose.infra.yml, where the services that share it live. caskeleton-infra: external: false