Skip to main content

Deployment

Requirements checklistโ€‹

  • PostgreSQL running and accessible
  • FHIR_BASE_URL, ISSUER_URL, DB_URL, DB_USER, DB_PASSWORD set
  • 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)
  • SmartDiscoveryProxyFilter registered on HAPI FHIR server
  • SmartScopeAuthorizationInterceptor registered 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:

ComponentChange needed
OAuth2AuthorizationServiceReplace InMemoryOAuth2AuthorizationService with JdbcOAuth2AuthorizationService
OAuth2AuthorizationConsentServiceReplace with JdbcOAuth2AuthorizationConsentService
RSA keysLoad from shared keystore โ€” all instances must use the same key
Launch tokensAlready in PostgreSQL โ€” no change needed
Clinician sessionsUse Redis: spring.session.store-type: redis

Production notesโ€‹

Change default credentials

DataInitializer seeds dr.smith / password and dr.jones / password on first startup. Change these before any non-local deployment.

PHI in logs

FHIR responses contain PHI. Set hapi.fhir.log-requests-and-responses: false. Set logging.level.com.ajfhir.auth: WARN in production.

HIPAA BAA

Before any real patient data flows through this server, a Business Associate Agreement must be signed with the hospital and any cloud/database provider.