Skip to content

Added docker build and quick-start #101

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/

**/.DS_Store
**/.classpath
**/.dockerignore
**/.env
**/.factorypath
**/.git
**/.gitignore
**/.idea
**/.project
**/.sts4-cache
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/.next
**/.cache
**/*.dbmdl
**/*.jfm
**/charts
**/docker-compose*
**/compose.y*ml
**/Dockerfile*
**/secrets.dev.yaml
**/values.dev.yaml
**/vendor
LICENSE
README.md
**/*.class
**/*.iml
**/*.ipr
**/*.iws
**/*.log
**/.apt_generated
**/.gradle
**/.gradletasknamecache
**/.nb-gradle
**/.springBeans
**/build
**/dist
**/gradle-app.setting
**/nbbuild
**/nbdist
**/nbproject/private
**/target
*.ctxt
.mtj.tmp
.mvn/timing.properties
buildNumber.properties
dependency-reduced-pom.xml
hs_err_pid*
pom.xml.next
pom.xml.releaseBackup
pom.xml.tag
pom.xml.versionsBackup
release.properties
replay_pid*
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.sh text eol=lf
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
hs_err_pid*
replay_pid*

##############################
## Javascript
##############################
node_modules/
.env

##############################
## Maven
##############################
Expand Down
33 changes: 33 additions & 0 deletions DEVELOPERS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# For developers and contributors

## "Easy Path" with docker

```bash
# Build using docker
docker build -t oie-dev .
# Start an ephemeral image
# NOTE: All data will be deleted on stop due to --rm. Use a volume for "real" use.
docker run --rm -p 8443:8443 oie-dev
```

