Overview of Server Installation Process (Prima v3.x)

Prima can be installed on-site or in the cloud, but is installed and managed by each client (licensed server). This allows each customer to own and protect their data, meet their own security standards and help their own users as they see fit.

Prima consists of:

  • Prima application server (Windows Server 2022 or newer) that hosts the Windows service application(s)
  • Microsoft SQL database (version 2019 or newer)
  • Clients (mainly Windows desktop, some internal web)
  • Digital storage (documents, slide images, etc)
In general, it is best to have two or three system set-ups: one for production and a scaled-down version for user and upgrade testing (and optionally a thrid for validation in a GLP environment). There are two stages to the installation: the prep work that is included here and running the installer. The installer does a lot for us. Not only does it install the software, but it configures the system to use SSL certs, configures firewall settings (locally), LDAP/AD and more. If all the prep work is completed and there aren't any hidden (e.g. security) hiccups, the Prima installation should only take 20 minutes.

Setting Up Server Hardware

As a general stance on supported versions of software, Fortelinea stays very much on top of using all new performance and security measures in new versions of software. This means newer versions are always better, as far as we are concerned.

Hardware Requirements

Hardware requirements can vary quite a bit from lab to lab based on the usage of the system. We like to set both minimum and recommended requirements. We find that, if as much as possible is done using virtual machines, systems can later be tweaked based on how the lab ends up using the system.

Note that recommended requirements are based on a lab with about 12 stations/users.

Prima Application Server

Parameter Minimum Recommended
CPU 3.0GHz, 2 cores 3.4+GHz, 8 cores
Memory 8GB 32GB
Storage 32GB 64GB
Operating System Windows Server 2019* Windows Server 2022 (or newer)

* Windows Server 2019 does not fully support grpc services and will incur a significant performance hit

Prima Database Server

While we list specs for a database server, feel free to host the database on a shared server, given the overhead to support Prima is available. If you choose to allocate an SQL server VM to Prima, I would recommend using one for both the test and production databases (two databases on a single server). This will save on costs and be one less thing to manage.

Expect database growth in the neighborhood of 1GB per year

Parameter Minimum Recommended
CPU 3.0GHz, 2 cores 3.4+GHz, 4 cores
Memory 16GB 64GB
Storage 8GB (prefer autogrow) 16GB (prefer autogrow)
Operating System SQL Server 2019 SQL Server 2022

Shared Storage

Storage is highly configurable, but is separated here for ease of planning and use.

Parameter Minimum Recommended
Digital Documents 4GB 80GB
Digital Images 16GB 1TB

Installing Features and Drivers, Etc.

  • Assign a vanity name to the servers (e.g. prima.mycompany.com, prima-test.mycompany.com or whatever you want)

    Using a human-readable vanity name makes remembering the server address easier when installing clients, accessing your internal Prima website, etc.

  • Ensure that the .NET 10 Desktop Runtime and the ASP.NET Core runtime are installed on the application server
  • It's highly recommended to install Visual Studio Code for editing config files on the app server. It will save a ton of time, effort and frustration.

Service Users and Access

User Setup
  1. Fortelinea Team members will need access to the application server and database. This will save untold amounts of time in the long run. There are three methods, so choose one that best fits your scenario:
    1. Recommended Add a new account for the Fortelinea dev team (e.g. FortelineaPrimaAdmin). This account should have admin rights on the app server and full rights to the database. This account will be used for installations and updates.
    2. Setup individual accounts for at least two Fortelinea developers (will require more maintenance). These accounts will be used for installations and updates (any further mentions of FortelineaPrimaAdmin will apply to these individual accounts).
    3. Assign an IT contact that can allow access or perform the work over a screen share. This is fairly common, but not recommended; Fortelinea support cannot respond ot the lab as quickly and any work over the screen share is much slower
  2. Add a new service account for the application and database (e.g. prima). This account should have db_reader and db_writer access to the SQL database and the ability to read and write files from a network file share.
  3. Add a new admin account for the database (e.g. primaAdmin). This account is used by the installation/upgrade process to make table changes and other migrations. Separating this access to a different database user is an extra step in security, but you could choose to give this level of access to the service user instead.

SSL Certificates

Prima uses an SSL certificate to encrypt traffic sent to the clients, just like an https website. While many companies have a system that places certificates on all computers, some may need to generate a certificate manually. There are only a few requirements that must be met:

  • The certificate must match the name of the computer (this can be a vanity name, computer name or a wildcard)
      If using a vanity name:
    • The certificate must list the vanity name
    • The certificate should list the ip and/or any standard names (e.g. WK0123L21.mycompany.com) as secondary names
  • The Prima service user account must have access/privileges to the private key
  • The cert should be signed by your CA or an outside trusted CA; the cert needs to be trusted by the clients that will connect
  • The cert should be placed in LocalMachine\My of the certificate store
  • The cert should be at least 256 bit

Network Prep

Prima will communicate over your network to clients, hardware and other computer systems. It will also communicate with our web server. By ensuring all paths are clear, we can save a lot of troubleshooting time.

Ports to clients
80, 443 (standard http and https)
Prima Web service
https://prima.fortelinea.com
Ventana Connect
Port 55550 (IP on your network)
Arcos block and slide storage
(IP on your network)
Other
Discussed prior to installation

Also:

  • Ensure that any network proxy information has been set up. Prima will ask Windows for this information and will attempt to use it.
  • Make sure that a newer, secure browser on the application server can reach https://prima.fortelinea.com and that js is enabled. We will use this for downloading installers, updates, grabbing license info, troubleshooting, etc.

Record Settings for Prima Installation

Before we run the Prima installer, you'll want to make sure you have the following:

  • Prima does a great job of finding your Active Directory server, but it is best to be prepared. Make sure you know the address and port.
  • Know the connection string (or at least the address and relevant parameters) for the SQL server.

Extra Installation Info and Troubleshooting

SQL Backup

It is highly recommended to set up a database backup plan. If a backup/restore is something that you can allow Fortelinea or the prima database admin user to do, it can save time and effort later.

Troubleshooting

