Product Packaging Examples

General Guidance

Configuration of Server-Side Applications from Environment Variables

Product configuration settings are best read upon every application boot by reading any number of key-value environment variables provided to you. Such end user values cannot be known at the time you build your product. The key naming convention is to:

  • Use uppercase letters and underscores.
  • Avoid other characters, though alphanumerics with underscore and dash should not present issues.
  • Keys starting with “SKYCAPP” and “SYSTEM” are prohibited.

For example, your application may need to read the following environment variables at boot time:

ACME_LOG_LEVEL=warn
ACME_DATABASE_URL=jdbc:postgresql://acme-database-hostname/acme-database
ACME_DATABASE_USERNAME=user
ACME_DATABASE_PASSWORD=password
ACME_SITE_TITLE=My Site

In this example, your application depends on an instance of PostgreSQL, and likely a specific major version. This dependency and version will be fully modeled in the “meta-product” (aka “solution”) definition established by the system curator, who will also register the official 3rd-party PostgreSQL image to the system. However, you will still want to define some of these environment variables with working defaults at the time you build your images. (Working default settings are required for product publication to support “zero configuration” first boots.)

STOP! If you are not familiar with building images with a Dockerfile, pause here for a tutorial.

Most developers elect to have a single ‘Dockerfile’ at the root directory of your project. If necessary, you may define default environment variable values like so:

ENV ACME_LOG_LEVEL=warn
ENV ACME_DATABASE_URL=jdbc:postgresql://acme-database-hostname/acme-database

Important: These default values will be baked into your built images, and can be read by everyone that downloads them! It is therefore not appropriate to bake in any form of production credential or sensitive data into a distributable build.

The Database Hostname

Notice that the “acme-database-hostname” portion of ACME_DATABASE_URL is not a valid public DNS name, nor is it defined anywhere. Yet, we have chosen to specify a default value in the image. So long as the meta-product configuration includes a PostgreSQL instance with the default hostname of “acme-database-hostname”, your application process will be able to resolve the DNS name automatically. The IP address will be provided to the database driver as it expects.

The Database Version

Your application code surely only supports a limited range of Postgres (or other database) releases. This is considered a runtime constraint and is not defined in your build files. Rather, this is managed by the system curator as part of the Skycapp’s dependency resolution functions by establishing a dependency between your product and a PostgreSQL interface: a loosely defined concept accounting for versioned inter-product dependencies.

Curators can add trusted third-party products to the system with an “invisible” flag that prevents them from polluting the public catalog. This is particularly useful for Open Source databases such as PostgreSQL, MySQL, MariaDB, Redis, memcached etc, as well as other commonly used software including HAPI FHIR.

Configuration of Compiled Single-Page Application (SPA) Web UIs

Popular web UI frameworks such as Angular and React compile down to optimized JavaScript at image build time. This presents a problem, as user configuration settings can’t be known until the user boots the container, such as a backend data URL, but have to be delivered as a static asset. For a typical npm-based application package, the easiest solution is to:

  • Add a configuration.template.js file with placeholder references to the boot-time environment variables.
  • Generate the real configuration.js file upon container boot as a static file.
  • Load the configuration.js in your index.html <head> element, such that it is loaded prior to other JavaScript files and populates the global “window” object with the settings.

Introducing Dynamic Configuration

Here is an example configuration.template.js file. Note that any rendered configuration values will be readable by the browser!

(function(window) {
    // Environment variables
    window["ACME_SERVER_BASE_URL"] = "${ACME_SERVER_BASE_URL}";
    window["ACME_SITE_TITLE"] = "${ACME_SITE_TITLE}";
})(this);

For npm projects with a package.json, you can render the template at startup like so:

{
    "name": "acme-ui",
    "version": "0.0.0",
    "scripts": {
        "ng": "ng",
        "start": "envsubst < src/assets/configuration.template.js > src/assets/configuration.js && ng serve",
        "build": "ng build",
        "watch": "envsubst < src/assets/configuration.template.js > src/assets/configuration.js && ng build --watch --configuration development",
        "test": "ng test"
    },
    ...
}

..which generates the following configuration.js upon startup from locally-set environment variables:

(function(window) {
    // Environment variables
    window["ACME_SERVER_BASE_URL"] = "https://acme-server.example.com";
    window["ACME_SITE_TITLE"] = "My Local Acme Site";
})(this);

Loading Values Into the Client Browser

With a static configuration file now generated and served by the web server, you can add a <script> tag to your index.html file causing the browser to execute the rendered configuration.js template as JavaScript, which in turn populates the global “window” object:

<!doctype html />
<html>
<head>
    <!-- Your application stuff -->
    <script src="assets/configuration.js"></script>
</head>

<body>
    <!-- Your framework-specific stuff -->
    <app></app>
</body>
</html>

Your application now has access to the values established on the server side during web server boot, via the browser’s “window” object. Example controller code:

// A generic Angular component in TypeScript
@Component({
    selector: 'app-header',
    templateUrl: './app-header.component.html',
    styleUrls: ['./app-header.component.scss'],
    standalone: true,
    imports: [NgIf, NgFor]
})
export class AppHeaderComponent implements OnInit {

	public title: string = (window as any)["SKYCAPP_TITLE"];
	public logo_url: string = (window as any)["SKYCAPP_LOGO_URL"];

	...
}

Hands-On Examples

Web-Based Client UI

The SHARES provider portal proof of concept is a web-based UI (TypeScript, Angular, Bootstrap) application for management of FHIR Consent documents. It is a statically compiled app, and uses the configuration.template.js -based approach to boot-time configuration for enabling the browser to access environment variable read from the server side at container start. It is packaged within an nginx web server image.

https://github.com/asushares/provider

RESTful API Server and Data Loading

HL7’s DaVinci CRD server Reference Implementation (RI) is a HAPI FHIR (Java) server that loads a private “VSAC_API_KEY” credential upon boot.

https://github.com/HL7-DaVinci/CRD/blob/master/server/README.md

CDS Hooks Server

The ASU SHARES CDS Hooks server is a backend (TypeScript / Node) server that uses a backing FHIR R5 server for lookup and enforcement of Consent resource documents.

https://github.com/asushares/cds

Static Content

Template project coming soon.