Then use [Ballista](https://github.com/kayyagari/ballista) to connect to
https://localhost:8443/ and login using admin admin.

## Build Environment

To build the solution, you must have a Java 1.8 JDK+FX and Apache Ant. This
can be installed by [sdkman](https://sdkman.io/) by executing `sdkman env install`.

## Build Process

From the `server/` directory, run `ant -f mirth-build.xml -DdisableSigning=true`.

If you are using Mirth Connect Administrator Launcher, you may need to omit
`-DdisableSigning=true` to support JWS signatures. Launchers like
[Ballista](https://github.com/kayyagari/ballista) do not require signing, and
signing adds considerable time to the build process.

## Run

After build, run the server by invoking `server/mirth-server-launcher.jar`. An
example of how to do this is listed in `docker/mirth-connect.sh`.
87 changes: 87 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# syntax=docker/dockerfile:1.7-labs

# Stages:
# 1. Builder Stage: Compiles the application and resolves dependencies. Produces
# JAR files that can be deployed.
# 1a. Install dependencies
# 1b. Build the application
# 2. Runner Stage: Creates a lightweight image that runs the application using the JRE.

FROM ubuntu:noble-20250415.1 AS builder
WORKDIR /app
# sdkman requires bash
SHELL ["/bin/bash", "-c"]

# Stage 1a: Install dependencies
# Install necessary tools
COPY .sdkmanrc .
RUN apt-get update\
&& apt-get install -y zip curl\
&& curl -s "https://get.sdkman.io?ci=true" | bash \
&& source "$HOME/.sdkman/bin/sdkman-init.sh" && sdk env install \
&& rm -rf /var/lib/apt/lists/*

# Stage 1b: Build the application
# Copy the entire source tree (excluding .dockerignore files), and build
COPY --exclude=docker . .
WORKDIR /app/server
RUN source "$HOME/.sdkman/bin/sdkman-init.sh" \
&& ANT_OPTS="-Dfile.encoding=UTF8" ant -f mirth-build.xml -DdisableSigning=true

# Stage 2b: JDK runtime container
FROM eclipse-temurin:21.0.7_6-jdk-noble as jdk-run

RUN groupadd mirth \
&& usermod -l mirth ubuntu \
&& adduser mirth mirth \
&& mkdir -p /opt/connect/appdata \
&& chown -R mirth:mirth /opt/connect

WORKDIR /opt/connect
COPY --chmod=0755 docker/entrypoint.sh docker/mirth-connect.sh ./
COPY --chown=mirth:mirth --from=builder \
--exclude=cli-lib \
--exclude=mirth-cli-launcher.jar \
--exclude=mccommand \
--exclude=manager-lib \
--exclude=mirth-manager-launcher.jar \
--exclude=mcmanager \
/app/server/setup ./

VOLUME /opt/connect/appdata
VOLUME /opt/connect/custom-extensions
EXPOSE 8443

USER mirth
ENTRYPOINT [ "/opt/connect/entrypoint.sh" ]
CMD ["/opt/connect/mirth-connect.sh"]

# Stage 2b: JRE runtime container
FROM eclipse-temurin:21.0.7_6-jre-alpine as jre-run

# Alpine does not include bash by default, so we install it
RUN apk add --no-cache bash
# useradd and groupadd are not available in Alpine
RUN addgroup -S mirth \
&& adduser -S -g mirth mirth \
&& mkdir -p /opt/connect/appdata \
&& chown -R mirth:mirth /opt/connect

WORKDIR /opt/connect
COPY --chmod=0755 docker/entrypoint.sh docker/mirth-connect.sh ./
COPY --chown=mirth:mirth --from=builder \
--exclude=cli-lib \
--exclude=mirth-cli-launcher.jar \
--exclude=mccommand \
--exclude=manager-lib \
--exclude=mirth-manager-launcher.jar \
--exclude=mcmanager \
/app/server/setup ./

VOLUME /opt/connect/appdata
VOLUME /opt/connect/custom-extensions
EXPOSE 8443

USER mirth
ENTRYPOINT [ "/opt/connect/entrypoint.sh" ]
CMD ["/opt/connect/mirth-connect.sh"]
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
---
## Table of Contents

- [Quick-start](#quick-start)
- [Mission Statement](#mission-statement)
- [Overview](#overview)
- [Why Open Integration Engine?](#why-open-integration-engine)
Expand All @@ -19,6 +20,12 @@
- [Licensing](#licensing)
- [Acknowledgments](#acknowledgments)

---
## Quick-start

Start an instance of OIE with `docker run -p 8443:8443 ghcr.io/mgaffigan/oie` and
connect using [Ballista](https://github.com/kayyagari/ballista) to https://localhost:8443/ using login admin/admin.

---
## Mission Statement

Expand Down
4 changes: 4 additions & 0 deletions connectservice/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
docker-compose.yml
test.http
Readme.md
11 changes: 11 additions & 0 deletions connectservice/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM node:18
WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

EXPOSE 3000
USER 1000
CMD ["npm", "run", "start"]
21 changes: 21 additions & 0 deletions connectservice/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Connect Service

Connect Service is a sample implementation of a web service which records usage
of Mirth Server, and provides access to a notifications feed. The service implements
the operations called by the client in `ConnectServiceUtil.java`.

## Running and testing

To start the example, run `docker-compose build` in this directory, followed by
`docker-compose up -d`. You can use `http://localhost:3000` as `URL_CONNECT_SERVER`
in `ConnectServiceUtil.java`, and see recorded data
[in Kibana.](http://localhost:5601/app/management/data/index_management/indices/index_details?indexName=registration)

## Not fit for production use as-is

This example is purely for expository purposes only. Use as-is is not recommended.
At a minimum, it would be neccesary to:

- Use a reverse proxy to encrypt in-transit data
- Secure communication between the connectservice and elasticsearch
- Require authentication for elasticsearch and kibana
36 changes: 36 additions & 0 deletions connectservice/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: connectservice

volumes:
connectservice_elastic_data: {}

services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:9.0.1
environment:
- discovery.type=single-node
# Don't do this in prod.
- xpack.security.enabled=false
ports:
- "9200:9200"
volumes:
- connectservice_elastic_data:/usr/share/elasticsearch/data

connectservice:
build:
context: .
dockerfile: Dockerfile
environment:
- ELASTICSEARCH_URL=http://elasticsearch:9200
ports:
- "3000:3000"
depends_on:
- elasticsearch

kibana:
image: docker.elastic.co/kibana/kibana:9.0.1
ports:
- "5601:5601"
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
depends_on:
- elasticsearch
13 changes: 13 additions & 0 deletions connectservice/elastic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Client } from '@elastic/elasticsearch';

export const client = new Client({
node: process.env.ELASTICSEARCH_URL || 'http://localhost:9200',
auth: process.env.ELASTICSEARCH_USERNAME ? {
username: process.env.ELASTICSEARCH_USERNAME,
password: process.env.ELASTICSEARCH_PASSWORD || 'changeme',
} : undefined,
tls: {
rejectUnauthorized: process.env.ELASTICSEARCH_IGNORETLSERRORS && !!JSON.parse(process.env.ELASTICSEARCH_IGNORETLSERRORS),
},
requestTimeout: process.env.ELASTICSEARCH_TIMEOUT ? JSON.parse(process.env.ELASTICSEARCH_TIMEOUT) : 30000,
});
31 changes: 31 additions & 0 deletions connectservice/github.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const REPO_URL = process.env.NOTIFICATIONS_URL ?? 'https://api.github.com/repos/OpenIntegrationEngine/engine/releases?per_page=10';

export async function getLatestReleases() {
const response = await fetch(REPO_URL, {
headers: {
'Accept': 'application/vnd.github.html+json',
'User-Agent': 'OIEConnectService',
},
});

if (!response.ok) {
throw new Error(`Failed to fetch latest release: ${response.statusText}`);
}

const releases = await response.json();
return releases.map(release => ({
id: release.id,
name: release.name,
body_html: release.body_html,
published_at: release.published_at,
}));
}

/*
interface Release {
id: number;
name: string;
body_html: string;
published_at: string;
}
*/
50 changes: 50 additions & 0 deletions connectservice/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import express from 'express';
import * as yup from 'yup';
import { postRegistration, postUsage, registrationBodySchema, usageBodySchema } from './usage.js';
import { postNotifications, notificationBodySchema } from './notifications.js';

const app = express();

app.use(express.urlencoded({ extended: true }));

app.post('/RegistrationServlet', [validateFormUrlEncoded, makeValidator(registrationBodySchema)], postRegistration);
app.post('/UsageStatisticsServlet', [validateFormUrlEncoded, makeValidator(usageBodySchema)], postUsage);

app.post('/NotificationServlet', [validateFormUrlEncoded, makeValidator(notificationBodySchema)], postNotifications);

const port = process.env.PORT || 3000;
app.listen(port, () =>
console.log(`connectservice available from http://localhost:${port}`),
);

function validateFormUrlEncoded(req, res, next) {
const contentType = req.headers['content-type'];
if (!contentType || !contentType.startsWith('application/x-www-form-urlencoded')) {
return res.status(415).send('Unsupported content-type');
}

next();
}

function makeValidator(schema) {
return async (req, res, next) => {
try {
// abortEarly: false collects all errors rather than stopping on the first
await schema.validate(req.body, { abortEarly: false });
next();
} catch (error) {
if (error instanceof yup.ValidationError) {
return res.status(400).json({
type: error.name,
message: error.message,
errors: error.inner.map(err => ({
path: err.path,
message: err.message,
type: err.type,
})),
});
}
next(error);
}
};
}
Loading