Prima Website Logs
As long as the application has access to talk to our server (https://prima.fortelinea.com), we get and log a variety of detailed information on crashes, exceptions, and configuration warnings. Usually, the website logs are the first tool Fortelinea developers use when troubleshooting.
Event Viewer
The Windows Event viewer is a great place to look. Most serious logs end up here. Be sure to check under Application and System event logs.
File logs
There are times when exceptions don't make it to the event viewer due to a crash and the logs don't make it to our website. The fallback are textual log files which can be found at C:\ProgramData\FortelineaSoftwareSystems\Prima\Logs

Server Installation Checklist

The following is meant to be a brief guideline, but the detailed sections should be followed for specifics:

  1. Requisition two Windows Server VMs for the Prima Application Server (test and production)
    1. Ensure a modern web browser (with js enable) is installed and can reach https://prima.fortelinea.com (set Windows proxy settings, if necessary)
    2. Ensure .Net Core Desktop runtime is installed
    3. Install a better editor than Notepad (e.g. Visual Studio Code)
    4. Create a Prima service user (network-capable) and assign logon rights
    5. Assign vanity names to the Prima servers
    6. Setup SSL certificates on the two application servers (ensure service user has PK rights)
    7. Download the 'packaged installer' from Fortelinea's website (https://prima.fortelinea.com/SoftwareRelease)
  2. Requisition an SQL Server (most cost-effective to have one SQL server with multiple Prima databases)
    1. Create two SQL databases: one for prod and one for test
    2. Create a Prima database admin user and assign db_owner rights to each of the Prima databases
    3. Ensure service user has read/write/logon for each database
    4. Ensure the SQL server has network access turned on and the appropriate ports are open

Docker Deployment

Pull prebuilt images from prima-registry.fortelinea.com and run the full Prima stack with Docker Compose — no build toolchain required on the production host.

Architecture

The stack runs six services:

Service Purpose Scaling Ports (host:container)
rabbitmq Message broker Single instance only — the bundled broker is not clustered 5672, 15672 (management UI)
dbinit Run-to-completion job: EF Core migrations + optional fresh-DB seed Runs once per deploy, then exits — not a persistent service none
sync Lock coordinator: the other services take their one-at-a-time locks here (case numbers, instrument loads, containers, protocols) Single instance only — locks live in its memory, so two instances would hand out the same lock none (internal only)
worker Singleton background services + Hangfire server Single instance only — do not run more than one at a time none
web-api REST, Razor pages, gRPC, OpenIddict Stateless — can run multiple instances behind a load balancer 8080 (gRPC), 8081 (HTTP)
notification gRPC streaming endpoints for client connections Can run multiple instances 8082 → 8080 (gRPC), 8083 → 8081 (HTTP/1 health)

dbinit runs first; worker, web-api, and notification wait for it to exit successfully (service_completed_successfully) and for rabbitmq and sync to be healthy. notification also waits for web-api to be healthy.

Prerequisites

  • Docker Desktop (Windows/Mac) or Docker Engine (Linux)
  • A reachable SQL Server instance (this stack does not include a database container)
  • A TLS terminator in front of the stack (a load balancer, or nginx, Caddy, HAProxy, etc. on the host) — see TLS termination

Registry Access

Log in with the API client id and secret provided by Fortelinea (created with the registry:pull scope) as username and password:

docker login prima-registry.fortelinea.com

If you ever need to pull an image directly (e.g. for testing, or to reference in your own deployment tooling), the registry namespace fortelinea-prima is required — it is not a placeholder or something you choose:

docker pull prima-registry.fortelinea.com/fortelinea-prima/prima-webservice:<version>

Omitting the fortelinea-prima segment produces an "authentication required" error even with valid login credentials, since it resolves to a repository that doesn't exist.

Production Deployment

docker/docker-compose.prod.yml is a standalone file that pulls prebuilt images from prima-registry.fortelinea.com instead of building from source. Use it alone — do not layer it on the base file:

  1. Create a docker folder and save the compose file below into it as docker-compose.prod.yml.
  2. In the same folder, create a .env file with the variables in Environment Variables. Docker Compose reads .env from the compose file's folder.
  3. Create the two token certificate files in docker/token-certs — see Token certificates. web-api will not start without them.
  4. Set up your TLS terminator (TLS termination).
  5. From the folder that contains docker, start the stack:
    # Pin a version (defaults to "latest" if unset)
    export PRIMA_VERSION=3.2.1
    docker compose -f docker/docker-compose.prod.yml up -d

NUGET_PAT is not required — nothing is built.

docker-compose.prod.yml
# Prima standalone production compose file.
# Used ALONE (not layered on docker-compose.yml):
#   docker compose -f docker/docker-compose.prod.yml up
# Pulls prebuilt images from the Prima registry instead of building from source.
# Requires docker/.env (copy from docker/.env.example); set PRIMA_VERSION to pin a tag.
#
# ⚠️ TLS TERMINATION IS REQUIRED IN FRONT OF THIS STACK ⚠️
# All service ports below bind to 127.0.0.1 ONLY. An external TLS terminator
# (cloud load balancer, customer-managed nginx/Caddy/HAProxy, etc.) MUST
# proxy public traffic to these loopback ports — never publish them on a
# public interface in cleartext. /connect/token sign-ins, bearer tokens and
# gRPC frames would otherwise traverse the network unencrypted.

# Shared logging config — JSON file driver with rotation so a misbehaving
# service can't fill the disk. Override per-service if a particular service
# needs more retention.
x-logging: &default-logging
  driver: json-file
  options:
    max-size: "50m"
    max-file: "5"

# Pin the project name so this stack can run alongside sibling projects that
# also live in a folder called `docker/` (e.g. provantisconnector) without
# service-name collisions.
name: prima

services:

  rabbitmq:
    image: rabbitmq:3-management
    # Bind to loopback only. Cross-host federation should run via the
    # external TLS terminator, not direct exposure.
    ports:
      - "127.0.0.1:5672:5672"
      - "127.0.0.1:15672:15672"
    environment:
      RABBITMQ_DEFAULT_USER: prima
      RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "check_port_connectivity"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq
    restart: unless-stopped
    logging: *default-logging
    deploy:
      resources:
        limits:
          memory: 2g

  dbinit:
    image: prima-registry.fortelinea.com/fortelinea-prima/prima-dbinit:${PRIMA_VERSION:-latest}
    environment:
      ConnectionStrings__PrimaContext: ${DB_CONNECTION_STRING}
      FortelineaWebConfig__ClientId: ${FortelineaWebConfig__ClientId:-}
      FortelineaWebConfig__ClientSecret: ${FortelineaWebConfig__ClientSecret:-}
      FortelineaWebConfig__BaseUrl: ${FortelineaWebConfig__BaseUrl:-https://prima.fortelinea.com}
      DBINIT_LAB_SITE_GUID: ${DBINIT_LAB_SITE_GUID:-}
      DBINIT_ADMIN_EMAIL: ${DBINIT_ADMIN_EMAIL:-}
      DBINIT_ADMIN_USERNAME: ${DBINIT_ADMIN_USERNAME:-}
    command: ${DBINIT_COMMAND:-update}
    restart: "no"
    logging: *default-logging

  # Lock coordinator. MUST run as exactly one instance; internal only (no ports).
  sync:
    image: prima-registry.fortelinea.com/fortelinea-prima/prima-sync:${PRIMA_VERSION:-latest}
    environment:
      ASPNETCORE_ENVIRONMENT: Docker
      # Signs the internal lock-service tokens. MUST be identical on sync,
      # worker, web-api and notification. Required: without it the images use
      # the well-known development secret.
      JwtTokenSettings__Secret: ${JWT_SECRET:?Set JWT_SECRET in docker/.env}
    deploy:
      replicas: 1
      restart_policy:
        condition: on-failure
      resources:
        limits:
          memory: 256m
    healthcheck:
      test: ["CMD", "curl", "-sf", "http://localhost:8081/health/live"]
      interval: 30s
      timeout: 10s
      start_period: 20s
      retries: 3
    logging: *default-logging

  worker:
    image: prima-registry.fortelinea.com/fortelinea-prima/prima-worker:${PRIMA_VERSION:-latest}
    depends_on:
      sync:
        condition: service_healthy
      rabbitmq:
        condition: service_healthy
      dbinit:
        condition: service_completed_successfully
    environment:
      ASPNETCORE_ENVIRONMENT: Docker
      ConnectionStrings__PrimaProduction: ${DB_CONNECTION_STRING}
      RabbitMq__Username: prima
      RabbitMq__Password: ${RABBITMQ_PASSWORD}
      # Signs the internal lock-service tokens. MUST be identical on sync,
      # worker, web-api and notification. Required: without it the images use
      # the well-known development secret.
      JwtTokenSettings__Secret: ${JWT_SECRET:?Set JWT_SECRET in docker/.env}
      # Fortelinea cloud credentials and address, needed by every Prima component
      FortelineaWebConfig__ClientId: ${FortelineaWebConfig__ClientId:-}
      FortelineaWebConfig__ClientSecret: ${FortelineaWebConfig__ClientSecret:-}
      FortelineaWebConfig__BaseUrl: ${FortelineaWebConfig__BaseUrl:-https://prima.fortelinea.com}
    volumes:
      - slide_images:/app/data/slide-images
      - specimen_images:/app/data/specimen-images
      - scanned_docs:/app/data/scanned-documents
    deploy:
      replicas: 1
      restart_policy:
        condition: on-failure
      resources:
        limits:
          memory: 2g
    healthcheck:
      test: ["CMD", "curl", "-sf", "http://localhost:8080/health/live"]
      interval: 30s
      timeout: 10s
      start_period: 60s
      retries: 3
    logging: *default-logging

  web-api:
    image: prima-registry.fortelinea.com/fortelinea-prima/prima-webservice:${PRIMA_VERSION:-latest}
    depends_on:
      sync:
        condition: service_healthy
      rabbitmq:
        condition: service_healthy
      dbinit:
        condition: service_completed_successfully
    # ⚠️ Loopback-bound — REQUIRES an external TLS terminator. Never change
    # to 0.0.0.0 without putting HTTPS in front; /connect/token traffic and
    # JWTs would traverse the network in cleartext.
    ports:
      - "127.0.0.1:8080:8080"
      - "127.0.0.1:8081:8081"
    environment:
      ASPNETCORE_ENVIRONMENT: Docker
      ConnectionStrings__PrimaProduction: ${DB_CONNECTION_STRING}
      RabbitMq__Username: prima
      RabbitMq__Password: ${RABBITMQ_PASSWORD}
      # Signs the internal lock-service tokens. MUST be identical on sync,
      # worker, web-api and notification. Required: without it the images use
      # the well-known development secret.
      JwtTokenSettings__Secret: ${JWT_SECRET:?Set JWT_SECRET in docker/.env}
      # Fortelinea cloud credentials and address, needed by every Prima component
      FortelineaWebConfig__ClientId: ${FortelineaWebConfig__ClientId:-}
      FortelineaWebConfig__ClientSecret: ${FortelineaWebConfig__ClientSecret:-}
      FortelineaWebConfig__BaseUrl: ${FortelineaWebConfig__BaseUrl:-https://prima.fortelinea.com}
      # Public HTTPS address of the TLS terminator in front of web-api
      TokenServer__Issuer: ${TOKEN_SERVER_ISSUER}
      TokenServer__SigningCertificatePassword: ${TOKEN_CERTIFICATE_PASSWORD}
      TokenServer__EncryptionCertificatePassword: ${TOKEN_CERTIFICATE_PASSWORD}
    volumes:
      # signing.pfx + encryption.pfx — see "Token certificates" in README.md
      - ${TOKEN_CERTIFICATE_DIRECTORY:-./token-certs}:/app/token-certs:ro
      - slide_images:/app/data/slide-images
      - specimen_images:/app/data/specimen-images
      - cassette_images:/app/data/cassette-images
      - scanned_docs:/app/data/scanned-documents
    healthcheck:
      test: ["CMD", "curl", "-sf", "http://localhost:8081/health/live"]
      interval: 30s
      timeout: 10s
      start_period: 60s
      retries: 3
    restart: unless-stopped
    logging: *default-logging
    deploy:
      resources:
        limits:
          memory: 4g

  notification:
    image: prima-registry.fortelinea.com/fortelinea-prima/prima-notification:${PRIMA_VERSION:-latest}
    depends_on:
      sync:
        condition: service_healthy
      rabbitmq:
        condition: service_healthy
      web-api:
        condition: service_healthy
      dbinit:
        condition: service_completed_successfully
    # ⚠️ Loopback-bound — REQUIRES an external TLS terminator (see web-api).
    ports:
      # Host 8082 → container 8080 (gRPC/HTTP2 streaming endpoints).
      # Host 8083 → container 8081 (HTTP/1 health endpoint).
      - "127.0.0.1:8082:8080"
      - "127.0.0.1:8083:8081"
    environment:
      ASPNETCORE_ENVIRONMENT: Docker
      ConnectionStrings__PrimaProduction: ${DB_CONNECTION_STRING}
      RabbitMq__Username: prima
      RabbitMq__Password: ${RABBITMQ_PASSWORD}
      # Signs the internal lock-service tokens. MUST be identical on sync,
      # worker, web-api and notification. Required: without it the images use
      # the well-known development secret.
      JwtTokenSettings__Secret: ${JWT_SECRET:?Set JWT_SECRET in docker/.env}
      # Fortelinea cloud credentials and address, needed by every Prima component
      FortelineaWebConfig__ClientId: ${FortelineaWebConfig__ClientId:-}
      FortelineaWebConfig__ClientSecret: ${FortelineaWebConfig__ClientSecret:-}
      FortelineaWebConfig__BaseUrl: ${FortelineaWebConfig__BaseUrl:-https://prima.fortelinea.com}
      # Reads the token server's signing keys from web-api's discovery document
      TokenServer__Authority: http://web-api:8081
    healthcheck:
      test: ["CMD", "curl", "-sf", "http://localhost:8081/health/live"]
      interval: 30s
      timeout: 10s
      start_period: 90s
      retries: 3
    restart: unless-stopped
    logging: *default-logging
    deploy:
      resources:
        limits:
          memory: 2g

volumes:
  rabbitmq_data:
  slide_images:
  specimen_images:
  cassette_images:
  scanned_docs:

Environment Variables

Variable Description
DB_CONNECTION_STRING SQL Server connection string used by all Prima services
RABBITMQ_PASSWORD Password for the prima RabbitMQ broker user
TOKEN_CERTIFICATE_PASSWORD Password of the token server's signing.pfx and encryption.pfx. Required in production
TOKEN_CERTIFICATE_DIRECTORY Host folder holding signing.pfx and encryption.pfx. Defaults to ./token-certs (beside the compose file)
TOKEN_SERVER_ISSUER Public HTTPS address clients reach Prima on (e.g. https://prima.your-org.com)
JWT_SECRET Required. Signs the internal tokens the services present to the sync lock coordinator; passed to sync, worker, web-api, and notification. Use a long random value (at least 128 bits). Compose refuses to start without it.
FortelineaWebConfig__ClientId Prima website login email (provided by Fortelinea). Used by every Prima service, and by the fresh-DB seed
FortelineaWebConfig__ClientSecret Prima website login password (provided by Fortelinea). Used by every Prima service, and by the fresh-DB seed
FortelineaWebConfig__BaseUrl (optional) Fortelinea cloud address. Defaults to https://prima.fortelinea.com
DBINIT_COMMAND (optional) DbInit verb. Defaults to update (migrations only)
DBINIT_LAB_SITE_GUID (optional) Lab site GUID to configure — fresh-DB seed
DBINIT_ADMIN_EMAIL (optional) Initial admin email — fresh-DB seed
DBINIT_ADMIN_USERNAME (optional) Initial admin username — fresh-DB seed

The three DBINIT_* seed variables are used together with the two FortelineaWebConfig credentials. When all five are set and the database is fresh, dbinit runs update --seed and configures the lab site. Otherwise dbinit runs migrations only.

Token certificates

web-api signs the tokens it issues (sign-ins, API access) and encrypts the ones only it reads back (such as refresh tokens) with two certificates, read from /app/token-certs/signing.pfx and /app/token-certs/encryption.pfx. Create them once and keep them: replacing them signs everyone out and invalidates every refresh token integrations hold.

Put both files in TOKEN_CERTIFICATE_DIRECTORY (default token-certs beside the compose file), protected with TOKEN_CERTIFICATE_PASSWORD. To create them, run these from the docker folder:

export TOKEN_CERTIFICATE_PASSWORD='a long random value'
mkdir -p token-certs && cd token-certs
openssl req -x509 -newkey rsa:3072 -nodes -keyout signing.key -out signing.crt -days 3650 \
  -subj "/CN=Prima Token Signing" -addext "keyUsage=critical,digitalSignature"
openssl pkcs12 -export -inkey signing.key -in signing.crt -out signing.pfx -passout env:TOKEN_CERTIFICATE_PASSWORD
openssl req -x509 -newkey rsa:3072 -nodes -keyout encryption.key -out encryption.crt -days 3650 \
  -subj "/CN=Prima Token Encryption" -addext "keyUsage=critical,keyEncipherment"
openssl pkcs12 -export -inkey encryption.key -in encryption.crt -out encryption.pfx -passout env:TOKEN_CERTIFICATE_PASSWORD
rm signing.key signing.crt encryption.key encryption.crt
  • The files must be readable by the container's app user (uid 1654).
  • Every web-api instance must use the same two files.
  • A certificate that is missing, has the wrong password, or cannot do its job stops web-api at startup with a message naming the TokenServer setting to fix.
Platforms that cannot mount files (for example DigitalOcean App Platform, or a Kubernetes Secret exposed as environment variables): set TokenServer__SigningCertificateBase64 and TokenServer__EncryptionCertificateBase64 to the PFX files' contents (base64 -w0 signing.pfx), set the two passwords as usual (TokenServer__SigningCertificatePassword / TokenServer__EncryptionCertificatePassword), and set TokenServer__SigningCertificatePath and TokenServer__EncryptionCertificatePath to an empty value so the image's default paths do not apply.

These are not HTTPS certificates. web-api runs on plain HTTP behind your TLS terminator; notification reads the token server's public keys from web-api's discovery document over the internal network (TokenServer__Authority: http://web-api:8081, already set in the compose file).

TLS termination

Every port in docker-compose.prod.yml is bound to 127.0.0.1 only, and the services speak plain HTTP. A TLS terminator on the same host (or a load balancer that reaches it) must accept HTTPS on your public hostname and route by path to three backends:

Path Backend Protocol
/grpc/pubsub/ 127.0.0.1:8082 (notification) HTTP/2 cleartext (h2c, gRPC)
/grpc/ 127.0.0.1:8080 (web-api) HTTP/2 cleartext (h2c, gRPC)
/ (everything else) 127.0.0.1:8081 (web-api) HTTP/1.1 (REST, /connect/token, web pages, WebSocket upgrades)
  • The most specific path must win: /grpc/pubsub/ before /grpc/ before /.
  • The terminator must offer HTTP/2 to clients and forward gRPC to the backends as HTTP/2 (for nginx, grpc_pass, not proxy_pass). Port 8080 rejects HTTP/1.1.
  • Allow long-lived gRPC calls: notification streams stay open, so raise read/send timeouts (for example 1 hour).
  • Pass the original Host header and set X-Forwarded-Proto (and X-Forwarded-For) on the HTTP/1.1 route, so Prima builds https:// addresses on your hostname.
  • The public address must match TOKEN_SERVER_ISSUER.

The desktop applications use that one address for everything: "PrimaGrpcApiUrl": "https://prima.your-org.com".

Data volumes and backups

Named Docker volumes hold the broker's data and stored images:

Volume Purpose
rabbitmq_dataRabbitMQ persistence
slide_imagesScanned slide images
specimen_imagesSpecimen photographs
cassette_imagesCassette images (web-api only)
scanned_docsRequisition forms and other scanned documents

The image volumes hold patient data: include them, along with the SQL Server database and your token certificate files, in your backups. For production, consider mounting host directories or network shares in place of named volumes so they are easier to back up.

Health Checks

sync, worker, web-api, and notification each expose a health check probing /health/live. Check container health with:

docker inspect --format='{{.State.Health.Status}}' <container-name>

Troubleshooting

.env file causes errors when sourcing

If you see \r: command not found, the file has Windows line endings:

sed -i 's/\r$//' docker/.env

Port conflicts

If ports 8080/8081/8082/8083 or 5672/15672 are already in use on the host, change the host side of the ports: mappings in docker/docker-compose.prod.yml (keep the 127.0.0.1: prefix), and point your TLS terminator at the new ports.

A service won't start

Confirm dbinit exited successfully and rabbitmq and sync are healthy — the app services block on all three (and notification also on web-api):

docker compose -f docker/docker-compose.prod.yml ps
docker compose -f docker/docker-compose.prod.yml logs dbinit

If web-api exits at startup, check its log for a TokenServer message — the token certificate files are missing, unreadable, or have the wrong password (see Token certificates):

docker compose -f docker/docker-compose.prod.yml logs web-api

OpenShift Deployment

Deploy Prima onto an OpenShift cluster using the provided manifests. Download the YAML files below, then work through the sections in order.

The manifests are plain Kubernetes (1.25 or newer) apart from the three OpenShift Route objects in 60-web-api.yaml and 80-notification.yaml. On another Kubernetes distribution, replace those with an Ingress (or Gateway) that does the same path routing over HTTP/2 — see Architecture.

Download YAML Files

Architecture

All external traffic — REST, the Prima desktop (WPF) apps over gRPC, and notification streams over gRPC — uses one hostname. Three OpenShift Routes share that host and fan out by URL path.

                  Browsers, WPF apps, external REST clients
                                   |
                                   | HTTPS  (HTTP/1 or HTTP/2 via ALPN)
                                   v
                   +------------------------------------+
                   |  OpenShift router (HAProxy)        |
                   |  Single host: prima.example.com    |
                   |  Edge-TLS termination              |
                   +-+------------+-----------+---------+
                     |            |           |
              path:  | /grpc/     | /grpc/    | (catch-all)
                     | pubsub     |           |
                     |            |           |
                     v            v           v
              +-------------+ +-------+  +-------+
              | notification| |web-api| |web-api|
              |   :8080 h2c | |:8080  | |:8081  |
              | Route       | |h2c    | |HTTP/1 |
              | notification| |Route  | |Route  |
              |   -grpc     | |web-api| |web-api|
              +-------------+ |-grpc  | |-rest  |
                              +---+---+ +---+---+
                                  |         |
                                  v         v
                              +-----------------+
                              | Service web-api |  ClusterIP
                              | (grpc + http)   |
                              +--------+--------+
                                       |
                                       v
                              +-----------------+
                              |   web-api Pod   |
                              +--+-----------+--+
                                 |           |
                                 | gRPC      | AMQP
                                 v           v
                      +-----------------+ +----------+
                      |   notification  | | rabbitmq |
                      | (ClusterIP svc) | |          |
                      +-----------------+ +----+-----+
                                               ^
                                               | AMQP
                                               |
                                        +-------------+
                                        |   worker    |   (no Service)
                                        +-------------+

  web-api, worker and notification all take their locks from:

                      +-----------------+
                      |      sync       |   lock coordinator, exactly one
                      | (ClusterIP svc) |   replica, no Route
                      +-----------------+

Path matching (longest-prefix wins):

Path Backend Protocol
/grpc/pubsub/... notification:8080 h2c (gRPC)
/grpc/... web-api:8080 h2c (gRPC)
/... (catch-all) web-api:8081 HTTP/1 (REST, /connect/token, Razor, SignalR)

Storage

  • rabbitmq-data — a single 10 Gi ReadWriteOnce (RWO) volume, used only by the bundled RabbitMQ broker.
  • Images, scanned documents, and label templates are stored in S3 object storage, so there are no in-cluster image volumes and no RWX (ReadWriteMany) StorageClass is required.

What you provide

  • A SQL Server instance reachable from the cluster.
  • S3 (or S3-compatible) object storage plus an access key / secret key pair — for images, scanned documents, and label templates.
  • Registry pull credentials for prima-registry.fortelinea.com (provided by Fortelinea).
  • A DNS hostname for the public Route (or accept the OpenShift-assigned hostname).
  • Two token certificates (signing and encryption) for the token server — generated once with openssl, see step 5.
  • (Optional) an application registration at Okta or Microsoft Entra ID, if users will sign in through single sign-on — see Single sign-on.
  • (Optional) an external RabbitMQ broker, if you'd rather not run the bundled one — see External RabbitMQ broker.

Prerequisites

Requirement Notes
OpenShift 4.11+ oc CLI logged in. 4.11 is the minimum version where the appProtocol: h2c Service port hint is honored by the router (required for gRPC). For OCP 4.9–4.10, additional manual router config is needed — contact Fortelinea.
HTTP/2 enabled on the IngressController One-time cluster-admin action so gRPC Routes work end-to-end. See Cluster prerequisite below.
SQL Server TCP-reachable from the cluster. Customer-managed. Connection string goes into the Secret.
S3 object storage An S3 bucket (AWS S3 or any S3-compatible store) for images, scanned documents, and label templates, plus an access key / secret key pair. Prima writes images here instead of to in-cluster volumes, so no RWX StorageClass is required. Bucket names and region are set on the Deployments; the keys go into the Secret.
(default) StorageClass Only the bundled RabbitMQ broker needs a PVC, and it is RWO — satisfied by virtually any StorageClass, so the cluster default is fine. If you use an external RabbitMQ broker you need no PVCs at all.
Registry credentials Username + access token for prima-registry.fortelinea.com, provided by Fortelinea. Image references must include the fortelinea-prima namespace, e.g. prima-registry.fortelinea.com/fortelinea-prima/prima-webservice:<version> — it is required, not a placeholder.
TLS certificate Optional — supply a custom cert for the Routes, or let OpenShift use its default ingress cert. WPF clients must trust the cert chain.

One-time setup

Deploying Prima involves two roles: a cluster administrator (for the one-time HTTP/2 step described below) and an application deployer (for everything else). These can be the same person if you have cluster-admin rights.

1. Create a project

oc new-project prima

2. Enable HTTP/2 on the IngressController (cluster-admin required)

The Prima WPF apps connect to the gRPC Route over HTTPS/HTTP-2. The default OpenShift IngressController does not have HTTP/2 enabled until you set the annotation below. This is a one-time, cluster-wide action a cluster admin must perform before applying 60-web-api.yaml:

oc -n openshift-ingress-operator annotate \
  ingresscontroller/default \
  ingress.operator.openshift.io/default-enable-http2=true

The default ingress router will roll automatically. Verify after rollout:

oc -n openshift-ingress get pods -l ingresscontroller.operator.openshift.io/deployment-ingresscontroller=default

If your cluster has multiple custom IngressControllers and Prima will be exposed through a non-default one, apply the same annotation to that IngressController.

3. Create the image pull secret

Every Prima Deployment and the dbinit Job pull their images with a secret named prima-registry-pull. Use the registry username and access token provided by Fortelinea:

oc create secret docker-registry prima-registry-pull \
  --docker-server=prima-registry.fortelinea.com \
  --docker-username='<your-registry-username>' \
  --docker-password='<your-registry-token>'

Without it, every Prima pod stays in ImagePullBackOff.

4. Create the application secrets

cp 20-secrets.example.yaml 20-secrets.yaml
# Edit 20-secrets.yaml — replace every CHANGE_ME value.
oc apply -f 20-secrets.yaml
Do not commit 20-secrets.yaml. The .example suffix on the template is intentional; add 20-secrets.yaml to your own ignore list if you keep this directory under version control.

Notable keys:

  • jwt-secret — signs the internal tokens the Prima pods present to the sync lock coordinator. Web-api, worker, notification, and sync all read this one key. Use a long random value (≥128 bits); if it is left unset the services fall back to a well-known development value, so never run production that way. (Sign-in tokens are signed with the token certificates from step 5, not with this.)
  • token-certificate-password — the password of the two token certificate files created in step 5.
  • fortelinea-client-id / fortelinea-client-secret — the Prima website credentials provided by Fortelinea. Every Prima component uses them, and dbinit uses them for the first-run seed.
  • oidc-client-secret — optional; leave blank unless you turn on single sign-on.

5. Create the token certificates

Web-api signs the tokens it issues, and encrypts the ones only it reads back (such as refresh tokens), with two certificates, kept in their own Secret, prima-token-certificates, and mounted at /app/token-certs. Create them once and keep them: replacing them signs everyone out and invalidates every refresh token integrations hold. The password is token-certificate-password from 20-secrets.yaml.

export TOKEN_CERTIFICATE_PASSWORD='the token-certificate-password value'
openssl req -x509 -newkey rsa:3072 -nodes -keyout signing.key -out signing.crt -days 3650 \
  -subj "/CN=Prima Token Signing" -addext "keyUsage=critical,digitalSignature"
openssl pkcs12 -export -inkey signing.key -in signing.crt -out signing.pfx -passout env:TOKEN_CERTIFICATE_PASSWORD
openssl req -x509 -newkey rsa:3072 -nodes -keyout encryption.key -out encryption.crt -days 3650 \
  -subj "/CN=Prima Token Encryption" -addext "keyUsage=critical,keyEncipherment"
openssl pkcs12 -export -inkey encryption.key -in encryption.crt -out encryption.pfx -passout env:TOKEN_CERTIFICATE_PASSWORD
oc create secret generic prima-token-certificates --from-file=signing.pfx --from-file=encryption.pfx
rm signing.key signing.crt encryption.key encryption.crt signing.pfx encryption.pfx

These are not HTTPS certificates — the Routes still terminate TLS with the ingress certificate. A token certificate that is missing, has the wrong password, or cannot do its job stops web-api at startup with a message naming the TokenServer setting to fix (oc logs deploy/web-api).

6. Create the persistent volume claim

Only the bundled RabbitMQ broker needs persistent storage (a single RWO PVC). Images and documents go to S3, so there are no image PVCs.

oc apply -f 30-pvcs.yaml

Verify it reaches Bound:

oc get pvc -w

If it stays Pending, check oc describe pvc rabbitmq-data — usually no default StorageClass is set on the cluster.

Using an external RabbitMQ broker? Skip this step and don't apply 40-rabbitmq.yaml — Prima then needs no PVCs at all. See External RabbitMQ broker.

7. Set the public hostname

All three Routes share one host: value. Replace the placeholder before applying anything — the same hostname must appear in 60-web-api.yaml (two Routes) and 80-notification.yaml (one Route):

sed -i 's/prima\.example\.com/prima.your-org.com/g' 60-web-api.yaml 80-notification.yaml

The same command also sets TokenServer__Issuer in 60-web-api.yaml — the public address (https:// + the Route host) that the tokens web-api issues carry as their issuer.

Deploy

The files use numeric prefixes so oc apply -f . would almost work, but dbinit must complete before the long-running services start. Apply in two stages:

# Stage 1 — broker + database migrations
# (Using an external RabbitMQ broker? Skip the 40-rabbitmq.yaml line.)
oc apply -f 40-rabbitmq.yaml
oc apply -f 50-dbinit-job.yaml
oc wait --for=condition=complete --timeout=10m job/prima-dbinit

# Stage 2 — application services
oc apply -f 55-sync.yaml
oc apply -f 60-web-api.yaml
oc apply -f 70-worker.yaml
oc apply -f 80-notification.yaml

Apply 55-sync.yaml first: until the sync pod is ready, anything that needs a lock (assigning a case number, loading an instrument) fails and can be retried.

The web-api / worker / notification pods include an initContainer that polls RabbitMQ before the main container starts, and notification has a second one that waits for web-api, so order between those three doesn't matter — they'll wait for each other naturally.

Check overall status:

oc get pods,svc,route

Confirm the three Routes:

oc get routes -o custom-columns=NAME:.metadata.name,HOST:.spec.host,PATH:.spec.path,SERVICE:.spec.to.name,PORT:.spec.port.targetPort

You should see three rows on the same host with paths <none>, /grpc, and /grpc/pubsub.

Configuring object storage (S3)

Prima stores slide / specimen / cassette images, scanned documents, and label templates in S3-compatible object storage rather than in-cluster volumes. Configuration lives on the web-api and worker Deployments; the access keys come from the Secret.

Env var File Default Notes
ImageStorage__File__RootDirectory 60-, 70- "" Empty string disables the on-disk backend, forcing the S3 backend. Leave empty.
ImageStorage__S3__BucketName 60-, 70- prima-images Bucket for slide/specimen/cassette images + scanned docs.
LabelTemplateStorage__S3__BucketName 60- prima-label-templates Bucket for label templates (web-api only).
StorageCredentialProfiles__0__Region 60-, 70- us-east-1 Region for the buckets.
StorageCredentialProfiles__0__Name / ImageStorage__S3__CredentialProfile / LabelTemplateStorage__S3__CredentialProfile 60-, 70- (LabelTemplateStorage__S3__CredentialProfile: 60- only) AWS Logical name tying the storage backends to credential profile 0. Keep them all matching.
aws-access-key-id / aws-secret-access-key 20-secrets.yaml — Access key pair, surfaced as StorageCredentialProfiles__0__AccessKeyId / __SecretAccessKey.
S3Settings__TimeoutSeconds / S3Settings__MaxRetryAttempts 60-, 70- 30 / 3 Client timeout and retry tuning.
S3-compatible stores (MinIO, Ceph RGW, Wasabi, DigitalOcean Spaces, etc.): add StorageCredentialProfiles__0__Endpoint with the store's URL (for example https://minio.your-org.com) on both web-api and worker. When an endpoint is set, Prima uses path-style addressing (https://endpoint/bucket/key) and ignores StorageCredentialProfiles__0__Region.

LDAP / Active Directory authentication (optional)

By default Prima uses its built-in (local) authentication. To authenticate users against your directory instead, set the LDAP variables on the web-api Deployment. A commented-out block is already present in 60-web-api.yaml — uncomment and fill it in:

Env var Example Notes
LdapConfiguration__AuthenticationType Ldap Prima (local, default), Ldap, Okta (users type their Okta password into Prima), or Oidc (single sign-on; see the next section). Set to Ldap to enable directory auth.
LdapConfiguration__Host dc01.example.com Domain-controller / LDAP server hostname.
LdapConfiguration__Port 636 636 for LDAPS, 389 for plain LDAP.
LdapConfiguration__Protocol LDAPS LDAP or LDAPS.
LdapConfiguration__Domain example.com AD domain.
LdapConfiguration__UserStore ou=Users,dc=example,dc=com Base DN for the user search.
LdapConfiguration__AuthType Negotiate Negotiate, Basic, or Anonymous.
LdapConfiguration__CertificateVerificationMethod Default Default, SkipVerification, or CustomCertificate (with LdapConfiguration__X509CertificatePath).

These values are not secrets (the directory connection uses the integrated AuthType above rather than a stored bind password), so they live as plain value: entries on the Deployment. This is authentication configuration only — it controls how users sign in, not Prima's in-app authorization/roles, which remain database-driven.

Single sign-on with Okta or Microsoft Entra ID (optional)

With single sign-on, everyone signs in through your organization's identity provider in the browser: the website directly, and the desktop apps by opening a browser window. Prima passwords are not used, and password sign-ins are refused. Okta, Microsoft Entra ID (formerly Azure AD), and other OpenID Connect providers are supported.

For Okta single sign-on, use Oidc below, not the Okta value from the LDAP table. That older mode has users type their Okta password into Prima.

1. Register Prima with your identity provider

Only the website is registered; the desktop apps sign in through Prima, so there is nothing to register per workstation. You need these two addresses, on the same hostname as the Routes (Set the public hostname):

Redirect URI (required) https://prima.your-org.com/signin-oidc
Sign-out redirect URI (optional, if the provider asks) https://prima.your-org.com/signout-callback-oidc

Microsoft Entra ID (Entra admin center)

  1. App registrations > New registration. Supported account types: Accounts in this organizational directory only (single tenant). Prima signs in the users of one directory only, and refuses the multi-tenant common / organizations addresses.
  2. Redirect URI: platform Web, the redirect URI above.
  3. From Overview, note the Application (client) ID and the Directory (tenant) ID.
  4. Certificates & secrets > New client secret. Copy its Value (not the Secret ID). Note the expiry date: when the secret expires, nobody can sign in until it is renewed (see Renewing the client secret).
  5. If your tenant does not let users consent to applications themselves, open API permissions and Grant admin consent for openid, profile, and email (Microsoft Graph, delegated).
  6. Optional: to limit who can sign in, open Enterprise applications > the Prima application > Properties, set Assignment required to Yes, and assign users or groups. Everyone also needs a Prima account (see step 3).

Okta (Admin Console)

  1. Applications > Create App Integration > OIDC - OpenID Connect > Web Application.
  2. Grant type: Authorization Code (the default). Sign-in redirect URIs: the redirect URI above. Sign-out redirect URIs: the sign-out address above.
  3. Assignments: assign the people or groups who use Prima. Okta refuses anyone not assigned.
  4. Copy the Client ID and Client secret.
  5. Prima uses Okta's default custom authorization server. If your Okta org does not have custom authorization servers, set OidcConfiguration__OktaAuthorizationServerId to an empty value ("") to use the org authorization server instead.

Other OpenID Connect providers

Register a confidential web application with the authorization code flow and the redirect URI above, and set Provider to Generic with Authority set to the provider's issuer URL (it must be https://).

2. Configure web-api

Put the client secret in 20-secrets.yaml as oidc-client-secret and re-apply it. Then, in 60-web-api.yaml, uncomment the single sign-on block (below the LDAP block) and fill it in. The block covers the common settings; add any other variable from the table below that you need (for example OidcConfiguration__Authority for a Generic provider, or OidcConfiguration__EntraCloud). Only web-api needs these settings.

Env var Example Notes
LdapConfiguration__AuthenticationType Oidc Turns single sign-on on. Set it once — not also in the LDAP block.
OidcConfiguration__Provider Entra Entra, Okta, or Generic.
OidcConfiguration__DisplayName Sign in with Microsoft The sign-in button's text.
OidcConfiguration__EntraTenantId 00000000-… Entra only: the Directory (tenant) ID, or a verified domain.
OidcConfiguration__EntraCloud Public Entra only: Public (default), USGovernment, or China.
OidcConfiguration__OktaDomain yourcompany.okta.com Okta only.
OidcConfiguration__OktaAuthorizationServerId default Okta only: default (default), another custom authorization server's id, or "" for the org authorization server.
OidcConfiguration__Authority https://login.example.com Generic only: the issuer URL.
OidcConfiguration__ClientId From step 1.
OidcConfiguration__ClientSecret From the oidc-client-secret Secret key (already wired in the commented block).
OidcConfiguration__UserMatchClaims preferred_username email Default shown. See step 3.
OidcConfiguration__UserMatchIgnoreDomain false true when Prima user names have no @domain. See step 3.

Then oc apply -f 60-web-api.yaml. At startup web-api logs Sign-in mode: Oidc. If a setting is missing it logs LdapConfiguration:AuthenticationType is Oidc, but sign-in uses Prima because OidcConfiguration is incomplete: followed by what to fix, and keeps password sign-in:

oc logs deploy/web-api | grep -i "sign-in"

3. How users are matched to Prima accounts

Single sign-on does not create Prima accounts: each person still needs a Prima user, as today. The first time someone signs in, Prima compares the provider's claims, in the order of UserMatchClaims, with Prima user names (ignoring case). The first claim that matches decides, and it must match exactly one active user. From then on the two are linked by the provider's permanent id for that person, so a later change of name or e-mail at the provider changes nothing. An administrator can remove the link in the Control Panel.

  • Entra: preferred_username is the user principal name, e.g. jsmith@contoso.com.
  • Okta: preferred_username is the Okta username, usually the e-mail address.
  • If your Prima user names are plain names such as jsmith (common for sites that used LDAP), set UserMatchIgnoreDomain to true: jsmith@contoso.com then also matches jsmith.
The first administrator. On a new installation, the dbinit-admin-username in 20-secrets.yaml must be the administrator's sign-in name at the provider (e.g. jsmith@contoso.com), or the part before the @ with UserMatchIgnoreDomain set to true. Otherwise nobody can sign in to set up everyone else.

4. Check it works

  1. Browse to https://prima.your-org.com/. The sign-in page shows a single button with your DisplayName, and no user name and password boxes.
  2. Sign in as the administrator. You return to Prima signed in.
  3. Start a desktop app. Signing in opens a browser window, which goes through your provider (or straight through if you are already signed in there), then returns to the app.

Troubleshooting single sign-on

The sign-in page still asks for a password
Single sign-on is not on. Check the startup log line above; it names the missing setting.
The provider says the redirect URI does not match
The address registered at the provider must be exactly https://<the Route host>/signin-oidc. Prima builds it from the address the browser used, so users must reach Prima on the Route hostname, over HTTPS.
"Correlation failed" after signing in at the provider
The browser did not return the sign-in cookies, usually because the provider sent the user back to a different hostname than the one they started on. Use the Route hostname everywhere: in the redirect URI, in bookmarks, and in the desktop apps' PrimaGrpcApiUrl.
"Your account is not set up in Prima"
No single active Prima user matched. Web-api's log says why, with the claims and values it tried (oc logs deploy/web-api | grep "Single sign-on refused"). Create the user with that user name, or adjust UserMatchClaims / UserMatchIgnoreDomain.
"Your Prima account is not active"
The matching user is deactivated in Prima.

Renewing the client secret

Entra client secrets expire (at most two years after they are created). Before that date, create a new secret at the provider, put it in oidc-client-secret, re-apply 20-secrets.yaml, and restart web-api:

oc rollout restart deployment/web-api

Turning single sign-on off

Set LdapConfiguration__AuthenticationType back to Prima (or comment the block out) and re-apply 60-web-api.yaml. Password sign-in returns; users who only ever signed in through the provider have no Prima password and need one set.

External RabbitMQ broker

The bundled 40-rabbitmq.yaml runs RabbitMQ in-cluster and is the simplest option. If you'd rather supply your own broker — a dedicated server, a managed service such as AWS Amazon MQ for RabbitMQ, or a container you operate — point Prima at it instead:

  1. Don't apply 40-rabbitmq.yaml, and skip the rabbitmq-data PVC in 30-pvcs.yaml (Prima then needs no PVCs at all).
  2. On the web-api, worker, and notification Deployments, set the broker connection:
    - name: RabbitMq__Host
      value: my-broker.example.com      # or the Amazon MQ endpoint hostname
    - name: RabbitMq__Port
      value: "5672"                     # 5671 for AMQPS/TLS
    - name: RabbitMq__VirtualHost
      value: "/"
    - name: RabbitMq__Username
      value: prima
    # RabbitMq__Password already comes from the prima-secrets "rabbitmq-password" key
  3. The wait-for-rabbitmq initContainers poll rabbitmq:5672 by name. Update them to your broker's host:port (or remove them if the broker is always reachable) so the pods don't block on startup:
    - until nc -z my-broker.example.com 5672; do echo "waiting for rabbitmq"; sleep 2; done

Set RabbitMq__Host consistently on all three client Deployments. The credentials must be valid on your broker; with Amazon MQ, create the user and vhost in the broker console first.

Upgrading

Replace :latest in the five image references with a pinned tag (for example prima-registry.fortelinea.com/fortelinea-prima/prima-webservice:3.2.1) before applying. Keep the fortelinea-prima/ namespace. Recommended upgrade order:

# 1. Run migrations for the new version
oc delete job prima-dbinit
oc apply -f 50-dbinit-job.yaml   # ensure image tag is updated
oc wait --for=condition=complete --timeout=10m job/prima-dbinit

# 2. Roll the services
oc apply -f 55-sync.yaml
oc apply -f 60-web-api.yaml
oc apply -f 70-worker.yaml
oc apply -f 80-notification.yaml

The Deployments use a Recreate rollout strategy (single replica each). Expect ~30s of API downtime per service. Now that images live in S3 rather than on an RWO volume, web-api and notification can be switched to RollingUpdate and scaled horizontally if you want zero-downtime upgrades — worker and sync must stay at a single replica — see the replica-count note in the customization checklist.

Sync (lock coordinator)

Actions that must happen one at a time — assigning the next case number, loading an instrument, changing a container or protocol — are coordinated by the sync pod, so they stay one-at-a-time across every web-api replica and the worker. The other pods reach it at http://sync:8080 (already set in their images as LockService__Provider=Remote / LockService__RemoteAddress) and sign their requests with the shared jwt-secret.

  • Exactly one replica. Locks live in the sync pod's memory; never scale it up. Its Recreate strategy keeps an upgrade from briefly running two.
  • Internal only. It has no Route, and its NetworkPolicy admits only the web-api, worker, and notification pods.
  • Restarts are safe but pause locking. A lock held by a pod that crashes expires after 15 s. After the sync pod itself restarts it grants nothing for 15 s, so a lock from before the restart cannot be handed out twice. Work that needs a lock during that window waits, or fails with "busy, try again" if it cannot wait that long.

Customization checklist

Setting File Default Tune for
S3 buckets / region 60-, 70- prima-images, prima-label-templates, us-east-1 Your object-storage layout. See Configuring object storage.
rabbitmq-data PVC size 30-pvcs.yaml 10 Gi Broker message backlog. Only relevant to the bundled broker.
Memory limits 40-, 55-, 60-, 70-, 80- 2 Gi / 256 Mi / 4 Gi / 2 Gi / 2 Gi Worker memory scales with concurrent slide-image processing.
Shared Route hostname 60-web-api.yaml ×2, 80-notification.yaml ×1 prima.example.com Single DNS hostname for all client traffic. All three Routes MUST match.
TLS certificate All three Routes OpenShift default ingress cert Add tls.key / tls.certificate / tls.caCertificate (same cert on all three Routes) if you bring your own.
LDAP / AD auth 60-web-api.yaml local (Prima) auth Uncomment the LdapConfiguration__* block to authenticate against your directory. See LDAP / Active Directory.
Single sign-on (Okta / Entra) 60-web-api.yaml, 20-secrets.yaml local (Prima) auth Uncomment the single sign-on block and set oidc-client-secret. See Single sign-on.
Replica count 60-, 70-, 80- 1 each Web-api and notification can scale horizontally (images are in S3 — switch their strategy to RollingUpdate to scale out; notification is otherwise stateful per client connection, so make sure clients reconnect cleanly across instances). Worker must stay at 1 — it runs singleton background services and the Hangfire server, and has no Service in front of it, so running more than one instance is not supported. Sync (55-) must stay at 1 — see Sync.

Configuring the Prima WPF apps

The Workstation, Pathologist, LabManager, ControlPanel, and LabelTemplateCreator clients all connect to the gRPC API. They use one URL for both web-api gRPC and notification gRPC streams — the path-based fan-out at the Route layer takes care of the rest:

"PrimaGrpcApiUrl": "https://prima.your-org.com"

REST clients (browser, scripts, third-party integrations) use the same hostname — /api/v1/..., /api/v2/..., /connect/token all route to the REST backend via the catch-all.

The WPF client uses Grpc.Net.Client with a standard HTTPS channel — no Http2UnencryptedSupport switch and no extra TLS configuration is needed, because the wire is HTTP/2 over TLS (the router terminates TLS and forwards h2c to the pod internally). Clients must trust the certificate chain presented by the Routes; if you bring your own cert, distribute the CA to client machines through your normal workstation provisioning.

Troubleshooting

Pods stuck in ImagePullBackOff
Check that the pull secret exists (oc get secret prima-registry-pull, see step 3) and that the credentials work with docker login prima-registry.fortelinea.com. Image references must include the fortelinea-prima/ namespace; without it the pull fails with "authentication required" even with valid credentials.
Pods stuck in Init:0/1
RabbitMQ (or, for notification, web-api) isn't ready yet. Check oc logs <pod> -c wait-for-rabbitmq; if it keeps printing "waiting", look at the RabbitMQ pod for crashes.
The dbinit Job fails
Inspect oc logs job/prima-dbinit. The most common causes are a typo in the connection string in the Secret, or SQL Server not reachable from the cluster. Fix the Secret, then oc delete job prima-dbinit && oc apply -f 50-dbinit-job.yaml.
web-api restarts at startup
Check oc logs deploy/web-api for a TokenServer message: the prima-token-certificates Secret is missing, or token-certificate-password does not match the files (see step 5).
The rabbitmq-data PVC stays Pending
The cluster has no default StorageClass. Set one cluster-wide, or add storageClassName: <name> under spec in 30-pvcs.yaml.
Image upload or retrieval fails (AccessDenied, NoSuchBucket, timeouts)
Check the S3 settings on web-api and worker and the aws-access-key-id / aws-secret-access-key Secret values. Confirm the buckets exist in the configured region and the key pair can read and write them. For S3-compatible stores, check StorageCredentialProfiles__0__Endpoint (see Configuring object storage).
The desktop apps can't connect over gRPC
Errors such as Grpc.Core.RpcException: Status(StatusCode=Internal, ...) or HTTP/2 negotiation failures mean the IngressController doesn't have HTTP/2 enabled. Run the oc annotate command from step 2 and wait for the router pods to roll. Verify with curl --http2 -I https://<host>/grpc/; the response should show HTTP/2, not HTTP/1.1.
Notification streams fail but other gRPC calls work (or the reverse)
The three Routes don't share the same host:. Run oc get routes and confirm all three show the same hostname; the router matches the host first, then the path.
A REST call reaches the gRPC backend, or returns 503 Application is not available
The URL starts with /grpc, so the router sends it to the gRPC Route. Contact Fortelinea.