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
| Variable | Default | Description |
|---|---|---|
SMART_CLIENT_ID | aj-fhir-immunization | OAuth2 client ID on the auth server |
SMART_REDIRECT_URI | http://localhost:8084/callback | Must match auth server registration |
AUTH_SERVER_URL | http://localhost:9000 | Auth server base URL |
FHIR_BASE_URL | http://localhost:8080/fhir | HAPI FHIR JPA server base URL |
REDIS_HOST | localhost | Redis host (only needed with session store-type: redis) |
REDIS_PASSWORD | — | Redis auth password |
Health check
curl http://localhost:8084/actuator/health
{"status": "UP"}
Available actuator endpoints: /actuator/health, /actuator/info, /actuator/metrics.
Next: Production Checklist →