Deployment
Requirements checklistโ
- PostgreSQL running and accessible
-
FHIR_BASE_URL,ISSUER_URL,DB_URL,DB_USER,DB_PASSWORDset - Default passwords changed (
dr.smith,dr.jones) - HTTPS configured โ use a reverse proxy or
server.ssl.* - RSA key loaded from keystore (not generated at startup)
-
SmartDiscoveryProxyFilterregistered on HAPI FHIR server -
SmartScopeAuthorizationInterceptorregistered on HAPI FHIR server
Dockerโ
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/ajfhir-smart-auth-server-*.jar app.jar
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 9000
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]
mvn package -DskipTests
docker build -t ajfhir-smart-auth-server:latest .
docker run -p 9000:9000 \
-e FHIR_BASE_URL=http://hapi-fhir:8080/fhir \
-e ISSUER_URL=https://auth.yourplatform.com \
-e DB_URL=jdbc:postgresql://postgres:5432/smartfhir \
-e DB_USER=smartfhir \
-e DB_PASSWORD=yourpassword \
ajfhir-smart-auth-server:latest
Docker Compose โ auth server + PostgreSQLโ
# docker-compose.yml
version: "3.9"
services:
auth-server:
image: ajfhir-smart-auth-server:latest
ports:
- "9000:9000"
environment:
FHIR_BASE_URL: http://hapi-fhir:8080/fhir
ISSUER_URL: http://localhost:9000
DB_URL: jdbc:postgresql://postgres:5432/smartfhir
DB_USER: smartfhir
DB_PASSWORD: ${DB_PASSWORD}
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: smartfhir
POSTGRES_USER: smartfhir
POSTGRES_PASSWORD: ${DB_PASSWORD}
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U smartfhir"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:
Start:
export DB_PASSWORD=yourpassword
docker compose up
HTTPSโ
=== "Reverse proxy (recommended)"
server {
listen 443 ssl;
server_name auth.yourplatform.com;
ssl_certificate /etc/ssl/certs/auth.crt;
ssl_certificate_key /etc/ssl/private/auth.key;
location / \{
proxy_pass http://localhost:9000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
\}
}
Add to application.yml:
server:
forward-headers-strategy: framework
=== "Spring Boot SSL (direct)"
server:
port: 9443
ssl:
key-store: classpath:keystore.p12
key-store-password: $\{SSL_KEYSTORE_PASSWORD\}
key-store-type: PKCS12
RSA key persistenceโ
By default, the RSA-2048 signing key is generated at startup and lost on restart. Tokens issued before a restart cannot be verified after it.
For production, load a persistent key from a PKCS12 keystore:
Generate a persistent keystore:
keytool -genkeypair -alias ajfhir-smart-auth-server \
-keyalg RSA -keysize 2048 \
-storetype PKCS12 \
-keystore ajfhir-smart-auth-server.p12 \
-validity 3650
Load it in RsaKeyConfig:
@Bean
public RSAKey rsaKey(
@Value("${smart.server.keystore-path}") Resource keystorePath,
@Value("${smart.server.keystore-password}") String password,
@Value("${smart.server.key-alias}") String alias) throws Exception {
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(keystorePath.getInputStream(), password.toCharArray());
RSAPrivateKey privateKey = (RSAPrivateKey) ks.getKey(alias, password.toCharArray());
RSAPublicKey publicKey = (RSAPublicKey) ks.getCertificate(alias).getPublicKey();
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(alias)
.build();
}
Clustered deploymentsโ
For load-balanced deployments:
| Component | Change needed |
|---|---|
OAuth2AuthorizationService | Replace InMemoryOAuth2AuthorizationService with JdbcOAuth2AuthorizationService |
OAuth2AuthorizationConsentService | Replace with JdbcOAuth2AuthorizationConsentService |
| RSA keys | Load from shared keystore โ all instances must use the same key |
| Launch tokens | Already in PostgreSQL โ no change needed |
| Clinician sessions | Use Redis: spring.session.store-type: redis |
Production notesโ
DataInitializer seeds dr.smith / password and dr.jones / password on first startup.
Change these before any non-local deployment.
FHIR responses contain PHI. Set hapi.fhir.log-requests-and-responses: false.
Set logging.level.com.ajfhir.auth: WARN in production.
Before any real patient data flows through this server, a Business Associate Agreement must be signed with the hospital and any cloud/database provider.