# KodeMed -- DevOps & Server Administration Guide **Audience**: Hospital IT administrators, DevOps engineers, system operators **Confidential** -- KodeMed GmbH, Switzerland --- ## Table of Contents 1. [Architecture Overview](#1-architecture-overview) 2. [Prerequisites](#2-prerequisites) 3. [Deployment Options](#3-deployment-options) 4. [Docker Compose Installation](#4-docker-compose-installation) 5. [Linux Native Installation](#5-linux-native-installation) 6. [OpenShift / Kubernetes Installation](#6-openshift--kubernetes-installation) 7. [SSO Configuration (Keycloak / OIDC)](#7-sso-configuration) 8. [Reverse Proxy Setup](#8-reverse-proxy-setup) 9. [Updating KodeMed](#9-updating-kodemed) 10. [Health Checks & Monitoring](#10-health-checks--monitoring) 11. [Backup & Recovery](#11-backup--recovery) 12. [Windows Client Deployment](#12-windows-client-deployment) 13. [Data Import (DataServer)](#13-data-import-dataserver) 14. [Classification Data Management](#14-classification-data-management) 15. [License Configuration](#15-license-configuration) 16. [Troubleshooting](#16-troubleshooting) 17. [Quick Reference](#17-quick-reference) > **Complete environment variable reference**: See [Section 16 — Troubleshooting → Required environment variables checklist](#required-environment-variables-checklist) --- ## 1. Architecture Overview KodeMed consists of four server-side services and one Windows desktop client: | Service | Default Port | Role | |---------|-------------|------| | **KodeMed Server** | 8080 | REST API, WebSocket, coding sessions, audit log | | **KodeMed DataServer** | 8081 | ICD-10, CHOP, ATC catalogs, SwissDRG data | | **KodeMed GrouperServer** | 8082 | SwissDRG, TARPSY, ST Reha grouping engine | | **KodeMed CodingUI** | 3000 | React web frontend (served by nginx) | **Database**: PostgreSQL 16 (shared by Server + DataServer) **Authentication**: OAuth2 / OpenID Connect (Keycloak, Azure AD, Okta, or Auth0) ``` HTTPS Hospital Users ---------> Reverse Proxy (Apache / nginx / Caddy) | +-------------------+-------------------+ | | | CodingUI :3000 Server :8080 DataServer :8081 | | | | | WebSocket | PostgreSQL | | | | Browser (React) <-----+ +---- GrouperServer :8082 | CodingClient (Windows) <--- WebSocket --- Server :8080 ``` --- ## 2. Prerequisites ### Hardware Minimums | Environment | CPU | RAM | Disk | |-------------|-----|-----|------| | Test / small hospital | 2 cores | 4 GB | 20 GB | | Production (< 500 users) | 4 cores | 8 GB | 50 GB | | Production (Kubernetes) | 4+ cores | 16 GB | 100 GB | ### Software | Component | Version | Notes | |-----------|---------|-------| | **Linux** | Ubuntu 22.04+ / Debian 12+ / RHEL 9+ | Server OS | | **PostgreSQL** | 16+ | Can run as container or external | | **Docker + Compose** | 24.0+ / 2.20+ | For Docker deployment | | **Java** | 21 (Temurin) | For native Linux deployment | | **Node.js** | 22+ | For native UI build (optional) | | **Kubernetes** | 1.28+ | For K8s/OpenShift deployment | | **Helm** | 3.12+ | For Helm chart deployment | ### Network - HTTPS termination (TLS certificates from Let's Encrypt or corporate CA) - Reverse proxy supporting WebSocket upgrades - Outbound access to your OIDC provider (e.g., `sso.hospital.ch`) --- ## 3. Deployment Options | Model | Best For | Complexity | |-------|----------|------------| | **Docker Compose** | Small-medium hospitals, single server | Low | | **Linux Native** | No-container policies, legacy infrastructure | Medium | | **Kubernetes / OpenShift** | Large hospitals, multi-site, cloud-native | High | All three models deploy the same four services. Choose based on your infrastructure. --- ## 4. Docker Compose Installation ### 4.1 Prepare ```bash sudo mkdir -p /opt/kodemed cd /opt/kodemed ``` You will receive three files from KodeMed GmbH: - `docker-compose.yml` -- Service definitions - `.env` -- Environment configuration (template) - `runtime-config.js` -- UI endpoint configuration ### 4.2 Configure `.env` ```bash # ── Database ── POSTGRES_DB=kodemed POSTGRES_USER=kodemed POSTGRES_PASSWORD= # ── OIDC / SSO ── OIDC_ISSUER_URI=https://sso.hospital.ch/realms/kodemed OIDC_JWK_URI=https://sso.hospital.ch/realms/kodemed/protocol/openid-connect/certs # ── CORS (your public URLs, comma-separated, no trailing slash) ── CORS_ALLOWED_ORIGINS=https://coding.hospital.ch,https://api.hospital.ch WEBSOCKET_ALLOWED_ORIGINS=https://coding.hospital.ch # ── Security keys ── # Generate with: openssl rand -base64 32 KODEMED_ENCRYPTION_KEY= # Generate with: openssl rand -hex 32 KODEMED_ADMIN_API_KEY= # ── Image version (provided by KodeMed GmbH) ── KODEMED_VERSION=2026.3.6.03626 ``` ### 4.3 Configure `runtime-config.js` This file tells the web frontend where to find the backend services: ```javascript window.__KODEMED_CONFIG__ = { apiUrl: "https://api.hospital.ch/api/v1", dataServerUrl: "https://data.hospital.ch", grouperServerUrl: "https://grouper.hospital.ch", wsUrl: "wss://api.hospital.ch/ws/dll", oauth2Url: "https://sso.hospital.ch", oauth2Realm: "kodemed", oauth2ClientId: "kodemed-ui", }; ``` ### 4.4 Login to Image Registry ```bash # Credentials provided by KodeMed GmbH docker login registry.kodemed.com ``` ### 4.5 Start ```bash docker compose pull docker compose up -d ``` ### 4.6 Verify ```bash docker compose ps # Each should return {"status":"UP"} curl -s http://localhost:8080/actuator/health curl -s http://localhost:8081/actuator/health curl -s http://localhost:8082/actuator/health curl -s http://localhost:3000/ # HTTP 200 ``` --- ## 5. Linux Native Installation ### 5.1 Extract & Run Installer ```bash tar -xzf kodemed-linux-.tar.gz cd kodemed-linux- sudo bash install-kodemed.sh ``` The interactive installer guides you through: 1. **Component selection** -- Server, DataServer, GrouperServer, UI 2. **Instance name** -- default: `default` (supports multiple instances) 3. **Port configuration** -- defaults: 8080, 8081, 8082, 3000 4. **PostgreSQL connection** -- host, port, database, user, password 5. **OIDC configuration** -- issuer URL, client ID 6. **Encryption key** -- auto-generated or manual 7. **systemd services** -- created and started automatically ### 5.2 Directory Layout | Path | Content | |------|---------| | `/opt/kodemed/` | JARs, UI distribution | | `/etc/kodemed/` | Configuration files | | `/var/log/kodemed/` | Application logs | | `/var/lib/kodemed/` | Data (imports, uploads) | For named instances, paths include the instance name: `/opt/kodemed-staging/`, etc. ### 5.3 Service Management ```bash # Start / stop / restart sudo systemctl start kodemed-server sudo systemctl stop kodemed-dataserver sudo systemctl restart kodemed-grouper # Status & logs sudo systemctl status kodemed-server journalctl -u kodemed-server -f # Enable auto-start on boot sudo systemctl enable kodemed-server kodemed-dataserver kodemed-grouper ``` ### 5.4 Multi-Instance (Optional) Run multiple KodeMed installations on the same host with port offsets: ```bash sudo bash install-kodemed.sh --instance staging --port-offset 100 # Results in ports: 8180, 8181, 8182, 3100 ``` --- ## 6. OpenShift / Kubernetes Installation ### 6.1 Create Secrets Secrets must be created **before** installing the Helm chart: ```bash # Database — key names become environment variables (Spring Boot expects these exact names) kubectl create secret generic kodemed-db-credentials -n kodemed \ --from-literal=SPRING_DATASOURCE_URL="jdbc:postgresql://db.hospital.ch:5432/kodemed" \ --from-literal=SPRING_DATASOURCE_USERNAME="kodemed" \ --from-literal=SPRING_DATASOURCE_PASSWORD="" # Encryption key (GDPR Art. 32) kubectl create secret generic kodemed-encryption-key -n kodemed \ --from-literal=KODEMED_ENCRYPTION_KEY="" # License file (REQUIRED — generate with kodemed-license-cli) kubectl create secret generic kodemed-license -n kodemed \ --from-file=kodemed.license=./kodemed.license # OIDC / JWT (required for authentication — all three Java services need this) kubectl create secret generic kodemed-oidc -n kodemed \ --from-literal=SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI="https://sso.hospital.ch/realms/kodemed" \ --from-literal=SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI="https://sso.hospital.ch/realms/kodemed/protocol/openid-connect/certs" # Admin API key (DataServer admin import endpoints) kubectl create secret generic kodemed-admin-api-key -n kodemed \ --from-literal=KODEMED_ADMIN_API_KEY="$(openssl rand -hex 32)" # DB SSL certificate (if your PostgreSQL requires it) kubectl create secret generic kodemed-db-ssl-cert -n kodemed \ --from-file=ca.crt=./db-ca.crt ``` ### 6.2 Customize `values.yaml` ```yaml # values-hospital.yaml — keys MUST match charts/kodemed/values.yaml global: imageRegistry: harbor.mieresit.com/kodemed server: replicaCount: 2 image: tag: "2026.3.6.03626" route: host: kodemed-server.apps.hospital.ch env: - name: JAVA_OPTS value: "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -Duser.timezone=Europe/Zurich" - name: KODEMED_AUTH_ENABLED value: "true" - name: WEBSOCKET_ALLOWED_ORIGINS value: "https://kodemed-ui.apps.hospital.ch" - name: KODEMED_PUBLIC_UI_URL value: "https://kodemed-ui.apps.hospital.ch" - name: KODEMED_PUBLIC_SERVER_URL value: "https://kodemed-server.apps.hospital.ch" - name: KODEMED_PUBLIC_DATASERVER_URL value: "https://kodemed-dataserver.apps.hospital.ch" - name: KODEMED_PUBLIC_WEBSOCKET_URL value: "wss://kodemed-server.apps.hospital.ch/ws/dll" - name: SWAGGER_SERVER_URL value: "https://kodemed-server.apps.hospital.ch" - name: SWAGGER_OAUTH2_CLIENT_ID value: "kodemed-server" dataserver: replicaCount: 2 image: tag: "2026.3.6.03626" route: host: kodemed-dataserver.apps.hospital.ch env: - name: JAVA_OPTS value: "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -Duser.timezone=Europe/Zurich" - name: KODEMED_AUTH_ENABLED value: "true" - name: SERVER_PORT value: "8081" # KODEMED_ADMIN_API_KEY is injected automatically from secrets.adminApiKey grouper: replicaCount: 2 image: tag: "2026.3.6.03626" route: host: kodemed-grouper.apps.hospital.ch grouperSpecs: enabled: true type: pvc existingClaim: kodemed-grouper-specs mountPath: /app/specs cataloguePath: /app/catalogues grouperCatalogues: enabled: true type: pvc existingClaim: kodemed-grouper-catalogues env: - name: JAVA_OPTS value: "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -Duser.timezone=Europe/Zurich" - name: GROUPER_AUTH_ENABLED value: "true" - name: SERVER_PORT value: "8082" ui: replicaCount: 2 image: tag: "2026.3.6.03626" route: host: kodemed-ui.apps.hospital.ch # UI runtime config — variable MUST be __KODEMED_CONFIG__ (not __RUNTIME_CONFIG__) uiRuntimeConfig: enabled: true content: | window.__KODEMED_CONFIG__ = { apiUrl: "https://kodemed-server.apps.hospital.ch/api/v1", dataServerUrl: "https://kodemed-dataserver.apps.hospital.ch", grouperServerUrl: "https://kodemed-grouper.apps.hospital.ch", wsUrl: "wss://kodemed-server.apps.hospital.ch/ws/dll", oauth2Url: "https://sso.hospital.ch", oauth2Realm: "kodemed", oauth2ClientId: "kodemed-ui" }; secrets: oidc: enabled: true name: kodemed-oidc adminApiKey: kodemed-admin-api-key config: springProfiles: "prod" serverCorsOrigins: "https://kodemed-ui.apps.hospital.ch" ``` ### 6.3 Install ```bash helm install kodemed charts/kodemed/ \ -f charts/kodemed/values-prod.yaml \ -f values-hospital.yaml \ -n kodemed --create-namespace ``` ### 6.4 Security Hardening (Default) All pods run with: - Non-root user (UID 1001 for Java, 101 for nginx) - Read-only root filesystem - No privilege escalation - All Linux capabilities dropped - Pod Disruption Budgets (minAvailable: 1) - Rolling updates (maxUnavailable: 0) ### 6.5 Resource Limits | Pod | CPU Request | CPU Limit | Memory Request | Memory Limit | |-----|-------------|-----------|----------------|--------------| | server | 250m | 1000m | 512Mi | 1Gi | | dataserver | 200m | 500m | 256Mi | 1Gi | | grouper | 200m | 500m | 512Mi | 1Gi | | ui (nginx) | 50m | 200m | 32Mi | 64Mi | The JVM respects container memory limits automatically via `-XX:MaxRAMPercentage=75.0`. --- ## 7. SSO Configuration KodeMed requires an OpenID Connect provider. Keycloak is recommended but Azure AD, Okta, and Auth0 are also supported. ### 7.1 Keycloak Setup If using KodeMed's pre-configured Keycloak image: ```bash cd keycloak/ cp .env.example .env # Edit .env: set domain, passwords docker compose up -d ``` The stack includes: - **Caddy** -- automatic HTTPS (Let's Encrypt) - **PostgreSQL 16** -- Keycloak database - **Keycloak 26** -- Identity provider - **pg-backup** -- daily backups at 02:00 UTC (30-day retention) ### 7.2 Realm Configuration | Item | Value | |------|-------| | Realm name | `kodemed` | | Client: UI | `kodemed-ui` (public, PKCE flow) | | Client: Server | `kodemed-server` (confidential) | | Roles | `admin`, `coder`, `approver`, `viewer` | ### 7.3 Brute Force Protection Enabled by default: max 5 failed logins, 60-second lockout increment. ### 7.4 Using Azure AD / Okta / Auth0 Set these values in your `.env` or `values.yaml`: ```bash OIDC_ISSUER_URI=https://login.microsoftonline.com//v2.0 # Azure AD OIDC_JWK_URI=https://login.microsoftonline.com//discovery/v2.0/keys ``` Ensure the OIDC provider issues tokens with: - `sub` claim (user ID) - `realm_access.roles` or `groups` claim (for role mapping) - PKCE support for the public UI client --- ## 8. Reverse Proxy Setup A reverse proxy is required for HTTPS termination and WebSocket support. ### 8.1 Apache ```apache # KodeMed Server (API + WebSocket) ServerName api.hospital.ch SSLEngine on SSLCertificateFile /etc/ssl/certs/hospital.pem SSLCertificateKeyFile /etc/ssl/private/hospital.key # WebSocket — must come BEFORE general ProxyPass ProxyPass /ws/ ws://localhost:8080/ws/ ProxyPassReverse /ws/ ws://localhost:8080/ws/ ProxyTimeout 86400 ProxyPass / http://localhost:8080/ ProxyPassReverse / http://localhost:8080/ ProxyPreserveHost On # KodeMed DataServer ServerName data.hospital.ch SSLEngine on # ... SSL config ... ProxyPass / http://localhost:8081/ ProxyPassReverse / http://localhost:8081/ ProxyPreserveHost On # KodeMed GrouperServer ServerName grouper.hospital.ch SSLEngine on # ... SSL config ... ProxyPass / http://localhost:8082/ ProxyPassReverse / http://localhost:8082/ ProxyPreserveHost On # KodeMed CodingUI ServerName coding.hospital.ch SSLEngine on # ... SSL config ... ProxyPass / http://localhost:3000/ ProxyPassReverse / http://localhost:3000/ ProxyPreserveHost On ``` Required Apache modules: ```bash sudo a2enmod proxy proxy_http proxy_wstunnel ssl headers sudo systemctl restart apache2 ``` ### 8.2 nginx ```nginx # KodeMed Server (API + WebSocket) server { listen 443 ssl; server_name api.hospital.ch; ssl_certificate /etc/ssl/certs/hospital.pem; ssl_certificate_key /etc/ssl/private/hospital.key; location /ws/ { proxy_pass http://localhost:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_read_timeout 86400s; proxy_send_timeout 86400s; } location / { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` --- ## 9. Updating KodeMed ### 9.1 Docker Compose ```bash cd /opt/kodemed # 1. Pull new images (version provided by KodeMed GmbH) # Either update KODEMED_VERSION in .env or pull :latest docker compose pull # 2. Restart services (health checks ensure readiness) docker compose up -d # 3. Verify docker compose ps curl -s http://localhost:8080/actuator/health ``` ### 9.2 Linux Native ```bash # 1. Stop services sudo systemctl stop kodemed-server kodemed-dataserver kodemed-grouper # 2. Backup current installation sudo cp -r /opt/kodemed /opt/kodemed.backup.$(date +%Y%m%d) # 3. Extract new version and run installer tar -xzf kodemed-linux-.tar.gz cd kodemed-linux- sudo bash install-kodemed.sh --upgrade # 4. Start services sudo systemctl start kodemed-server kodemed-dataserver kodemed-grouper # 5. Verify curl -s http://localhost:8080/actuator/health ``` ### 9.3 Kubernetes / OpenShift ```bash # 1. Update image tags in your values file (each service has its own tag) # server.image.tag / dataserver.image.tag / grouper.image.tag / ui.image.tag # 2. Apply helm upgrade kodemed charts/kodemed/ \ -f charts/kodemed/values-prod.yaml \ -f values-hospital.yaml \ -n kodemed # 3. Monitor rollout kubectl rollout status deployment/kodemed-server -n kodemed kubectl rollout status deployment/kodemed-dataserver -n kodemed kubectl rollout status deployment/kodemed-grouper -n kodemed kubectl rollout status deployment/kodemed-ui -n kodemed ``` ### 9.4 Rollback **Docker Compose:** ```bash # Set previous version in .env KODEMED_VERSION=2026.3.5.12345 docker compose up -d ``` **Kubernetes:** ```bash helm rollback kodemed 1 -n kodemed ``` ### 9.5 Version Compatibility All KodeMed components share the same version number. Matching versions are always compatible. Do not mix versions between services. --- ## 10. Health Checks & Monitoring ### 10.1 Endpoints | Service | Health URL | Expected | |---------|-----------|----------| | Server | `/actuator/health` | `{"status":"UP"}` | | DataServer | `/actuator/health` | `{"status":"UP"}` | | GrouperServer | `/actuator/health` | `{"status":"UP"}` | | CodingUI | `/` | HTTP 200 | For Kubernetes, detailed probes are available: - `/actuator/health/liveness` -- Is the service alive? - `/actuator/health/readiness` -- Is the service ready to accept requests? ### 10.2 Simple Monitoring Script ```bash #!/bin/bash SERVICES=("Server:8080" "DataServer:8081" "GrouperServer:8082" "UI:3000") for entry in "${SERVICES[@]}"; do name="${entry%%:*}" port="${entry##*:}" if [ "$port" = "3000" ]; then url="http://localhost:$port/" else url="http://localhost:$port/actuator/health" fi code=$(curl -s -o /dev/null -w "%{http_code}" "$url" --max-time 5) if [ "$code" = "200" ]; then echo "[OK] $name (:$port)" else echo "[FAIL] $name (:$port) -- HTTP $code" fi done ``` ### 10.3 Log Locations | Deployment | How to access | |------------|--------------| | Docker Compose | `docker compose logs kodemed-server` | | Linux Native | `journalctl -u kodemed-server -f` or `/var/log/kodemed/` | | Kubernetes | `kubectl logs deployment/kodemed-server -n kodemed` | --- ## 11. Backup & Recovery ### 11.1 Database (PostgreSQL) The database is the only stateful component. Back it up regularly. ```bash # Docker Compose docker exec kodemed-postgres pg_dump -U kodemed kodemed \ | gzip > kodemed-db-$(date +%Y%m%d).sql.gz # Linux Native (external PostgreSQL) pg_dump -h db-host -U kodemed kodemed \ | gzip > kodemed-db-$(date +%Y%m%d).sql.gz ``` **Restore:** ```bash gunzip < kodemed-db-20260306.sql.gz \ | docker exec -i kodemed-postgres psql -U kodemed kodemed ``` ### 11.2 Configuration ```bash # Back up all config files tar -czf kodemed-config-$(date +%Y%m%d).tar.gz \ /opt/kodemed/.env \ /opt/kodemed/docker-compose.yml \ /opt/kodemed/runtime-config.js ``` ### 11.3 Keycloak If using KodeMed's Keycloak stack, automated backups run daily at 02:00 UTC with 30-day retention. For manual backup: ```bash docker exec keycloak-postgres pg_dump -U keycloak keycloak \ | gzip > keycloak-$(date +%Y%m%d).sql.gz ``` --- ## 12. Windows Client Deployment The KodeMed CodingClient runs on Windows workstations and communicates with the server via WebSocket. ### 12.1 Standard Installation (per-user, no admin required) Download `KodeMed.msi` from the KodeMed portal and double-click. By default, the installer runs **per-user** — no administrator rights are required. Files are installed to `%LOCALAPPDATA%\KodeMed`. The MSI supports **dual-mode** installation: | Mode | Install dir | COM | Env vars | Admin | Use case | |------|-------------|-----|----------|-------|----------| | **Per-user** (default) | `%LOCALAPPDATA%\KodeMed` | HKCU | User-level | No | Standard desktop | | **Per-machine** (`ALLUSERS=1`) | `%ProgramFiles%\KodeMed` | HKLM | System-wide | Yes | Citrix / Terminal Server | The installer UI includes a checkbox to select per-machine mode. For silent installs, pass `ALLUSERS=1`. ### 12.2 Enterprise Deployment (MSI silent install) For GPO, SCCM, Intune, or Citrix: ```powershell # Silent install (per-user, default) msiexec /i KodeMed.msi /quiet /norestart SERVERURL="https://api.hospital.ch" # Full options (per-user) msiexec /i KodeMed.msi /quiet /norestart ` SERVERURL="https://api.hospital.ch" ` LANGUAGE="de" ` AUTOSTART=1 # Per-machine install (Citrix / Terminal Server — requires admin) msiexec /i KodeMed.msi /quiet /norestart ` SERVERURL="https://api.hospital.ch" ` ALLUSERS=1 MSIINSTALLPERUSER="" ` AUTOSTART=0 # Custom install directory (e.g. network share) msiexec /i KodeMed.msi /quiet /norestart ` SERVERURL="https://api.hospital.ch" ` INSTALLDIR="D:\Apps\KodeMed" ``` ### 12.3 MSI Properties | Property | Required | Default | Description | |----------|----------|---------|-------------| | `SERVERURL` | Recommended | — | KodeMed server URL (e.g. `https://api.hospital.ch`). If omitted in silent install, the app prompts on first launch. | | `LANGUAGE` | No | auto-detect | UI language: `de`, `fr`, `it`, or `en` | | `AUTOSTART` | No | `1` | Start at Windows login: `1` = yes, `0` = no | | `INSTALLDIR` | No | per-user: `%LOCALAPPDATA%\KodeMed`, per-machine: `%ProgramFiles%\KodeMed` | Installation directory | | `ALLUSERS` | No | *(empty)* | Set to `1` for per-machine install (Citrix/Terminal Server). Requires admin. | | `MSIINSTALLPERUSER` | No | `1` | Set to `""` when using `ALLUSERS=1` for per-machine mode | | `LAUNCHAPP` | No | `1` | Launch app after install (UI mode only, per-user only) | ### 12.4 Configuration Resolution The CodingClient searches for `kodemed-client-config.json` in the following order: | Priority | Path | Use case | |----------|------|----------| | 1 | `%KODEMED_HOME%\` | Explicit override via environment variable | | 2 | `%LOCALAPPDATA%\KodeMed\` | Standard per-user MSI installation | | 3 | `%ProgramFiles%\KodeMed\` | Per-machine MSI installation (`ALLUSERS=1`) | | 4 | Next to EXE | Citrix shared drive / portable deployment | The first file found is used. Changes are saved back to the same location. ### 12.5 Citrix / VDI Deployment #### Per-machine install (recommended for Citrix/Terminal Server) Use `ALLUSERS=1` for a system-wide installation. This registers COM in HKLM (visible to all users/services), sets system-wide environment variables, and installs to `%ProgramFiles%\KodeMed`: ```powershell msiexec /i KodeMed.msi /quiet /norestart ` SERVERURL="https://api.hospital.ch" ` ALLUSERS=1 MSIINSTALLPERUSER="" ` AUTOSTART=0 ``` > **⚠️ Warning**: The installer's `CloseRunningInstances` custom action kills **ALL** running KodeMed instances system-wide, regardless of which user started them. On **Citrix / Terminal Server**, schedule MSI upgrades during **maintenance windows** when no users are actively coding. #### Static Desktops (persistent VDI) Standard per-user MSI installation works — `%LOCALAPPDATA%` persists between sessions. #### Dynamic Desktops (non-persistent VDI) On non-persistent desktops, `%LOCALAPPDATA%` is reset on logoff. Three approaches: **Option A: Citrix Profile Management (recommended)** Configure Citrix Profile Management to include `AppData\Local\KodeMed` in the user profile. The MSI installs normally and the profile manager preserves it. **Option B: Shared drive deployment (no MSI)** Place the binaries and config on a network share accessible to all users: ``` \\fileserver\kodemed\ KodeMed.CodingClient.exe KodeMed.dll KodeMed.comhost.dll kodemed-client-config.json ← config next to EXE ``` Publish `KodeMed.CodingClient.exe` as a Citrix Published Application. No installation required — the CodingClient reads config from its own directory. **Option C: Logon script** Run the MSI silently at each logon: ```powershell # Logon script — only installs if not already present if (-not (Test-Path "$env:LOCALAPPDATA\KodeMed\KodeMed.CodingClient.exe")) { msiexec /i "\\fileserver\kodemed\KodeMed.msi" /quiet /norestart ` SERVERURL="https://api.hospital.ch" AUTOSTART=0 } ``` > **Note**: Disable autostart (`AUTOSTART=0`) on Citrix — the CodingClient is typically started on demand or published as a Citrix application. ### 12.6 Upgrading from Previous Versions The current MSI uses `MajorUpgrade` with `AllowSameVersionUpgrades=yes`. Within the same scope (per-user → per-user, or per-machine → per-machine), upgrades are automatic — the old version is removed before the new one installs. The installation scope is persisted to the registry (`HKCU\Software\KodeMed\AllUsers` and `HKLM\SOFTWARE\KodeMed\AllUsers`) so upgrades automatically preserve the original scope. **Cross-scope upgrades** (e.g. old per-machine → new per-user): Windows Installer does **not** auto-detect different-scope installs as upgrades. Manual steps required: 1. Uninstall the old MSI: `msiexec /x {old-product-code} /quiet` 2. Install the new MSI with the desired scope For enterprise rollout, include the uninstall step in your GPO/SCCM deployment sequence. ### 12.7 COM Registration The MSI registers the COM object (`KodeMed.Coding`, CLSID `{F6E73053-8D49-480F-AC06-4E0A4882373F}`) in the appropriate registry hive based on installation mode: | Mode | Registry | Visibility | Admin | |------|----------|-----------|-------| | **Per-user** (default) | HKCU\Software\Classes | Current user only | No | | **Per-machine** (`ALLUSERS=1`) | HKLM\SOFTWARE\Classes | All users + services | Yes | **Impact on KIS integration:** | Scenario | Per-user | Per-machine | |----------|----------|-------------| | KIS runs as logged-in user | Yes | Yes | | KIS runs as Windows service (SYSTEM) | **No** | Yes | | KIS runs as different user | **No** | Yes | | Citrix Published App (same user) | Yes | Yes | > **Recommendation**: If the hospital's KIS runs as a service account or under a different user context, use **per-machine installation** (`ALLUSERS=1`) instead of manual `regsvr32`. The MSI handles HKLM COM registration, environment variables, and autostart automatically. ### 12.8 Requirements - Windows 10 or 11 - WebView2 Runtime (pre-installed on Windows 11; auto-downloaded on Windows 10 if missing) - ~80 MB disk space - Network access to KodeMed Server (port 8080 / HTTPS) - Per-machine mode requires administrator rights --- ## 13. Data Import (DataServer) The DataServer requires classification data to be imported before it can serve thesaurus searches. Import is done via a hot-folder mechanism: place files in the inbox, then trigger a scan. ### 13.1 Required Source Files Download annually from [swissdrg.org](https://www.swissdrg.org) and [bfarm.de](https://www.bfarm.de): | # | File Pattern | Source | Description | |---|-------------|--------|-------------| | 1 | `icd10gm{YYYY}syst-claml.zip` | BfArM | ICD-10-GM German (ClaML XML) | | 2 | `icd10gm{YYYY}syst-meta.zip` | BfArM | ICD-10-GM German metadata | | 3 | `icd10gm{YYYY}alpha-txt.zip` | BfArM | ICD-10-GM German alphabetical index | | 4 | `dz-f-*-cim10-gm-*-ClaML-SI.zip` | BFS | CIM-10 French (ClaML) | | 5 | `dz-i-*-cim10-gm-*-ClaML-SI.zip` | BFS | CIM-10 Italian (ClaML) | | 6 | `dz-f-*-cim10-gm-*-CSV-SI.zip` | BFS | CIM-10 French CSV systematik (pre-composed labels) | | 7 | `dz-i-*-cim10-gm-*-CSV-SI.zip` | BFS | CIM-10 Italian CSV systematik (pre-composed labels) | | 8 | `dz-f-*-cim10-gm-alp-*.zip` | BFS | CIM-10 French alphabetical index | | 9 | `dz-i-*-cim10-gm-alp-*.zip` | BFS | CIM-10 Italian alphabetical index | | 10 | `dz-d-*chop*-sys-*.zip` | BFS | CHOP German systematic | | 11 | `dz-f-*chop*-sys-*.zip` | BFS | CHOP French systematic | | 12 | `dz-i-*chop*-sys-*.zip` | BFS | CHOP Italian systematic | | 13 | `dz-d-*chop*-alp-*.zip` | BFS | CHOP German alphabetical | | 14 | `dz-f-*chop*-alp-*.zip` | BFS | CHOP French alphabetical | | 15 | `dz-i-*chop*-alp-*.zip` | BFS | CHOP Italian alphabetical | | 16 | `*Liste*Medikamente*.xlsx` | SwissDRG | High-cost medications DE | | 17 | `*Liste*Medikamente*_f.xlsx` | SwissDRG | High-cost medications FR | | 18 | `*Liste*Medikamente*_i.xlsx` | SwissDRG | High-cost medications IT | | 19 | `*Fallpauschalenkatalog*.xlsx` | SwissDRG | DRG catalog DE | | 20 | `*Fallpauschalenkatalog*_f.xlsx` | SwissDRG | DRG catalog FR | | 21 | `*Fallpauschalenkatalog*_i.xlsx` | SwissDRG | DRG catalog IT | | 22 | `*Technisches_Begleitblatt*.xlsx` | SwissDRG | Technical supplement | | 23 | `*Begleitdokumente*SwissDRG*.zip` | SwissDRG | CCL, POA, plausibility rules (DE/FR/IT) | Raw source files are also stored in the repo at `KodeMed.DataServer/import/raw/` (gitignored for licensed files). ### 13.2 Import Phases ``` Phase 1: Base classifications Phase 2: Build thesaurus Phase 3: Enrichment ───────────────────────────── ────────────────────────── ───────────────────── ICD-10 + CHOP + Medications ──► POST /api/thesaurus/ Begleitdokumente (hot-folder scan) build/{version} (hot-folder scan) → CCL, POA, plausibility → cache auto-reloads ``` ### 13.3 Docker Compose Import ```bash # 1. Create inbox directory inside the dataserver container docker exec kodemed-dataserver mkdir -p /app/import/inbox/2026 # 2. Copy source files into the container # Option A: From local machine docker cp /path/to/files/. kodemed-dataserver:/app/import/inbox/2026/ # Option B: From a tar archive docker cp import-2026.tar.gz kodemed-dataserver:/tmp/ docker exec kodemed-dataserver tar xzf /tmp/import-2026.tar.gz -C /app/import/inbox/2026/ # 3. Verify files are in place docker exec kodemed-dataserver ls -la /app/import/inbox/2026/ # 4. Trigger import scan (requires admin API key) docker exec kodemed-dataserver wget -qO- --post-data='' \ --header='X-Admin-Key: YOUR_ADMIN_API_KEY' \ http://localhost:8081/api/admin/import/scan # 5. Monitor progress docker logs -f kodemed-dataserver # 6. Check import status docker exec kodemed-dataserver wget -qO- \ --header='X-Admin-Key: YOUR_ADMIN_API_KEY' \ http://localhost:8081/api/admin/import/scan/status ``` ### 13.4 Linux Native Import ```bash # 1. Copy files to inbox mkdir -p /opt/kodemed/dataserver/import/inbox/2026 cp /path/to/files/*.zip /opt/kodemed/dataserver/import/inbox/2026/ cp /path/to/files/*.xlsx /opt/kodemed/dataserver/import/inbox/2026/ cp /path/to/files/*.csv /opt/kodemed/dataserver/import/inbox/2026/ chown -R kodemed:kodemed /opt/kodemed/dataserver/import/inbox/2026/ # 2. Trigger scan curl -X POST http://localhost:8081/api/admin/import/scan \ -H "X-Admin-Key: YOUR_ADMIN_API_KEY" # 3. Monitor journalctl -u kodemed-dataserver -f # 4. Check status curl -s http://localhost:8081/api/admin/import/scan/status | jq . ``` ### 13.5 Post-Import Steps After all files are imported, run the performance SQL and verify: ```bash # 1. Performance indexes (CRITICAL for search speed) # Without these, text searches do full table scans on 150,000+ entries psql -U kodemed -d kodemed -f thesaurus_performance.sql # For Docker: docker exec -i kodemed-postgres psql -U kodemed -d kodemed < \ KodeMed.DataServer/src/main/resources/db/thesaurus_performance.sql # 2. Restart DataServer to reload enrichment cache # Docker: docker restart kodemed-dataserver # Native: sudo systemctl restart kodemed-dataserver # 3. Verify thesaurus statistics curl -s http://localhost:8081/api/thesaurus/statistics?version=2026 # Expected: icd10Codes > 15000, chopCodes > 45000, atcCodes > 100 # 4. Verify enrichment cache curl -s http://localhost:8081/api/thesaurus/cache/stats # 5. Run smoke test bash scripts/test/test-import-flow.sh http://localhost:8081 2026 ``` ### 13.6 Reimport from Another Server To copy import data from an existing server (e.g., devel → demo): ```bash # On source server: prepare files (strip timestamp prefixes from success/) # Use scripts/ci/prep-demo-import.sh scp scripts/ci/prep-demo-import.sh SOURCE_HOST:/tmp/ ssh SOURCE_HOST "sudo bash /tmp/prep-demo-import.sh" # Download and upload to target scp SOURCE_HOST:/tmp/kodemed-import-2026.tar.gz /tmp/ scp /tmp/kodemed-import-2026.tar.gz TARGET_HOST:/tmp/ # On target (Docker): docker exec kodemed-dataserver mkdir -p /app/import/inbox/2026 docker cp /tmp/kodemed-import-2026.tar.gz kodemed-dataserver:/tmp/ docker exec kodemed-dataserver tar xzf /tmp/kodemed-import-2026.tar.gz \ -C /app/import/inbox/2026/ # Don't forget CSV systematik files if not in success/: # dz-f-*-cim10-gm-*-CSV-SI.zip and dz-i-*-cim10-gm-*-CSV-SI.zip # Available in repo: KodeMed.DataServer/import/raw/ # Trigger scan + post-import steps (see 13.5) ``` ### 13.7 Troubleshooting Import | Problem | Cause | Solution | |---------|-------|----------| | 401 on `/api/admin/import/scan` | Auth enabled, no API key | Add `-H "X-Admin-Key: KEY"` (from `.env` `KODEMED_ADMIN_API_KEY`) | | Files not processed | Files in `inbox/` root, not in year subfolder | Move to `inbox/2026/` | | "Unknown file type" | Filename doesn't match expected pattern | Check patterns in IMPORT_GUIDE.md | | FR/IT texts incomplete | Missing CSV-SI files | Add `dz-f/i-*-CSV-SI.zip` to inbox | | Searches slow (>1s) | Performance SQL not executed | Run `thesaurus_performance.sql` | | Enrichment missing (CCL=null) | Begleitdokumente not imported or cache not loaded | Reimport Begleitdokumente, then `POST /api/thesaurus/cache/reload/2026` | --- ## 14. Classification Data Management ### 14.1 Data Sources KodeMed imports official Swiss medical classification data from the following sources. Hospitals and partners can download raw files directly from these official sites: | Source | Data | URL | |--------|------|-----| | **BfArM** (Germany) | ICD-10-GM (ClaML, Metadata, Alphabetical) | https://www.bfarm.de/EN/Code-systems/Classifications/ICD/ICD-10-GM/ | | **BFS** (Switzerland) | CHOP (DE/FR/IT), CIM-10 (FR/IT ClaML + CSV-SI) | https://www.bfs.admin.ch/bfs/de/home/statistiken/gesundheit/nomenklaturen.html | | **SwissDRG AG** | SwissDRG catalogs, TARPSY, ST Reha, Medications, Begleitdokumente | https://www.swissdrg.org/de/akutsomatik/swissdrg | ### 14.2 kodemed-data Repository Classification data files are stored in a **separate repository** (`kodemed-data`) to keep the main `kodemed` repo lightweight for CI builds. ``` kodemed-data/ ├── data/ │ ├── 2026/ # 36 files for tariff year 2026 │ ├── 2025/ # (future years) │ └── ... ├── package.sh # Create distributable tar.gz ├── publish.sh # Publish to Harbor OCI registry └── README.md # File catalog with patterns and sources ``` **Harbor OCI artifact**: Published as `harbor.mieresit.com/kodemed/kodemed-import-data:YYYY` ### 14.3 Packaging and Publishing ```bash # In kodemed-data repo: # 1. Package for a specific year bash package.sh 2026 # Output: dist/kodemed-import-2026.tar.gz + .sha256 # 2. Publish to Harbor bash publish.sh 2026 # Published: harbor.mieresit.com/kodemed/kodemed-import-data:2026 ``` ### 14.4 Deploying Import Data to a Server Use the deploy script from the main `kodemed` repo: ```bash # Full automated deploy: clean → upload → scan → monitor → post-import bash scripts/ci/deploy-import.sh --full SERVER # Step by step: bash scripts/ci/deploy-import.sh SERVER # upload only bash scripts/ci/deploy-import.sh --scan SERVER # upload + trigger scan bash scripts/ci/deploy-import.sh --clean --scan SERVER # wipe + upload + scan bash scripts/ci/deploy-import.sh --full --year 2026 SERVER # explicit year # Options: # --clean Wipe existing inbox/success/error for the year # --scan Trigger import scan after upload # --monitor Watch progress until completion # --full All of the above + post-import (performance SQL, restart) # --year YYYY Override year (default: auto-detect from package name) # --key KEY Admin API key (default: reads from server .env) # --dry-run Show commands without executing ``` ### 14.5 Adding a New Tariff Year Each year (typically in December/January), new classification data is released: 1. **Download** raw files from official sources (see 14.1) 2. **Place** files in `kodemed-data/data/YYYY/` 3. **Package**: `bash package.sh YYYY` 4. **Publish**: `bash publish.sh YYYY` 5. **Deploy**: `bash scripts/ci/deploy-import.sh --full SERVER` 6. **Verify**: Run smoke test and check all languages (DE/FR/IT) **Important**: The ICD-10-GM version may differ from the tariff year (e.g., ICD-10-GM 2024 is used for tariff year 2026). The DataServer handles this mapping automatically. ### 14.6 GrouperServer Specifications and Catalogues The GrouperServer requires **specification files** (`.sgs`) and **catalogue files** (`.csv`) to perform DRG grouping. These are NOT classification import data — they are the grouper engine's configuration. **Files required** (in `installer/server/docker/`): ``` specs/ ├── swissdrg-15.0.sgs # SwissDRG specification ├── tarpsy-6.3.sgs # TARPSY specification └── streha-3.4.sgs # ST Reha specification catalogues/ ├── swissdrg-15.0-catalogue.csv ├── swissdrg-15.0-birthhouse-catalogue.csv ├── tarpsy-6.3-catalogue.csv └── streha-3.4-catalogue.csv ``` **Deployment**: `deploy-docker-compose.sh` automatically validates these directories exist and uploads them: ```bash # The script will fail if specs/ or catalogues/ are missing or empty bash scripts/ci/deploy-docker-compose.sh --env=ch ``` **Docker Compose**: Specs are bind-mounted read-only into the grouper container: ```yaml kodemed-grouper: volumes: - ./specs:/app/specs:ro - ./catalogues:/app/catalogues:ro ``` **Kubernetes/Helm**: Use a PVC or ConfigMap (see `charts/kodemed/values.yaml` → `grouper.grouperSpecs`). **Version defaults** (configurable via env vars): | Variable | Default | Description | |----------|---------|-------------| | `GROUPER_DEFAULT_SWISSDRG` | `15.0` | Active SwissDRG version | | `GROUPER_DEFAULT_TARPSY` | `6.3` | Active TARPSY version | | `GROUPER_DEFAULT_STREHA` | `3.4` | Active ST Reha version | ### 14.7 Licensing and Data Distribution - Classification data files are **publicly available** from official sources (BfArM, BFS, SwissDRG) - KodeMed provides **pre-packaged imports** via Harbor for convenience - Hospitals with `kodemed-admin` role can download raw files themselves and run their own imports - The KodeMed license determines which **components** (Server, DataServer, Grouper, UI, Client, DLL) are activated — not the data itself --- ## 15. License Configuration KodeMed **requires a valid license** to operate. Each server component validates its RSA-signed license on startup. Without a valid license, all API endpoints return HTTP 402 (Payment Required). ### 15.1 License Types | Type | Use Case | Typical Duration | |------|----------|-----------------| | `DEMO` | Sales demos, evaluations | 30-365 days | | `TRIAL` | Customer evaluation | 30-90 days | | `PRODUCTION` | Hospital production use | 1-3 years | ### 15.2 Generating a License ```bash # Generate RSA-4096 keypair (once, store private key securely) bash scripts/license/generate-keys.sh # Generate a demo license (all components, 1 year) java -jar kodemed-license-*-cli.jar generate \ --private-key kodemed-private.pem \ --type DEMO \ --org "Hospital Name" \ --days 365 \ --components "kodemed-server,kodemed-data-server,kodemed-grouper-server" \ --max-users 10 \ --output kodemed.license # Verify a license java -jar kodemed-license-*-cli.jar verify \ --public-key kodemed-public.pem \ --license kodemed.license \ --component kodemed-server ``` > **Note:** To license all components, use `--components '*'` (single quotes to prevent shell glob expansion). ### 15.3 License File Search The license file (`kodemed.license`) is searched in order: 1. Path specified by `KODEMED_LICENSE_FILE` environment variable 2. `./kodemed.license` (working directory) 3. `/etc/kodemed/kodemed.license` (Linux) 4. `%APPDATA%/KodeMed/kodemed.license` (Windows) ### 15.4 Component Names Each service validates that the license covers its component: | Service | Component Name | |---------|---------------| | KodeMed Server | `kodemed-server` | | KodeMed DataServer | `kodemed-data-server` | | KodeMed GrouperServer | `kodemed-grouper-server` | The license file must list these in its `components` array, or use `"*"` for all. ### 15.5 Docker Compose Mount the license file as a read-only volume: ```yaml kodemed-server: environment: - KODEMED_LICENSE_FILE=/app/kodemed.license volumes: - ./kodemed.license:/app/kodemed.license:ro ``` Place the `kodemed.license` file in the same directory as `docker-compose.yml`. ### 15.6 OpenShift / Kubernetes (Helm) Set in `values.yaml`: ```yaml config: licenseFile: "/app/kodemed.license" ``` Mount the license file as a Kubernetes Secret (recommended): ```bash kubectl create secret generic kodemed-license \ --from-file=kodemed.license=./kodemed.license ``` Then add a volume mount in the deployment templates. ### 15.7 Public Key Distribution The public key (`kodemed-public.pem`) must be on the classpath of each server module: ``` KodeMed.Server/src/main/resources/kodemed-public.pem KodeMed.DataServer/src/main/resources/kodemed-public.pem KodeMed.GrouperServer/src/main/resources/kodemed-public.pem ``` It is embedded in the JAR/Docker image at build time. The private key is **never** distributed. ### 15.8 License Status API All three servers expose a license status endpoint: ```bash curl https://your-server/api/v1/license/status ``` Returns 200 with license details when valid, or 402 with `NO_LICENSE`/`EXPIRED`/`INVALID` status. ### 15.9 License Renewal When a license approaches expiration: 1. Generate a new license with the CLI tool (same private key): ```bash java -jar kodemed-license-*-cli.jar generate \ --private-key kodemed-private.pem \ --type PRODUCTION \ --org "Hospital Name" \ --days 365 \ --components "kodemed-server,kodemed-data-server,kodemed-grouper-server" \ --output kodemed.license ``` 2. Replace the license file in the deployment: - **Docker Compose:** Replace `kodemed.license` on the host and restart: `docker compose restart` - **Kubernetes:** Update the Secret: `kubectl create secret generic kodemed-license --from-file=kodemed.license=./kodemed.license --dry-run=client -o yaml | kubectl apply -f -` 3. Verify via the status API: ```bash curl https://your-server/api/v1/license/status | jq . ``` No application restart is required for Kubernetes (Secret updates propagate automatically). Docker Compose requires a container restart. ### 15.10 License Generator (Internal Portal) The **Downloads Portal** (portal.kodemed.com) includes a license generator for internal use. **Access requirements**: User must have **both** `kodemed-admin` AND `kodemed-sales` Keycloak roles. **How it works**: 1. Sign in at https://portal.kodemed.com 2. Navigate to "License Generator" section (visible only with both roles) 3. Fill in customer details (organization, type, validity, components, max users) 4. Click "Generate Command" — the portal generates the CLI command with all parameters 5. Copy the command and run it on a machine with the private key and `kodemed-license-cli.jar` **Note**: The portal does NOT generate the license file directly (the private key must never leave secure storage). It generates the CLI command for convenience. --- ## 16. Troubleshooting ### Service does not start ```bash # Check logs for the specific error docker compose logs kodemed-server --tail=50 # Common causes: # - PostgreSQL not ready -> wait or check: docker compose logs kodemed-postgres # - OIDC_ISSUER_URI unreachable -> verify DNS and firewall # - Port already in use -> ss -tlnp | grep 8080 ``` ### Database connection failed ```bash # Test connectivity docker exec kodemed-postgres pg_isready -U kodemed # or for native install: pg_isready -h db-host -U kodemed ``` ### OIDC / login errors ```bash # Test OIDC discovery endpoint curl -s https://sso.hospital.ch/realms/kodemed/.well-known/openid-configuration | jq .status # Verify JWK endpoint curl -s https://sso.hospital.ch/realms/kodemed/protocol/openid-connect/certs | jq .keys[0].kid ``` ### WebSocket connection fails - Reverse proxy must support WebSocket upgrades - Apache: enable `mod_proxy_wstunnel` and place `/ws/` ProxyPass before `/` - nginx: add `Upgrade` and `Connection` headers (see Section 8.2) - Check `WEBSOCKET_ALLOWED_ORIGINS` includes the UI domain exactly ### Windows client cannot connect - Verify server URL in client configuration points to the HTTPS reverse proxy - Check that WebSocket port is not blocked by hospital firewall - Test: open `https://api.hospital.ch/actuator/health` in browser ### DLL client opens wrong URL (404 on coding page) **Symptom**: CodingBrowser opens `https://server.hospital.ch/coding?...` instead of the CodingUI URL, resulting in a 404 error. **Cause**: `KODEMED_PUBLIC_UI_URL` is not set. The server returns its own URL as `codingUIUrl` in `/api/v1/config`, but the coding page only exists on the React CodingUI. **Fix**: Set `KODEMED_PUBLIC_UI_URL` to the CodingUI URL in your environment: ```yaml # docker-compose.yml environment: - KODEMED_PUBLIC_UI_URL=https://coding-ui.hospital.ch ``` **Verify**: The server logs a warning at startup if this is missing: ``` CONFIG ⚠ KODEMED_PUBLIC_UI_URL is not set. DLL clients will use the server URL as CodingUI URL, which causes 404. ``` ### WebSocket returns HTTP 200 instead of 101 **Symptom**: DLL clients connect via HTTP but WebSocket upgrade fails silently (HTTP 200 instead of 101 Switching Protocols). **Cause**: The reverse proxy forwards the WebSocket request as a regular HTTP GET, stripping the `Upgrade: websocket` header. **Fix (Apache)**: ```apache # Enable required modules a2enmod proxy_wstunnel rewrite # Virtual host configuration — RewriteRule BEFORE ProxyPass RewriteEngine On RewriteCond %{HTTP:Upgrade} websocket [NC] RewriteCond %{HTTP:Connection} upgrade [NC] RewriteRule ^/ws/(.*) ws://localhost:8080/ws/$1 [P,L] ProxyPass / http://localhost:8080/ ProxyPassReverse / http://localhost:8080/ ``` **Fix (nginx)**: ```nginx location /ws/ { proxy_pass http://localhost:8080/ws/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } ``` **Verify**: `apachectl -M | grep wstunnel` must show `proxy_wstunnel_module`. ### Portal returns 500 on login (oauth2-proxy) **Symptom**: Clicking "Sign In" on the portal results in a 500 error. **Common causes**: | Error in logs | Cause | Fix | |---------------|-------|-----| | `unauthorized_client` | Client secret mismatch | Update `OAUTH2_DOWNLOADS_CLIENT_SECRET` to match the OIDC provider | | `invalid_scope` | `groups` scope not configured | Remove `groups` from `OAUTH2_PROXY_SCOPE` or configure the scope in your OIDC provider | | `invalid_redirect_uri` | Redirect URL mismatch | Ensure `OAUTH2_PROXY_REDIRECT_URL` matches the allowed redirect URIs in the OIDC client | **Debug**: ```bash # Check oauth2-proxy logs docker compose logs kodemed-downloads-auth --tail=50 ``` ### Startup configuration warnings The server validates its configuration at startup and logs warnings for missing settings. Check the logs after deployment: ```bash docker compose logs kodemed-server | grep "CONFIG" ``` Expected output when everything is configured: ``` CONFIG ✓ All public URL configuration present ``` If configuration is missing, you will see specific warnings: ``` CONFIG ⚠ KODEMED_PUBLIC_UI_URL is not set. DLL clients will use the server URL... CONFIG ⚠ 1 configuration warning(s) — DLL clients may not connect correctly. ``` ### Required environment variables checklist | Variable | Required | Used by | Purpose | |----------|----------|---------|---------| | `KODEMED_PUBLIC_UI_URL` | **Yes** (production) | DLL clients | CodingUI URL for browser redirect | | `KODEMED_PUBLIC_SERVER_URL` | Recommended | DLL clients | Server URL returned by `/api/v1/config` | | `KODEMED_PUBLIC_DATASERVER_URL` | Recommended | DLL clients | DataServer URL for classification data | | `KODEMED_PUBLIC_WEBSOCKET_URL` | Recommended | DLL clients | WebSocket URL for real-time communication | | `SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI` | **Yes** | Auth | OIDC issuer URI for token validation | | `CORS_ALLOWED_ORIGINS` | **Yes** | CORS | Allowed origins for API requests | | `WEBSOCKET_ALLOWED_ORIGINS` | **Yes** | WebSocket | Allowed origins for WS connections | | `LOGGING_LEVEL_ROOT` | No | Logging | Root logger level (default: INFO). Use DEBUG for troubleshooting | | `KODEMED_AUTH_ENABLED` | No | Server, DataServer | Enable/disable authentication (default: true) | | `GROUPER_AUTH_ENABLED` | No | GrouperServer **only** | Enable/disable auth on GrouperServer (default: true). **Note: GrouperServer uses `GROUPER_AUTH_ENABLED`, NOT `KODEMED_AUTH_ENABLED`** | | `KODEMED_ADMIN_API_KEY` | Recommended | DataServer | API key for script access to admin endpoints (e.g. reimport). Auto-generated by Linux installer | | `KODEMED_ENCRYPTION_KEY` | **Yes** | Server | AES-256 key (base64, 32 bytes) for sensitive data encryption | | `KODEMED_LICENSE_FILE` | **Yes** | All services | Path to license file (default: `/app/kodemed.license`) | --- ## 17. Quick Reference | Task | Command | |------|---------| | **Start (Docker)** | `docker compose up -d` | | **Stop (Docker)** | `docker compose down` | | **Update (Docker)** | `docker compose pull && docker compose up -d` | | **Logs (Docker)** | `docker compose logs -f kodemed-server` | | **Start (systemd)** | `sudo systemctl start kodemed-server` | | **Status (systemd)** | `sudo systemctl status kodemed-server` | | **Logs (systemd)** | `journalctl -u kodemed-server -f` | | **Health check** | `curl -s localhost:8080/actuator/health` | | **DB backup** | `pg_dump -U kodemed kodemed \| gzip > backup.sql.gz` | | **Helm install** | `helm install kodemed charts/kodemed/ -f values.yaml -n kodemed` | | **Helm upgrade** | `helm upgrade kodemed charts/kodemed/ -f values.yaml -n kodemed` | | **Helm rollback** | `helm rollback kodemed 1 -n kodemed` | | **K8s rollout** | `kubectl rollout status deployment/kodemed-server -n kodemed` | | **MSI silent install** | `msiexec /i KodeMed.msi /quiet SERVERURL="https://..."` | | **Package import data** | `bash scripts/ci/package-import-data.sh 2026` | | **Deploy import data** | `bash scripts/ci/deploy-import.sh --full SERVER` | | **Import scan status** | `curl -H "X-Admin-Key: KEY" localhost:8081/api/admin/import/scan/status` | | **Trigger import scan** | `curl -X POST -H "X-Admin-Key: KEY" localhost:8081/api/admin/import/scan` | --- **Support**: support@kodemed.com | www.kodemed.com *KodeMed DevOps Guide v2026.3 -- Updated 2026-03-08* *KodeMed GmbH -- Switzerland*