Publisert - 18.09.2026

Deployment

Local Development

Prerequisites

  • .NET 10 SDK
  • Docker and Docker Compose

Start all services

docker-compose up -d

Docker Compose reads local secrets (API keys, database credentials) from the .env file in the project root — see Configure API keys and database credentials.

This starts:

  • Postgres on port 5432
  • API on port 5069
  • Frontend on port 5173

To start only the database service:

docker-compose up -d postgres-db

Configure API keys and database credentials

Local secrets live in a .env file in the project root (git-ignored). Copy the example file and fill in the values:

cp .env.example .env
Variable Description
API_KEY_1 API key for the output endpoints (X-API-KEY header) and the frontend
VITE_API_KEY API key for the frontend dev server (Vite)
UPSTREAM_API_KEY API key for the upstream FHIR API
POSTGRES_USER Username for the local Postgres service
POSTGRES_PASSWORD Password for the local Postgres service
Postgres__Username Postgres username for the API when running on the host (dotnet run)
Postgres__Password Postgres password for the API when running on the host (dotnet run)

docker compose reads .env automatically and substitutes the values into docker-compose.yaml. The containerized API gets its database credentials from POSTGRES_USER/POSTGRES_PASSWORD; Postgres__Username/Postgres__Password are only used when running the API on the host (see Run the API).

Run the API

Load the variables from .env into the shell environment, then run the API:

set -a && source .env && set +a
cd backend/src/Api
dotnet run

The API starts on http://localhost:5069 (see Properties/launchSettings.json).

Test endpoints

Use the provided Api.http file (works with VS Code REST Client or JetBrains HTTP Client):

# Trigger sync
curl -X POST http://localhost:5069/api/internal/sync \
  -H "X-API-KEY: dev-test-key-1"

# Fetch treatment groups
curl http://localhost:5069/api/v1/treatment-group \
  -H "X-API-KEY: dev-test-key-1"

Postgres Indexes

Indexes are defined in AppDbContext.OnModelCreating() and applied automatically via EF Core migrations:

Table Index Type
TreatmentGroups BusinessKey Unique
TreatmentGroups Version Ascending
ReimbursementGroups BusinessKey Unique
ReimbursementGroups Version Ascending

These indexes ensure:

  • Upserts by BusinessKey are efficient
  • Queries with ?since-version=N are fast (version index)

Kubernetes Deployment

API Deployment

Standard ASP.NET Core deployment. Key environment variables:

env:
  - name: ALLOWED_CORS_URLS__0
    value: "https://utviklerportal.nhn.no"
  - name: ConnectionStrings__Postgres
    valueFrom:
      secretKeyRef:
        name: postgres-auth
        key: postgres-connection-string
  - name: Postgres__Username
    valueFrom:
      secretKeyRef:
        name: postgres-auth
        key: username
  - name: Postgres__Password
    valueFrom:
      secretKeyRef:
        name: postgres-auth
        key: password
  - name: FORWARDED_TRUSTED_NETWORKS
    value: "10.244.0.0/16"  # Pod CIDR of the Gateway API controller
envFrom:
  - secretRef:
      name: postgres-auth

The FORWARDED_TRUSTED_NETWORKS value is set from forwardedHeaders.trustedNetworks in the Helm values and should be the pod CIDR range where the Gateway API controller runs. This allows the API to trust the X-Forwarded-Proto header only from the gateway, ensuring request.Scheme is correct for DPoP htu validation.

The postgres-auth generic secret is created from an env file by manifests/scripts/create-backend-secret.sh. Besides the explicit postgres-connection-string, username and password keys, it carries the remaining settings as env-file keys, e.g. Sync__UpstreamApiKey and ApiKeys__0.

Database Migrations

The deployment runs EF Core migrations automatically via an init container before the main API starts. The init container uses the same image as the API and passes --migrate as an argument:

initContainers:
  - name: migrate
    image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
    args: ["--migrate"]
    env:
      - name: ConnectionStrings__Postgres
        valueFrom:
          secretKeyRef:
            name: postgres-auth
            key: postgres-migrate-connection-string
    envFrom:
      - secretRef:
          name: postgres-auth

The migrate init container uses an elevated database user (secret key postgres-migrate-connection-string) instead of the regular postgres-connection-string used by the API container. Both map to the same ConnectionStrings__Postgres env var.

The --migrate flag calls dbContext.Database.MigrateAsync() and exits. If migrations fail, the init container fails and the main API container never starts, preventing the app from running against an outdated schema.

To run migrations locally:

# Via the running container
docker exec nompd-api dotnet Api.dll --migrate

# Or via the EF Core CLI
dotnet ef database update --project backend/src/Api/Api.csproj

Sync CronJob

apiVersion: batch/v1
kind: CronJob
metadata:
  name: sync-job
spec:
  schedule: "0 3 * * *"   # Every night at 03:00
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      backoffLimit: 0
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: sync-job
              image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
              args: ["--sync"]
              env:
                - name: ConnectionStrings__Postgres
                  valueFrom:
                    secretKeyRef:
                      name: postgres-auth
                      key: postgres-connection-string
                - name: Postgres__Username
                  valueFrom:
                    secretKeyRef:
                      name: postgres-auth
                      key: username
                - name: Postgres__Password
                  valueFrom:
                    secretKeyRef:
                      name: postgres-auth
                      key: password
              envFrom:
                - secretRef:
                    name: postgres-auth

Key settings:

  • The job runs the same API image with --sync, executing the sync in-process against Postgres — no HTTP call to the API required
  • concurrencyPolicy: Forbid prevents overlapping syncs
  • backoffLimit: 0 means a failed job is not retried; the next scheduled run picks it up (see Sync Pipeline)

Health Checks

The API exposes an unauthenticated health check endpoint suitable for Kubernetes liveness and readiness probes:

curl http://localhost:5069/api/internal/health

Response when healthy (200 OK):

{
  "status": "healthy",
  "timestamp": "2025-04-10T08:30:00Z",
  "components": {
    "postgres": "healthy"
  }
}

Response when unhealthy (503 ServiceUnavailable):

{
  "status": "unhealthy",
  "timestamp": "2025-04-10T08:30:00Z",
  "components": {
    "postgres": {
      "status": "unhealthy",
      "error": "Connection refused"
    }
  }
}

The endpoint pings Postgres to verify database connectivity. It does not require the X-API-KEY header. See API Reference for full details.

Docker Build

The API Dockerfile uses a multi-stage build with layer caching for NuGet restore. Note that docker-compose.yaml sets context: ./backend/src/Api, so paths are relative to the Api directory.

Postgres Backups

The db-backup Helm chart (manifests/apps/db-backup) handles scheduled backups and restores of the CNPG Postgres cluster. See Database Backup and Restore for component details and the restore runbook.

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY Api.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app

FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app .
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
ENTRYPOINT ["dotnet", "Api.dll"]

Søk i Utviklerportalen

Søket er fullført!