Skip to main content

Deployment

Docker

Dockerfile
FROM eclipse-temurin:21-jdk-jammy AS build
WORKDIR /workspace
COPY pom.xml .
RUN mvn -B dependency:go-offline -q
COPY src ./src
RUN mvn -B package -DskipTests -q

FROM eclipse-temurin:21-jre-jammy AS runtime
RUN groupadd --system ajfhir && useradd --system --gid ajfhir ajfhir
WORKDIR /app
COPY --from=build /workspace/target/aj-fhir-immunization-*.jar app.jar
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8084/actuator/health || exit 1
USER ajfhir
EXPOSE 8084
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+UseG1GC"
ENTRYPOINT ["sh","-c","java $JAVA_OPTS -jar app.jar"]
# Build image
docker build -t ajfhir/immunization:1.0.0 .

# Run
docker run -p 8084:8084 \
-e SMART_CLIENT_ID=aj-fhir-immunization \
-e SMART_REDIRECT_URI=https://immunization.your-hospital.org/callback \
-e AUTH_SERVER_URL=https://auth.your-hospital.org \
-e FHIR_BASE_URL=https://fhir.your-hospital.org/fhir \
ajfhir/immunization:1.0.0

Docker Compose — full platform

docker-compose.yml
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: smartfhir
POSTGRES_USER: ${DB_USER:-smartfhir}
POSTGRES_PASSWORD: ${DB_PASSWORD:?required}
volumes: [postgres_data:/var/lib/postgresql/data]
networks: [smart-net]

hapi-fhir:
image: hapiproject/hapi:v7.4.0
ports: ["8080:8080"]
networks: [smart-net]

auth-server:
image: ajfhir/smart-auth-server:1.0.0
ports: ["9000:9000"]
environment:
DB_URL: jdbc:postgresql://postgres:5432/smartfhir
DB_USER: ${DB_USER:-smartfhir}
DB_PASSWORD: ${DB_PASSWORD}
FHIR_BASE_URL: http://hapi-fhir:8080/fhir
ISSUER_URL: http://auth-server:9000
depends_on: [postgres]
networks: [smart-net]

immunization:
image: ajfhir/immunization:1.0.0
ports: ["8084:8084"]
environment:
SMART_CLIENT_ID: aj-fhir-immunization
SMART_REDIRECT_URI: http://localhost:8084/callback
AUTH_SERVER_URL: http://auth-server:9000
FHIR_BASE_URL: http://hapi-fhir:8080/fhir
depends_on: [hapi-fhir, auth-server]
healthcheck:
test: ["CMD","curl","-f","http://localhost:8084/actuator/health"]
interval: 30s
start_period: 60s
networks: [smart-net]

volumes:
postgres_data:
networks:
smart-net:

HTTPS

Epic App Orchard and most EHR integrations require HTTPS for the redirect URI. Terminate TLS at a reverse proxy:

nginx.conf
server {
listen 443 ssl;
server_name immunization.your-hospital.org;

ssl_certificate /etc/ssl/certs/cert.pem;
ssl_certificate_key /etc/ssl/private/key.pem;

location / {
proxy_pass http://localhost:8084;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $host;
}
}

Add to application.yml so Spring generates correct redirect URIs:

server:
forward-headers-strategy: native

Redis session store

For multi-instance deployments, all instances must share session state:

application.yml
spring:
session:
store-type: redis
timeout: 3600s
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}

Add to pom.xml:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>

All session objects implement Serializable with pinned serialVersionUID = 1L.

Environment variables reference

VariableDefaultDescription
SMART_CLIENT_IDaj-fhir-immunizationOAuth2 client ID on the auth server
SMART_REDIRECT_URIhttp://localhost:8084/callbackMust match auth server registration
AUTH_SERVER_URLhttp://localhost:9000Auth server base URL
FHIR_BASE_URLhttp://localhost:8080/fhirHAPI FHIR JPA server base URL
REDIS_HOSTlocalhostRedis host (only needed with session store-type: redis)
REDIS_PASSWORDRedis auth password

Health check

curl http://localhost:8084/actuator/health
{"status": "UP"}

Available actuator endpoints: /actuator/health, /actuator/info, /actuator/metrics.


Next: Production Checklist →