207 lines
8.4 KiB
Docker
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"]
|