Mirth Connect (now NextGen Connect) is the most widely deployed open-source healthcare integration engine. It handles the fundamental challenge of healthcare IT: connecting systems that speak different protocols and data formats. Hospitals use it to route HL7v2 messages between EHRs, lab systems, pharmacy systems, and clinical applications.
For the broader picture beyond HL7 interfaces — use cases, troubleshooting, and performance tuning — see our complete Mirth Connect guide.
This guide covers everything you need to deploy Mirth Connect in production: installation, channel architecture, JavaScript transformers, high availability with PostgreSQL, Docker deployment, monitoring with Prometheus, and security hardening. These are not sandbox configurations — they are the patterns we use at Nirmitee for production healthcare integrations.
Installation and Initial Configuration
System Requirements
| Component | Minimum (Dev) | Recommended (Production) | High Volume (50K+ msgs/day) |
|---|---|---|---|
| CPU | 2 cores | 8 cores | 16+ cores |
| RAM | 4 GB | 16 GB | 32 GB |
| Disk | 20 GB SSD | 100 GB SSD | 500 GB NVMe |
| JDK | OpenJDK 17 | OpenJDK 17 (Temurin) | OpenJDK 17 (Temurin) |
| Database | Derby (embedded) | PostgreSQL 15+ | PostgreSQL 16 with replication |
| OS | Any (Java) | RHEL 8+ or Ubuntu 22.04 | RHEL 8+ or Ubuntu 22.04 |
Installation Steps
# 1. Download Mirth Connect (version 4.5.x as of 2026)
wget https://github.com/nextgenhealthcare/connect/releases/download/4.5.0/mirthconnect-4.5.0-unix.tar.gz
tar xzf mirthconnect-4.5.0-unix.tar.gz
cd Mirth\ Connect/
# 2. Configure PostgreSQL database
sudo -u postgres psql << SQL
CREATE USER mirth WITH PASSWORD 'your-secure-password';
CREATE DATABASE mirthdb OWNER mirth;
GRANT ALL PRIVILEGES ON DATABASE mirthdb TO mirth;
SQL
# 3. Update mirth.properties for PostgreSQL
cat > conf/mirth.properties << EOF
database = postgres
database.url = jdbc:postgresql://localhost:5432/mirthdb
database.username = mirth
database.password = your-secure-password
database.max-connections = 50
# Admin console HTTPS
https.port = 8443
https.client.protocols = TLSv1.2,TLSv1.3
# JVM tuning
jvm.heap.size = 8g
jvm.args = -XX:+UseG1GC -XX:MaxGCPauseMillis=100
EOF
# 4. Start Mirth Connect
./mcservice start
# 5. Verify it is running
curl -sk https://localhost:8443/api/server/version
# Should return: "4.5.0"Channel Architecture: Patterns That Scale
A well-designed channel architecture separates concerns and enables independent scaling and monitoring.
Pattern 1: Protocol-Specific Inbound Channels
Create one inbound channel per protocol and source system combination. This isolates failures — if the lab system sends malformed messages, only the lab inbound channel is affected.
- ADT-Inbound-Epic — TCP listener on port 6661, receives ADT^A01/A02/A03/A04/A08 from Epic
- Lab-Inbound-Sunquest — TCP listener on port 6662, receives ORU^R01 from Sunquest LIS
- Pharmacy-Inbound-Pyxis — TCP listener on port 6663, receives RDE^O11 from BD Pyxis
- FHIR-Inbound-API — HTTP listener on port 8080, receives FHIR R4 Bundles from modern applications
Pattern 2: Transformation Channels
Separate transformation logic from I/O. Inbound channels pass raw messages to transformation channels via the Channel Writer destination. This allows you to reuse transformers across multiple sources.
Pattern 3: Outbound Channels with Retry Logic
Outbound channels deliver transformed messages to destination systems. Each destination gets its own channel with appropriate retry and error handling.
JavaScript Transformers: Real Production Examples
Mirth's JavaScript transformer is where the integration logic lives. Here are production-tested transformer patterns.
ADT Message Parser and Router
// Source Transformer: Parse ADT and enrich with routing metadata
// Channel: ADT-Transform
// Extract key fields from the HL7v2 message
var msgType = msg['MSH']['MSH.9']['MSH.9.1'].toString();
var triggerEvent = msg['MSH']['MSH.9']['MSH.9.2'].toString();
var patientId = '';
var patientName = '';
// Defensive null checks (production messages have missing segments)
if (msg['PID'] && msg['PID']['PID.3'] && msg['PID']['PID.3']['PID.3.1']) {
patientId = msg['PID']['PID.3']['PID.3.1'].toString();
}
if (msg['PID'] && msg['PID']['PID.5']) {
var lastName = msg['PID']['PID.5']['PID.5.1'] ? msg['PID']['PID.5']['PID.5.1'].toString() : '';
var firstName = msg['PID']['PID.5']['PID.5.2'] ? msg['PID']['PID.5']['PID.5.2'].toString() : '';
patientName = lastName + ', ' + firstName;
}
// Set channel map variables for routing
channelMap.put('messageType', msgType + '^' + triggerEvent);
channelMap.put('patientId', patientId);
channelMap.put('patientName', patientName);
// Determine destination based on message type
var destinations = [];
switch (triggerEvent) {
case 'A01': // Admission
destinations = ['ADT-Out-Billing', 'ADT-Out-Pharmacy', 'ADT-Out-Analytics'];
break;
case 'A03': // Discharge
destinations = ['ADT-Out-Billing', 'ADT-Out-Quality', 'ADT-Out-Analytics'];
break;
case 'A08': // Update
destinations = ['ADT-Out-Demographics'];
break;
default:
destinations = ['ADT-Out-Archive'];
}
channelMap.put('destinations', destinations.join(','));
logger.info('ADT ' + triggerEvent + ' for patient ' + patientId);HL7v2 to FHIR Transformer
// Transformer: HL7v2 ORU^R01 -> FHIR DiagnosticReport + Observations
// Channel: Lab-FHIR-Transform
var patientRef = 'Patient/' + msg['PID']['PID.3']['PID.3.1'].toString();
var observations = [];
// Iterate over OBX segments (lab results)
for (var i = 0; i < msg['OBX'].length(); i++) {
var obx = msg['OBX'][i];
var observation = {
"resourceType": "Observation",
"status": mapObservationStatus(obx['OBX.11'].toString()),
"code": {
"coding": [{
"system": "http://loinc.org",
"code": obx['OBX.3']['OBX.3.1'].toString(),
"display": obx['OBX.3']['OBX.3.2'].toString()
}]
},
"subject": { "reference": patientRef },
"valueQuantity": {
"value": parseFloat(obx['OBX.5'].toString()),
"unit": obx['OBX.6']['OBX.6.1'] ? obx['OBX.6']['OBX.6.1'].toString() : '',
"system": "http://unitsofmeasure.org"
},
"referenceRange": [{
"text": obx['OBX.7'] ? obx['OBX.7'].toString() : ''
}],
"interpretation": obx['OBX.8'] ? [{
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation",
"code": mapInterpretation(obx['OBX.8'].toString())
}]
}] : undefined
};
observations.push(observation);
}
function mapObservationStatus(hl7Status) {
var statusMap = {'F': 'final', 'P': 'preliminary', 'C': 'corrected', 'X': 'cancelled'};
return statusMap[hl7Status] || 'unknown';
}
function mapInterpretation(hl7Interp) {
var interpMap = {'H': 'H', 'L': 'L', 'HH': 'HH', 'LL': 'LL', 'N': 'N', 'A': 'A'};
return interpMap[hl7Interp] || hl7Interp;
}
// Build FHIR Bundle
var bundle = {
"resourceType": "Bundle",
"type": "transaction",
"entry": observations.map(function(obs) {
return {
"resource": obs,
"request": { "method": "POST", "url": "Observation" }
};
})
};
channelMap.put('fhirBundle', JSON.stringify(bundle));High Availability with PostgreSQL
Production healthcare integrations require high availability. Here is the HA architecture we deploy:
- Two Mirth Connect nodes behind an L4 load balancer (HAProxy or AWS NLB). Both nodes are active, processing different channels or sharing load for high-volume channels.
- PostgreSQL primary + synchronous replica. Both Mirth nodes write to the primary. The replica provides automatic failover through Patroni or pg_auto_failover.
- Shared configuration through the PostgreSQL database. Channel configurations, code templates, and global scripts are stored in the database and shared across nodes.
- Health checks on each node. The load balancer checks Mirth's API health endpoint. If a node fails health checks, traffic routes to the surviving node.
HAProxy Configuration
# haproxy.cfg for Mirth Connect HL7 TCP
frontend hl7_adt
bind *:6661
mode tcp
default_backend mirth_adt
backend mirth_adt
mode tcp
balance roundrobin
option tcp-check
server mirth1 10.0.1.10:6661 check inter 5s fall 3 rise 2
server mirth2 10.0.1.11:6661 check inter 5s fall 3 rise 2
frontend hl7_lab
bind *:6662
mode tcp
default_backend mirth_lab
backend mirth_lab
mode tcp
balance roundrobin
server mirth1 10.0.1.10:6662 check inter 5s fall 3 rise 2
server mirth2 10.0.1.11:6662 check inter 5s fall 3 rise 2Docker Deployment
# docker-compose.yml - Production Mirth Connect
version: "3.8"
services:
mirth:
image: nextgenhealthcare/connect:4.5.0
ports:
- "8443:8443"
- "6661:6661"
- "6662:6662"
- "6663:6663"
volumes:
- ./mirth-config:/opt/connect/appdata
- ./custom-lib:/opt/connect/custom-lib
- ./ssl:/opt/connect/ssl
environment:
- DATABASE=postgres
- DATABASE_URL=jdbc:postgresql://postgres:5432/mirthdb
- DATABASE_USERNAME=mirth
- DATABASE_PASSWORD_FILE=/run/secrets/db_password
- VMOPTIONS=-Xmx8g -XX:+UseG1GC
secrets:
- db_password
healthcheck:
test: ["CMD", "curl", "-skf", "https://localhost:8443/api/server/version"]
interval: 30s
timeout: 10s
retries: 3
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
postgres:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
environment:
- POSTGRES_DB=mirthdb
- POSTGRES_USER=mirth
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
secrets:
- db_password
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mirth -d mirthdb"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
secrets:
db_password:
file: ./secrets/db_password.txt
volumes:
pgdata:Security Hardening for HIPAA
Healthcare integration engines handle PHI and must meet HIPAA Technical Safeguards. Here is the security hardening checklist:
Network Security
- TLS 1.2+ for all connections — Configure Mirth to use TLS for both the admin console and all channel listeners/destinations. Disable TLS 1.0 and 1.1.
- IP allowlisting — Restrict inbound connections to known source system IPs. Use firewall rules at the OS level and Mirth's source filter settings.
- Network segmentation — Place Mirth servers in a private subnet with no direct internet access. Use a jump box or VPN for administrative access.
- HL7 port protection — HL7 TCP listeners (MLLP) do not natively support TLS. Use stunnel or HAProxy TLS termination in front of MLLP ports.
Application Security
- RBAC (Role-Based Access Control) — Create separate user accounts for administrators, channel developers, and monitoring. Never share the admin account.
- Password policy — Enforce strong passwords (12+ characters, complexity requirements). Rotate service account credentials quarterly.
- Encrypted credential storage — Store database passwords and API keys in Mirth's encrypted global map or external secrets manager (HashiCorp Vault, AWS Secrets Manager).
- Audit logging — Enable Mirth's built-in audit log for all configuration changes and message access. Forward audit logs to your SIEM.
- Message content encryption — For messages at rest in the Mirth database, enable PostgreSQL Transparent Data Encryption (TDE) or full-disk encryption.
Monitoring with Prometheus
See our comprehensive guide on Mirth Connect monitoring in production for full details. The key integration: expose Mirth JMX metrics via a Prometheus exporter and build Grafana dashboards for channel throughput, error rates, queue depth, and JVM health.
Common Production Issues and Solutions
| Issue | Symptoms | Root Cause | Solution |
|---|---|---|---|
| Message queue growing | Queue depth > 500, latency increasing | The destination system is slow or down | Check destination connectivity; implement a circuit breaker |
| Out of memory crashes | JVM heap > 90%, GC thrashing | Large messages or too many channels | Increase heap, enable message attachment mode for large payloads |
| Database connection exhaustion | Channel errors, admin console slow | Connection pool too small for channel count | Increase max-connections in mirth.properties; tune PostgreSQL |
| Character encoding corruption | Garbled patient names, broken accents | Mismatch between source and Mirth encoding | Set character encoding explicitly on channel source connector |
| Duplicate messages | Same message processed twice | TCP reconnect replays the last message | Implement an idempotency check using MSH.10 (Message Control ID) |
| Channel deployment failures | The channel is stuck in the deploying state | JavaScript syntax error in transformer | Check server log; fix transformer syntax; redeploy |
ACK Handling: The Reliability Contract
HL7 v2 reliability rests on acknowledgments, and misconfigured ACK behavior is behind a large share of “lost message” incidents. Two modes matter:
- Original mode — the receiving application ACKs after fully processing the message. Stronger guarantee, slower, and ties your sender's throughput to the destination's processing speed.
- Enhanced mode — the receiver ACKs on safe receipt (commit accept), then processes asynchronously. This is what Mirth's source queue effectively gives you: ACK on persist, process from the queue.
The design rule: never ACK a message you haven't durably stored, and never let a destination failure silently swallow a message that was already ACKed upstream. That second half is what dead letter queues are for — failed messages must land somewhere a human will see them, with enough context to replay safely. Our guide to message replay and dead letter queues in production covers the full pattern, including ordered replay after a destination outage.
Testing Channels Before They Touch Production
Most Mirth estates are tested by sending one happy-path message and declaring victory. Production traffic will send you: messages with missing optional segments, fields longer than spec, unexpected message types on a shared port, character encoding surprises, and malformed timestamps from systems older than your team. A channel that hasn't been tested against these will fail on them.
A practical regression approach: maintain a library of real (de-identified) message samples per interface — including the malformed ones that caused past incidents — and run every channel change against the full library before deployment, asserting on transformed output. This turns channel changes from “deploy and pray” into a reviewable, repeatable process. The complete setup, including running these tests in a pipeline, is in our CI/CD guide for Mirth channels.
Channel Governance: Promotion, Versioning, Rollback
The GUI makes it dangerously easy to edit production directly. Production-grade estates enforce three rules:
- Channels live in Git. Export channel XML on every change; the diff is your change review. No exceptions for “small tweaks” — those are the ones that take interfaces down.
- Changes promote dev → staging → production through the REST API, not by hand-editing in three places. Be aware the deployment API has sharp edges — we documented seven undocumented REST API gotchas after hitting them in real deployments, including the deploy endpoint that returns success when nothing deployed.
- Every deployment has a rollback artifact — the previous channel XML, one click away. Mean time to recovery matters more than deployment elegance.
Capacity Planning: Know Your Numbers Before They Surprise You
Size the engine against measured reality, not vendor folklore. Three numbers to establish early: peak messages per minute per interface (ADT bursts at shift change are routinely 5–10x the daily average), average message size (a lab result with embedded PDF is not a 2 KB ADT), and destination latency under load. From there, the levers are the ones covered in our performance tuning guide: JVM heap, message storage levels, destination queue threading, and database tuning. Plan for double your current peak — hospital message volume only moves in one direction.
Cutover Day: Moving an Interface Into Production
The go-live of a new HL7 interface has a standard choreography, and skipping steps is how organizations earn their war stories:
- Freeze the message sample baseline. Capture a day of production traffic from the legacy path (or the sending system's test harness) and verify your channel processes 100% of it in staging — not just the samples the vendor gave you.
- Run in shadow mode first. Where the architecture allows, tee live traffic to the new channel while the legacy path remains authoritative. Compare outputs message-by-message for at least one full business cycle — including a shift change and a batch window, which is where volume and edge cases live.
- Cut over at low tide with everyone on the call. Mid-morning on a weekday beats 2 AM Saturday: volumes are moderate, and the people who can fix problems — yours and the destination system's — are awake and reachable.
- Watch the right numbers for 48 hours. Message counts in vs. out reconciled against the sending system, error queue depth, and destination ACK latency. A successful cutover isn't “no errors in the first hour” — it's reconciled counts across two full days.
- Keep the rollback warm for a week. The legacy path stays configured and tested until the new interface has survived a full weekly cycle, including any weekly batch jobs.
For the broader operational picture — troubleshooting patterns, performance levers, and where the interface engine fits in a modern stack — see the complete Mirth Connect guide.
The Operational Habits That Keep Interfaces Healthy
The difference between an interface estate that pages someone nightly and one that runs quietly is rarely the initial build — it's the operating rhythm around it. The habits that compound: a morning glance at a single dashboard showing per-channel throughput deltas against the same hour last week (a feed running 40% below its normal volume is failing upstream, even if Mirth shows green); weekly review of the error queue with every message either replayed or explicitly dismissed with a reason; monthly restore-from-backup tests of the Mirth database, because a backup that has never been restored is a hope, not a plan; and a living interface inventory documenting each connection's owner, contact, and escalation path on the other side. None of this is glamorous. All of it is what reliable production monitoring looks like in practice, and it's the operational baseline we assume in the complete Mirth Connect guide.
Deploy Production Mirth Connect with Nirmitee
At Nirmitee, we deploy and maintain production Mirth Connect instances for healthcare organizations processing millions of messages monthly. From initial setup through high availability configuration to Kafka integration, our team handles the full lifecycle of healthcare integration infrastructure.



