Files
bd-fhir-national/hapi-overlay/Dockerfile
2026-03-16 00:02:58 +06:00

207 lines
8.4 KiB
Docker

# =============================================================================
# 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"]