A Docker Swarm-based Health Information Exchange (HIE) for Haiti, built on Instant OpenHIE v2. SEDISH connects multiple iSantePlus (OpenMRS) clinic sites to a centralized data exchange layer for patient identity management, shared health records, and clinical data analytics.
┌──────────────┐
│ OpenCR │
│ (Patient MPI)│
└──────▲───────┘
│ /CR/fhir
┌─────────────┐ ┌───────┴────────┐ ┌──────────────┐
│ iSantePlus │──mpi-client───▶│ │ │ │
│ (Site 1) │ │ OpenHIM │───/SHR/fhir──▶│ HAPI FHIR │
│ iSantePlus │──mpi-client───▶│ (Mediator) │ │ (SHR) │
│ (Site 2) │ │ │ │ │
│ iSantePlus │──mpi-client───▶│ Port 5001 │ └──────────────┘
│ (Site N) │ └─────────────────┘
└─────────────┘
Data flows:
| Flow | Trigger | Path | Purpose |
|---|---|---|---|
| Patient identity | Patient create/update | iSantePlus → OpenHIM → OpenCR | Real-time MPI registration via mpi-client module |
| Clinical documents | Lab order (VL/EID) | iSantePlus → OpenHIM → SHR → HAPI FHIR | Real-time via xds-sender module |
| Component | Image | Purpose |
|---|---|---|
| iSantePlus | itechuw/docker-isanteplus-server:local-2 |
OpenMRS-based EMR (multiple clinic instances) |
| OpenHIM | jembi/openhim-core:v8.5.0 |
Interoperability layer — routes, logs, and secures all data exchange |
| OpenCR | itechuw/opencr |
Master Patient Index (MPI) — de-duplicates patient identities |
| HAPI FHIR | jembi/hapi:v7.0.3-wget |
FHIR R4 data store — Shared Health Record (SHR) |
| SHR Mediator | itechuw/shared-health-record:main |
Proxies FHIR requests to HAPI FHIR with validation |
| Keycloak | keycloak/keycloak:20.0 |
Identity and access management (SSO) |
| Nginx | nginx:stable |
Reverse proxy with Let's Encrypt SSL |
| Monitoring | Grafana + Prometheus + Loki | Dashboards, metrics, and log aggregation |
- Server: Ubuntu 20.04+ with at least 16 GB RAM (32 GB recommended)
- Docker: Docker CE with Swarm mode
- Domain: A domain with wildcard DNS (
*.yourdomain.com) pointing to the server - Git LFS: Required for
.omodand.sqlbinary files
# 1. Install dependencies
sudo apt-get update && sudo apt-get install -y git jq
curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | sudo bash
sudo apt-get install -y git-lfs && git lfs install
# Install Docker CE: https://docs.docker.com/engine/install/ubuntu/
# 2. Initialize Docker Swarm
docker swarm init
# 3. Clone and fetch LFS files
git clone https://github.com/charess-org/sedish.git
cd sedish
git lfs pull
# 4. Configure
cp .env.hie .env
# Edit .env — set DOMAIN_NAME, SUBDOMAINS, RENEWAL_EMAIL, STAGING=false
# 5. Build
./get-cli.sh linux
sudo mkdir -p /backups/elasticsearch /tmp/backups
./build-custom-images.sh
./build-image.sh
# 6. Deploy
./instant project init --env-file .env
# 7. Apply HAPI FHIR overrides (required after every deploy)
./packages/fhir-datastore-hapi-fhir/post-deploy.sh
# 8. Verify
docker service lsiSantePlus instances take 10–15 minutes to fully boot on first start. The
post-start.shscript automatically configures xds-sender endpoints once OpenMRS is ready.
Copy .env.hie to .env and configure at minimum:
| Variable | Description | Example |
|---|---|---|
DOMAIN_NAME |
Base domain for all services | sedishtest.live |
SUBDOMAINS |
Comma-separated list of all subdomains | opencr.sedishtest.live,openhimconsole.sedishtest.live,... |
RENEWAL_EMAIL |
Email for Let's Encrypt notifications | admin@example.com |
STAGING |
Set to false for production SSL certs |
false |
INSECURE |
Set to false to enable HTTPS |
false |
See .env.hie for the full list of configurable variables.
After deployment, the following services are accessible via HTTPS:
| Service | URL | Credentials |
|---|---|---|
| iSantePlus (HUEH) | https://hueh.<domain>/openmrs |
admin / Admin123 |
| iSantePlus (La Paix) | https://lapaix.<domain>/openmrs |
admin / Admin123 |
| iSantePlus (OFATMA) | https://ofatma.<domain>/openmrs |
admin / Admin123 |
| iSantePlus (Foyer St-Camille) | https://foyer-saint-camille.<domain>/openmrs |
admin / Admin123 |
| OpenHIM Console | https://openhimconsole.<domain> |
root@openhim.org / instant101 |
| OpenCR | https://opencr.<domain>/crux |
— |
| SHR (HAPI FHIR Browser) | https://shr.<domain>/fhir |
— |
| Grafana | https://grafana.<domain> |
— |
| Keycloak | https://keycloak.<domain> |
— |
Note: iSantePlus serves on
/openmrs, not/. Going tohttps://hueh.<domain>/will return 404.
Each iSantePlus instance has its own MySQL database (openmrs, openmrs2, openmrs3, ...) initialized from the same SQL dump. To prevent patient ID collisions in OpenCR, two mechanisms are used:
-
Sequence offsets: Each database starts its idgen sequence at a different value (instance 1 at 100000, instance 2 at 200000, etc.) so generated IDs never overlap.
-
Unique FHIR system URIs: Each instance has a unique
mpi-client.pid.localvalue (e.g.,http://hueh.sedishtest.live/ws/fhir2/pid/openmrsid/) so OpenCR can distinguish patient sources even if IDs were to collide.
Both are configured automatically by projects/isanteplus-db/initdb/20-configure-per-instance.sh during fresh database initialization. The FACILITY_NAMES env var controls the mapping (default: hueh,lapaix,ofatma,fsc).
Patient ID format: iSantePlus uses the Luhn Mod-30 check digit validator with the character set 0123456789ACDEFGHJKLMNPRTUVWXY. Note that B, I, O, Q, S, Z are deliberately excluded to avoid ambiguity.
There are two ways to deploy: the Instant CLI (recommended) or manual docker stack deploy (for development or when you need direct control).
The Instant CLI reads package-metadata.json for env var injection and handles Docker config management. You must rebuild the management image after any local changes:
# Build the management image (required after any local file changes)
./build-image.sh
# Deploy everything at once
./instant project init --env-file .env
# Post-deploy fixes (Instant CLI doesn't handle these)
./packages/fhir-datastore-hapi-fhir/post-deploy.shOr deploy per-package in dependency order:
./instant package init -n database-postgres --env-file .env
./instant package init -n database-mysql --env-file .env
./instant package init -n interoperability-layer-openhim --env-file .env
./instant package init -n reverse-proxy-nginx --env-file .env
./instant package init -n fhir-datastore-hapi-fhir --env-file .env
./packages/fhir-datastore-hapi-fhir/post-deploy.sh
./instant package init -n client-registry-opencr --env-file .env
./instant package init -n shared-health-record-fhir --env-file .env
./instant package init -n emr-isanteplus --env-file .env
# Wait 10-15 min for iSantePlus to boot before deploying pipelines
./instant package init -n data-pipeline-isanteplus --env-file .envUses local files directly — no ./build-image.sh needed. However, env vars from package-metadata.json won't be substituted (use for packages that don't depend on them, or set the vars in .env).
# 1. Databases (no dependencies)
docker stack deploy -c packages/database-postgres/docker-compose.yml postgres
docker stack deploy -c packages/database-mysql/docker-compose.yml mysql
# 2. Interoperability layer (bundles its own MongoDB)
docker stack deploy -c packages/interoperability-layer-openhim/docker-compose.yml \
-c packages/interoperability-layer-openhim/docker-compose.config.yml openhim
# 3. Reverse proxy
docker stack deploy -c packages/reverse-proxy-nginx/docker-compose.yml reverse-proxy
# 4. FHIR datastore (needs postgres)
docker stack deploy -c packages/fhir-datastore-hapi-fhir/docker-compose.yml hapi-fhir
./packages/fhir-datastore-hapi-fhir/post-deploy.sh
# 5. Client Registry (needs its own ES + HAPI FHIR + postgres)
docker stack deploy \
-c packages/client-registry-opencr/docker-compose-es.yml \
-c packages/client-registry-opencr/docker-compose-postgres.yml \
-c packages/client-registry-opencr/docker-compose-hapi.yml \
-c packages/client-registry-opencr/docker-compose.yml \
client-registry-opencr
# 6. SHR Mediator (needs openhim + hapi-fhir)
docker stack deploy -c packages/shared-health-record-fhir/docker-compose.yml shared-health-record
# 7. iSantePlus EMR (needs mysql + openhim — takes 10-15 min to boot)
docker stack deploy -c packages/emr-isanteplus/docker-compose.yml isanteplus
# 8. FHIR Data Pipelines (needs isanteplus + openhim + hapi-fhir — deploy LAST)
docker stack deploy -c packages/data-pipeline-isanteplus/docker-compose.yml pipeline
# 9. Verify all services are up
docker service ls --format 'table {{.Name}}\t{{.Replicas}}'Note: OpenCR may need a force restart after first deploy if ES wasn't ready:
docker service update --force client-registry-opencr_opencr
Each HIE component is deployed as a package. Use the instant CLI to manage them:
# Deploy a package (first time)
./instant package init -n <package-name> --env-file .env
# Stop a package (preserves data)
./instant package down -n <package-name> --env-file .env
# Restart a package
./instant package up -n <package-name> --env-file .env| Package ID | Stack | Description |
|---|---|---|
reverse-proxy-nginx |
reverse-proxy |
Nginx + Let's Encrypt SSL |
interoperability-layer-openhim |
openhim |
OpenHIM Core + Console + MongoDB |
fhir-datastore-hapi-fhir |
hapi-fhir |
HAPI FHIR R4 Server (SHR) |
shared-health-record-fhir |
shared-health-record |
SHR Mediator |
emr-isanteplus |
isanteplus |
iSantePlus EMR instances |
client-registry-opencr |
client-registry-opencr |
OpenCR MPI |
database-postgres |
postgres |
PostgreSQL (HAPI FHIR, Keycloak) |
database-mysql |
mysql |
MySQL (iSantePlus) |
identity-access-manager-keycloak |
keycloak |
Keycloak SSO |
monitoring |
monitoring |
Grafana + Prometheus + Loki |
initvsup: Useinitonly for first-time deployment or after wiping data. Useupfor restarts.- HAPI FHIR: Always run
./packages/fhir-datastore-hapi-fhir/post-deploy.shafter deploying or updating HAPI FHIR. This adds thereverse-proxy_publicnetwork (for SHR browser access) and sets referential integrity + placeholder target settings. The instant CLI reads compose files from the jembi/platform base image, which doesn't include our HAPI FHIR overrides — this script applies them. - OpenHIM: If MongoDB was wiped, use
init(notup) to re-run the config importer. - Pipeline resource order: The data pipeline syncs resources in the order defined in
resourceListinapplication-isanteplus*.yaml. Referenced resources (Practitioner, Location) must come before resources that reference them (Encounter, Observation) to avoid referential integrity errors.
Each iSantePlus instance has its own dedicated pipeline that syncs FHIR resources to the SHR. Pipelines run on a schedule (incremental sync every hour) and can be managed from the terminal.
pipeline-isanteplus1 → http://isanteplus:8080/openmrs/ws/fhir2/R4 → SHR (via OpenHIM)
pipeline-isanteplus2 → http://isanteplus2:8080/openmrs/ws/fhir2/R4 → SHR (via OpenHIM)
Schedules are staggered to avoid concurrent writes:
- Pipeline 1: runs at
:00(top of hour) - Pipeline 2: runs at
:30(half hour)
# List pipeline services
docker service ls -f name=pipeline
# Check last run time and next scheduled run
docker service logs pipeline_pipeline-isanteplus1 --tail 5
docker service logs pipeline_pipeline-isanteplus2 --tail 5# Full run (re-syncs all resources)
docker exec $(docker ps -q -f name=pipeline_pipeline-isanteplus1) \
curl -s -X POST http://localhost:8080/run -F "runMode=FULL"
# Incremental run (only changed resources since last run)
docker exec $(docker ps -q -f name=pipeline_pipeline-isanteplus2) \
curl -s -X POST http://localhost:8080/run -F "runMode=INCREMENTAL"# Check for errors in a specific pipeline
docker service logs pipeline_pipeline-isanteplus1 2>&1 | grep -i "error\|fail\|409" | tail -20
# Check SHR mediator for MPI resolution activity
docker service logs shared-health-record_shr 2>&1 | grep -i "MPI resolved" | tail -10docker service update --force pipeline_pipeline-isanteplus1- Create
packages/data-pipeline-isanteplus/config/application-isanteplusN.yaml— copy from an existing config and changefhirServerUrlto point at the new instance - Add the new service to
packages/data-pipeline-isanteplus/docker-compose.yml - Stagger the
incrementalScheduleto avoid overlap with other pipelines
Each iSantePlus instance requires configuration across multiple components. Here's the checklist for adding instance N (e.g., isanteplus5):
The projects/isanteplus-db/initdb/10-create-dbs.sh script automatically creates databases openmrs, openmrs2, ..., openmrsN on first boot. Set OPENMRS_DB_COUNT in .env to cover the number of instances.
The 20-configure-per-instance.sh script runs on fresh database init and sets:
- xds-sender endpoints pointing to your OpenHIM domain
- idgen sequence offset (instance N starts at N * 100000)
- Unique
mpi-client.pid.localFHIR system URI per facility
Add the new facility to the FACILITY_NAMES env var (comma-separated, lowercase).
Add the new service to packages/emr-isanteplus/docker-compose.yml:
isanteplusN:
image: itechuw/docker-isanteplus-server:local-2
environment:
- OMRS_CONFIG_CONNECTION_URL=${OMRS_CONFIG_CONNECTION_URL_N}
- OMRS_CONFIG_CONNECTION_USERNAME=${OMRS_CONFIG_CONNECTION_USERNAME_N}
- OMRS_CONFIG_CONNECTION_PASSWORD=${OMRS_CONFIG_CONNECTION_PASSWORD_N}
# ... (copy remaining OMRS env vars from existing instances)
volumes:
- isanteplusN-data:/openmrs/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
networks:
- public
- reverse-proxy
- mysql
- openhimAdd the volume under volumes: and the database variables to package-metadata.json.
Add a server block to packages/reverse-proxy-nginx/package-conf-secure/:
server {
listen 80;
server_name facilityname.*;
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl;
server_name facilityname.*;
location / {
resolver 127.0.0.11 valid=30s;
set $upstream_isanteplusN isanteplusN;
proxy_pass http://$upstream_isanteplusN:8080;
}
}Add the subdomain to SUBDOMAINS in .env.
OMRS_CONFIG_CONNECTION_URL_N=jdbc:mysql://mysql:3306/openmrsN?autoReconnect=true
OMRS_CONFIG_CONNECTION_USERNAME_N=openmrsN
OMRS_CONFIG_CONNECTION_PASSWORD_N=dev_password_only
SUBDOMAIN_CORE_ISANTEPLUSN=facilityname
Certificates are provisioned during init via Certbot. The set-secure-mode.sh script:
- Generates a staging (dummy) cert to bootstrap nginx
- Scales nginx down to free port 80
- Generates a production cert via HTTP-01 challenge
- Updates nginx Docker secrets with the real cert and scales back up
Let's Encrypt allows 5 certificates per exact domain set per 7 days. If you hit this limit, you'll see:
too many certificates (5) already issued for this exact set of identifiers
Workaround: Request a cert with a different subset of domains (a different "exact set" is not rate-limited):
# Scale down nginx to free port 80
docker service scale reverse-proxy_reverse-proxy-nginx=0
# Request cert with fewer domains
docker run --rm --network host --name certbot \
-v prod-certbot-conf:/etc/letsencrypt/archive/${DOMAIN_NAME} \
certbot/certbot:v1.23.0 certonly -n --standalone \
-m admin@${DOMAIN_NAME} \
-d "${DOMAIN_NAME},subdomain1.${DOMAIN_NAME},subdomain2.${DOMAIN_NAME}" \
--agree-tos
# Copy certs and create Docker secrets
docker run --rm --network host -w /temp \
-v prod-certbot-conf:/temp-certificates \
-v instant:/temp busybox sh \
-c "rm -rf certificates; mkdir -p certificates; cp -r /temp-certificates/* /temp/certificates"
TIMESTAMP=$(date "+%Y%m%d%H%M%S")
docker secret create --label name=nginx "${TIMESTAMP}-fullchain.pem" \
<(docker run --rm -v instant:/temp busybox cat /temp/certificates/fullchain1.pem)
docker secret create --label name=nginx "${TIMESTAMP}-privkey.pem" \
<(docker run --rm -v instant:/temp busybox cat /temp/certificates/privkey1.pem)
# Swap secrets on nginx and scale back up
CURR_FULL=$(docker service inspect reverse-proxy_reverse-proxy-nginx \
--format '{{(index .Spec.TaskTemplate.ContainerSpec.Secrets 0).SecretName}}')
CURR_KEY=$(docker service inspect reverse-proxy_reverse-proxy-nginx \
--format '{{(index .Spec.TaskTemplate.ContainerSpec.Secrets 1).SecretName}}')
docker service update --replicas 1 \
--secret-rm "$CURR_FULL" --secret-rm "$CURR_KEY" \
--secret-add source=${TIMESTAMP}-fullchain.pem,target=/run/secrets/fullchain.pem \
--secret-add source=${TIMESTAMP}-privkey.pem,target=/run/secrets/privkey.pem \
reverse-proxy_reverse-proxy-nginx
# Clean up
docker volume rm prod-certbot-conf- Place
fullchain.pemandprivkey.pemon the host - Set paths in
.env:USE_PROVIDED_CERTIFICATES=true HOST_PROVIDED_CERT_FULLCHAIN_PATH=/ssl/your_domain/fullchain.pem HOST_PROVIDED_CERT_PRIVKEY_PATH=/ssl/your_domain/privkey.pem - Rebuild:
./build-image.sh - Redeploy:
./instant package init -n reverse-proxy-nginx --env-file .env
./instant package down -n <package-name> --env-file .env
./instant package up -n <package-name> --env-file .env
# If HAPI FHIR:
./packages/fhir-datastore-hapi-fhir/post-deploy.sh# If iSantePlus Dockerfile or modules changed:
docker build -t itechuw/docker-isanteplus-server:local-2 packages/emr-isanteplus/
# Force the service to pick up the new image:
docker service update --force isanteplus_isanteplus# 1. Stop all services
./instant project down --env-file .env
# 2. Rebuild images (if code changed)
docker build -t itechuw/docker-isanteplus-server:local-2 packages/emr-isanteplus/
./build-image.sh
# 3. Deploy
./instant project init --env-file .env
# 4. Apply HAPI FHIR overrides
./packages/fhir-datastore-hapi-fhir/post-deploy.sh
# 5. Verify
docker service ls --format '{{.Name}} {{.Replicas}}' | grep '0/' | grep -v 'await-helper\|config-importer'sudo bash purge-local.sh
# Then follow Quick Start from step 5# Which services are down?
docker service ls --format '{{.Name}} {{.Replicas}}' | grep '0/'
# Expected at 0/1: await-helper, config-importer. Everything else should be 1/1.
# Check logs for a specific service
docker service logs <service_name> --tail 50
# Check logs for a specific container (faster for large log histories)
docker logs $(docker ps -q -f name=<service_name>.1) --tail 50
# Force restart a stuck service
docker service update --force <service_name>
# Check what networks a service is on
docker inspect $(docker ps -q -f name=<service_name>) \
--format '{{range $net, $conf := .NetworkSettings.Networks}}{{$net}} {{end}}'
# Check env vars on a running service
docker service inspect <service_name> \
--format '{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}'
# Check env vars inside a running container
docker exec $(docker ps -q -f name=<service_name>.1) env | grep <VAR>OpenMRS is still booting. First boot takes 10–15 minutes (module loading, liquibase migrations, Spring context refresh). Check progress:
docker logs $(docker ps -q -f name=isanteplus_isanteplus.1) --tail 10Refreshing Context— still loading, almost doneDispatcherServlet.noHandlerFound— REST API not mapped yet, still refreshingreferenceapplication.started value: true— boot complete
If it's stuck, check the container uptime:
docker ps --format "{{.Names}}\t{{.Status}}" | grep isanteplusThe base image startup.sh uses wait ${!} which waits for the last background process (post-start.sh), not Tomcat. When post-start.sh completes, the container exits. This is fixed in the custom Dockerfile which captures TOMCAT_PID=$! and uses wait $TOMCAT_PID.
If the container is still restarting, rebuild the image:
docker build -t itechuw/docker-isanteplus-server:local-2 packages/emr-isanteplus/
docker service update --force isanteplus_isanteplus# Check post-start output for a specific instance
docker logs $(docker ps -q -f name=isanteplus_isanteplus.1) 2>&1 | grep "\[post-start\]"Waiting for OpenMRS to be ready...— still polling, OpenMRS not ready yetOpenMRS is ready (after Xs)— success, then check forSet xdssender.*linesERROR: OpenMRS did not become ready after 900s— timed out; OpenMRS may need more time or has an error
If post-start timed out, check if OpenMRS is actually running:
docker exec $(docker ps -q -f name=isanteplus_isanteplus.1) \
curl -sf -u admin:Admin123 http://localhost:8080/openmrs/ws/rest/v1/sessionThe idgen auto-generation is not working. Check the idgen configuration:
# Check idgen sequence config
docker exec $(docker ps -q -f name=mysql_mysql) \
mysql -u openmrs -pdev_password_only openmrs -e \
"SELECT prefix, next_sequence_value FROM idgen_seq_id_gen WHERE id = 1;"
# Check auto-generation is enabled
docker exec $(docker ps -q -f name=mysql_mysql) \
mysql -u openmrs -pdev_password_only openmrs -e \
"SELECT * FROM idgen_auto_generation_option;"The automatic_generation_enabled column must be 1 and source must point to a valid idgen_identifier_source.
Check that each instance has a unique mpi-client.pid.local:
for svc in isanteplus isanteplus2 isanteplus3 isanteplus4; do
echo -n "$svc: "
docker exec $(docker ps -q -f name=isanteplus_${svc}.1) \
curl -s -u admin:Admin123 \
"http://localhost:8080/openmrs/ws/rest/v1/systemsetting/mpi-client.pid.local" 2>/dev/null \
| grep -o '"value":"[^"]*"' | sed 's/"value":"//;s/"//'
doneIf they're all the same (e.g., http://isanteplus/ws/fhir2/pid/openmrsid/), the 20-configure-per-instance.sh didn't run. Fix manually:
# Replace <db>, <facility>, <domain> for each instance
docker exec $(docker ps -q -f name=mysql_mysql) \
mysql -u <db> -pdev_password_only <db> -e \
"UPDATE global_property SET property_value = 'http://<facility>.<domain>/ws/fhir2/pid/openmrsid/'
WHERE property = 'mpi-client.pid.local';"Then restart the iSantePlus instances to pick up the change.
# Check current certificate
docker exec $(docker ps -q -f name=reverse-proxy_reverse-proxy-nginx) \
openssl x509 -in /run/secrets/fullchain.pem -noout -issuer -subject -datesIf issuer contains (STAGING), the production cert generation failed. Common causes:
- Port 80 was not free — nginx wasn't scaled down before certbot ran
- Rate limited — too many certs issued in the last 7 days
- DNS not pointing to server — the domain must resolve to this server's IP
See the SSL/TLS Certificates section for the manual fix.
Check the SANs on the current cert:
docker exec $(docker ps -q -f name=reverse-proxy_reverse-proxy-nginx) \
openssl x509 -in /run/secrets/fullchain.pem -noout -text | grep "DNS:"If a subdomain is missing, regenerate the cert with the correct domain list (see rate limit workaround above).
Firefox/Zen will refuse to load a page with a staging cert if HSTS is enabled. The only fix is to get a valid production certificate. In Chrome, you can:
- Go to
chrome://net-internals/#hsts - Delete the domain under "Delete domain security policies"
- Revisit and click "Advanced → Proceed"
HAPI FHIR needs to be on the reverse-proxy_public network:
docker service update --network-add reverse-proxy_public hapi-fhir_hapi-fhirdocker exec $(docker ps -q -f name=postgres_postgres-1) \
env PGPASSWORD=instant101 psql -U postgres -c 'CREATE DATABASE hapi;'
docker service update --force hapi-fhir_hapi-fhirdocker service inspect hapi-fhir_hapi-fhir \
--format '{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' \
| grep -E 'referential|client_id'If missing, run ./packages/fhir-datastore-hapi-fhir/post-deploy.sh.
# Check replica set status
docker exec $(docker ps -q -f name=openhim_mongo-1) mongo --eval "rs.status()"
# Initialize replica set
docker exec $(docker ps -q -f name=openhim_mongo-1) mongo --eval \
'rs.initiate({_id:"mongo-set",members:[{_id:0,host:"mongo-1:27017"}]})'
# Restart OpenHIM Core
docker service update --force openhim_openhim-core
# Re-import channels/clients (if MongoDB was wiped)
./instant package init -n interoperability-layer-openhim --env-file .envIf you run docker stack deploy directly (instead of via ./instant), the ${OMRS_CONFIG_*} variables in docker-compose.yml will resolve to empty strings because they come from package-metadata.json, which the instant CLI reads.
Always use the instant CLI to deploy packages:
./instant package init -n emr-isanteplus --env-file .envIf you need to update a single service's env var without redeploying:
docker service update --env-add KEY=VALUE <service_name># Check all databases
for db in openmrs openmrs2 openmrs3 openmrs4; do
echo "=== $db ==="
docker exec $(docker ps -q -f name=mysql_mysql) \
mysql -u "$db" -pdev_password_only "$db" -e \
"SELECT prefix, next_sequence_value FROM idgen_seq_id_gen WHERE id = 1;" 2>&1 | grep -v Warning
done
# Update a specific database's sequence offset
docker exec $(docker ps -q -f name=mysql_mysql) \
mysql -u openmrs2 -pdev_password_only openmrs2 -e \
"UPDATE idgen_seq_id_gen SET next_sequence_value = 200000 WHERE id = 1;"OpenHIM clients authenticate on port 5001 using Basic auth with SHA512-hashed passwords.
Generate a password hash:
python3 -c "
import hashlib, os
password = 'instant101'
salt = os.urandom(16).hex()
hash_val = hashlib.sha512((password + salt).encode()).hexdigest()
print(f'passwordSalt: {salt}')
print(f'passwordHash: {hash_val}')
"Formula:
sha512(password + salt)— password first, then salt.
Files that must stay in sync:
| File | Field |
|---|---|
packages/data-pipeline-isanteplus/config/application.yaml |
sinkUserName / sinkPassword |
packages/interoperability-layer-openhim/importer/volume/openhim-import.json |
Client passwordHash / passwordSalt |
- Default passwords: Change
Admin123,instant101,dev_password_onlybefore production use - Docker Secrets: Use for sensitive configuration (passwords, API keys)
- Swarm Locking: Rotate the CA key with
docker swarm ca --rotate - SSH Hardening: Key-based auth only, disable root login
- Firewall: Restrict inbound traffic to ports 80, 443 only
- Encryption: Enable disk encryption for data at rest
sedish/
├── .env # Environment configuration
├── build-custom-images.sh # Builds iSantePlus, MySQL, ES images
├── build-image.sh # Builds the management/deployment image
├── get-cli.sh # Downloads the Instant OpenHIE CLI
├── instant # Instant OpenHIE CLI binary
├── packages/
│ ├── reverse-proxy-nginx/ # Nginx + Let's Encrypt
│ ├── interoperability-layer-openhim/ # OpenHIM
│ ├── fhir-datastore-hapi-fhir/ # HAPI FHIR + post-deploy.sh
│ ├── shared-health-record-fhir/ # SHR Mediator
│ ├── emr-isanteplus/ # iSantePlus EMR
│ │ ├── Dockerfile # Custom image with post-start.sh
│ │ ├── config/post-start.sh # Auto-configures xds-sender on boot
│ │ └── docker-compose.yml # Service definitions for all instances
│ ├── data-pipeline-isanteplus/ # FHIR data pipelines (one per instance)
│ │ ├── config/application-isanteplus1.yaml # Pipeline 1 → isanteplus
│ │ ├── config/application-isanteplus2.yaml # Pipeline 2 → isanteplus2
│ │ └── docker-compose.yml
│ ├── client-registry-opencr/ # OpenCR
│ ├── database-postgres/ # PostgreSQL
│ ├── database-mysql/ # MySQL
│ ├── identity-access-manager-keycloak/ # Keycloak
│ └── monitoring/ # Grafana + Prometheus + Loki
└── projects/
└── isanteplus-db/ # MySQL seed data for iSantePlus
├── Dockerfile # Custom MySQL image
└── initdb/
├── 10-create-dbs.sh # Creates openmrs, openmrs2, ... databases
├── 20-configure-per-instance.sh # Sets sequence offsets + unique URIs
└── isanteplus-db.sql # Base SQL dump (Git LFS)