# ============================================================================= # BD FHIR National — HAPI Overlay Dockerfile # Multi-stage build: Maven builder + lean JRE runtime # # BUILD (CI machine): # docker build \ # --build-arg IG_PACKAGE=bd.gov.dghs.core-0.2.1.tgz \ # --build-arg BUILD_VERSION=1.0.0 \ # --build-arg GIT_COMMIT=$(git rev-parse --short HEAD) \ # -t your-registry.dghs.gov.bd/bd-fhir-hapi:1.0.0 \ # -f hapi-overlay/Dockerfile \ # . # # PUSH: # docker push your-registry.dghs.gov.bd/bd-fhir-hapi:1.0.0 # # The production server never builds — it only pulls. # The IG package.tgz must be present at: # hapi-overlay/src/main/resources/packages/${IG_PACKAGE} # before the build runs. CI pipeline is responsible for placing it there. # # IG VERSION UPGRADE: # 1. Drop new package.tgz into src/main/resources/packages/ # 2. Update IG_PACKAGE build arg to new filename # 3. Rebuild and push new image tag # 4. Redeploy via docker-compose pull + up # 5. Call cache flush endpoint (see ops/version-upgrade-integration.md) # ============================================================================= # ----------------------------------------------------------------------------- # STAGE 1: Builder # Uses full Maven + JDK image. Result discarded after JAR is built. # Only the fat JAR is carried forward to the runtime stage. # ----------------------------------------------------------------------------- FROM maven:3.9.6-eclipse-temurin-17 AS builder LABEL stage=builder WORKDIR /build # Copy parent POM first — allows Docker layer caching to skip dependency # download if only source code changes (not POM dependencies). COPY pom.xml ./pom.xml COPY hapi-overlay/pom.xml ./hapi-overlay/pom.xml # Download all dependencies into the Maven local repository cache layer. # This layer is invalidated only when a POM file changes. # On a CI machine with layer caching enabled, this saves 3-5 minutes # per build when only Java source changes. RUN mvn dependency:go-offline \ --batch-mode \ --no-transfer-progress \ -pl hapi-overlay \ -am # Now copy source — this layer changes on every code commit. COPY hapi-overlay/src ./hapi-overlay/src # Build fat JAR. Skip tests here — tests run in a separate CI stage # against TestContainers before the Docker build is invoked. # If your CI runs tests inside Docker, remove -DskipTests. RUN mvn package \ --batch-mode \ --no-transfer-progress \ -pl hapi-overlay \ -am \ -DskipTests \ -Dspring-boot.repackage.skip=false # Verify the fat JAR was produced with the expected name RUN ls -lh /build/hapi-overlay/target/bd-fhir-hapi.jar && \ echo "JAR size: $(du -sh /build/hapi-overlay/target/bd-fhir-hapi.jar | cut -f1)" # ----------------------------------------------------------------------------- # STAGE 2: Runtime # Lean JRE image — no JDK, no Maven, no build tools. # Attack surface reduced. Image size ~300MB vs ~800MB for builder. # ----------------------------------------------------------------------------- FROM eclipse-temurin:17-jre-jammy AS runtime # Build arguments — embedded in image labels for traceability. # Every production image must be traceable to a specific git commit # and IG version. If you cannot answer "what IG version is running", # you cannot validate your validation engine. ARG BUILD_VERSION=unknown ARG GIT_COMMIT=unknown ARG IG_PACKAGE=unknown ARG BUILD_TIMESTAMP # Set default build timestamp if not provided RUN if [ -z "${BUILD_TIMESTAMP}" ]; then BUILD_TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ); fi LABEL org.opencontainers.image.title="BD FHIR National HAPI Server" \ org.opencontainers.image.description="National FHIR R4 repository and validation engine, Bangladesh" \ org.opencontainers.image.vendor="DGHS/MoHFW Bangladesh" \ org.opencontainers.image.version="${BUILD_VERSION}" \ org.opencontainers.image.revision="${GIT_COMMIT}" \ bd.gov.dghs.ig.version="${IG_PACKAGE}" \ bd.gov.dghs.fhir.version="R4" \ bd.gov.dghs.hapi.version="7.2.0" # ----------------------------------------------------------------------------- # SYSTEM SETUP # ----------------------------------------------------------------------------- # Create non-root user. Running as root inside a container is a security # vulnerability — if the JVM is exploited, the attacker gets root on the host # if the container runs privileged or has volume mounts. RUN groupadd --gid 10001 hapi && \ useradd --uid 10001 --gid hapi --shell /bin/false --no-create-home hapi # Install curl for Docker health checks. # tini: init process to reap zombie processes and forward signals correctly. # Without tini, SIGTERM from docker stop is not forwarded to the JVM and # the container is killed after the stop timeout (ungraceful shutdown). RUN apt-get update && \ apt-get install -y --no-install-recommends \ curl \ tini \ && rm -rf /var/lib/apt/lists/* # Application directory WORKDIR /app # ----------------------------------------------------------------------------- # COPY ARTIFACTS FROM BUILDER # ----------------------------------------------------------------------------- COPY --from=builder /build/hapi-overlay/target/bd-fhir-hapi.jar /app/bd-fhir-hapi.jar # Set correct ownership — hapi user must be able to read the JAR RUN chown hapi:hapi /app/bd-fhir-hapi.jar # ----------------------------------------------------------------------------- # RUNTIME CONFIGURATION # ----------------------------------------------------------------------------- # Switch to non-root user before any further commands USER hapi # JVM tuning arguments. # These are defaults — override via JAVA_OPTS environment variable # in docker-compose.yml for environment-specific tuning. # # -XX:+UseContainerSupport # Enables JVM to read CPU/memory limits from cgroup (Docker constraints). # Without this, JVM reads host machine memory and over-allocates heap. # Available since Java 8u191 — always present in temurin:17. # # -XX:MaxRAMPercentage=75.0 # Heap = 75% of container memory limit. # For a 2GB container: heap = 1.5GB. Remaining 512MB for non-heap # (Metaspace, thread stacks, code cache, direct buffers). # HAPI 7.x with full IG loaded needs ~512MB heap minimum. # Recommended container memory: 2GB minimum, 4GB for production. # # -XX:+ExitOnOutOfMemoryError # Kill the JVM immediately on OOM instead of limping along in a broken # state. Docker will restart the container. Prefer clean restart over # degraded service. # # -Djava.security.egd=file:/dev/urandom # Prevents SecureRandom from blocking on /dev/random in containerised # environments where hardware entropy is limited. # Critical for JWT validation performance — Nimbus JOSE uses SecureRandom. ENV JAVA_OPTS="\ -XX:+UseContainerSupport \ -XX:MaxRAMPercentage=75.0 \ -XX:+ExitOnOutOfMemoryError \ -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/tmp/heapdump.hprof \ -Djava.security.egd=file:/dev/urandom \ -Dfile.encoding=UTF-8 \ -Duser.timezone=UTC" # Spring profile — overridable via environment variable in docker-compose ENV SPRING_PROFILES_ACTIVE=prod # FHIR server base URL — must match nginx configuration ENV HAPI_FHIR_SERVER_ADDRESS=https://fhir.dghs.gov.bd/fhir # Expose HTTP port. nginx terminates TLS and proxies to this port. # Do NOT expose this port directly — it must only be reachable via nginx. EXPOSE 8080 # Health check — used by Docker and docker-compose depends_on condition. # /actuator/health returns 200 when application is fully started and # all health indicators pass (including the custom AuditDataSourceHealthIndicator). # --fail-with-body: return non-zero exit on HTTP error responses. # start_period: allow 120s for startup (IG loading + Flyway migrations). HEALTHCHECK \ --interval=30s \ --timeout=10s \ --start-period=120s \ --retries=3 \ CMD curl --fail --silent --show-error \ http://localhost:8080/actuator/health/liveness || exit 1 # ----------------------------------------------------------------------------- # ENTRYPOINT # tini as PID 1 → JVM as child process. # tini handles SIGTERM correctly: forwards to JVM, waits for graceful # shutdown, then exits. Without tini, docker stop sends SIGTERM to PID 1 # (the JVM) but the JVM may ignore it depending on signal handling setup. # ----------------------------------------------------------------------------- ENTRYPOINT ["/usr/bin/tini", "--"] CMD ["sh", "-c", "exec java ${JAVA_OPTS} -jar /app/bd-fhir-hapi.jar"]