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
BusinessKeyare efficient - Queries with
?since-version=Nare 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: Forbidprevents overlapping syncsbackoffLimit: 0means 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"]