Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Docker Compose

Docker Compose is the quickest way to run FerroEHR together with a preconfigured PostgreSQL 18, for local development and evaluation. This chapter describes the two published images, the Compose services, the environment variables that tune them, and the optional observability overlay. For a step-by-step first run, see Getting started.

The two images

FerroEHR publishes two container images to GHCR:

ImageContents
ghcr.io/rubentalstra/ferroehrThe ferroehr server binary. A distroless, non-root, shell-less multi-arch image (amd64 + arm64). Configured entirely via FERROEHR_* environment variables.
ghcr.io/rubentalstra/ferroehr-postgrespostgres:18.4 with the application role, the layered group roles (ferroehr_migrator, ferroehr_app, ferroehr_reader), database, schemas (ehr, ext), and required extensions (uuid-ossp, pgcrypto, pg_trgm, btree_gist) pre-created, so the app role never needs superuser.

The PostgreSQL image is init-scripts only — it creates roles, schemas, and extensions, but does not bake in migration state. The server owns the schema content and applies its migrations idempotently at every boot, so a fresh database self-provisions and a restart is a no-op.

Note

PostgreSQL init scripts run only when the data volume is empty. If you see startup notices like skipping role creation (no CREATEROLE privilege) or roles absent, your volume predates the image’s role setup (or you are running a plain postgres image): either recreate the volume (docker compose down -vdestroys data) or create the three group roles once by hand as a superuser. The server runs fine either way — the grants are a defense-in-depth layer, not a functional requirement.

Bringing up the stack

docker compose up --build

This starts the core services (no profile needed):

  • ferroehr-postgres — the database image, with a named data volume and a pg_isready healthcheck.
  • ferroehr — the server, which waits for the database to report healthy (depends_on: condition: service_healthy), then boots, migrates, and serves on port 8080. Its healthcheck is the binary’s own healthcheck subcommand (there is no shell in the image).
  • ferroehr-admin-ui — the admin console, which waits for the server to be healthy and serves on port 3000.

Note

All base images are pinned by digest (name:tag@sha256:…), not by a mutable tag, so a rebuild always resolves the exact same base. Each service also declares a memory/CPU limit (deploy.resources.limits) mirroring the Helm chart, so a local stack cannot exhaust the host.

The server’s development configuration is mounted read-only from docker/ferroehr.dev.toml to /etc/ferroehr/ferroehr.toml, where the server auto-discovers it. That file enables Basic auth with the throwaway users ferroehr / ferroehr and ferroehr-admin / ferroehr, and turns on permissive CORS — development only.

Warning

PostgreSQL 18’s official image stores data in a major-version subdirectory, so the data volume mounts at /var/lib/postgresql (the parent), not the pre-18 /var/lib/postgresql/data. The bundled Compose file already does this correctly; keep the convention if you adapt it.

Environment variables

The Compose file reads these host environment variables (with the defaults shown), so you can retune without editing it:

VariableDefaultEffect
FERROEHR_IMAGEghcr.io/rubentalstra/ferroehr:localServer image to run (set to a published tag to skip the build).
FERROEHR_POSTGRES_IMAGEghcr.io/rubentalstra/ferroehr-postgres:localDatabase image to run.
FERROEHR_PORT8080Host port mapped to the server.
FERROEHR_DB_PORT5432Host port mapped to PostgreSQL.
PG_INIT_USER / PG_INIT_PASSWORD / PG_INIT_DBferroehrApp role, password, and database created by the DB image’s init script.
POSTGRES_PASSWORDpostgresBootstrap superuser password (init only).
FERROEHR__LOG__FORMATprettyLog rendering for docker compose logs. Set json for log collectors.

The server container itself is passed FERROEHR__DB__URL (assembled from the DB variables); its ferroehr.toml is the mounted docker/ferroehr.dev.toml, auto-discovered at /etc/ferroehr/ferroehr.toml. Any other FERROEHR_* setting from the configuration reference can be added under the ferroehr service’s environment: block.

Optional services (Compose profiles)

The Compose file also defines services the quickstart does not depend on — the server defaults to Basic auth, inline multimedia and the in-process openEHR terminology, so none of them is required. Each sits behind a Compose profile and stays down until you enable it:

  • seaweedfs (--profile s3) — an S3 gateway for large DV_MULTIMEDIA externalization (development/test only). Start it with docker compose --profile s3 up, then set FERROEHR__MULTIMEDIA__ENABLED=true, FERROEHR__MULTIMEDIA__ENDPOINT=http://seaweedfs:8333, FERROEHR__MULTIMEDIA__BUCKET=openehr-multimedia, and (dev only) FERROEHR__MULTIMEDIA__ALLOW_HTTP=true. In production, point the multimedia settings at a real, credentialed, HTTPS S3 endpoint instead.

  • keycloak (--profile keycloak) — an OIDC provider with a preloaded ferroehr realm, on port 8081. Start it with docker compose --profile keycloak up; to use bearer auth instead of Basic, point the auth OIDC settings at http://localhost:8081/auth/realms/ferroehr. Its healthcheck probes the realm’s OIDC discovery document, so services can gate their startup on it being fully ready.

  • terminology (--profile terminology) — a real FHIR R4 terminology server (HAPI FHIR JPA) on port 8090, plus a one-shot container that seeds it with synthetic test code systems and value sets over the server’s own FHIR API. The profile only starts the server; pointing the CDR at it is a small overlay, so the plain quickstart is unaffected:

    docker compose --profile terminology \
      -f docker-compose.yml -f docker/sut-terminology.yml up
    

    The overlay switches on the [terminology.external] providers that docker/ferroehr.dev.toml already carries in the disabled state. See Terminology servers for what is seeded, how several servers are routed per terminology, and the fail-open/fail-closed choice.

Build provenance

Images built by CI (and any docker compose build you drive) embed a build SHA reported at /management/info and on the ferroehr_build_info metric. The build does not read .git; instead the SHA is passed as the standard REVISION build argument — the same value that fills the org.opencontainers.image.revision image label (CI uses the commit SHA; the project’s own scripts export git rev-parse --short=12 HEAD). When no value is supplied the identity falls back to the workspace version with an unknown SHA — the build never fails for lack of it.

Observability overlay

A second Compose file adds a full local telemetry stack — an OTLP collector, Prometheus, Tempo, Loki, and Grafana with a provisioned service-overview dashboard:

docker compose -f docker-compose.yml -f docker-compose.observability.yml up --build
# Grafana → http://localhost:3000

The overlay reconfigures the server for that stack: it exports traces over OTLP/gRPC (FERROEHR__TELEMETRY__OTLP_ENDPOINT), switches stdout to JSON lines (FERROEHR__LOG__FORMAT=json), and enables the management surface on its own internal port 9464 (FERROEHR__MANAGEMENT__ENABLED, FERROEHR__MANAGEMENT__PORT) with info, metrics, and prometheus set to public so the bundled Prometheus can scrape /management/prometheus without credentials. That port is only reachable on the Compose network — Grafana’s 3000 is the sole published port. Every variable uses the same FERROEHR__… grammar as the rest of the configuration reference; a single-underscore spelling is rejected at startup, not ignored.

This is the easiest way to see the server’s metrics and traces without wiring up a collector by hand. See Operations for what the server exports and how to consume it in production.

Next