Deployment Guide
Overview
Virtufin API can be deployed using Docker containers and Kubernetes. This guide covers setup for private package registries, local Docker deployment, and Kubernetes deployment.
Private Package Registries
All client libraries are published to private Gitea package registries. Configure authentication before installing.
Obtaining API Tokens
- Navigate to
https://git.haenerconsulting.com/user/settings/applications - Create a new access token with scope
read:packages - Store the token securely as it will be used as a password
Docker
# Authenticate
docker login docker.haenerconsulting.com -u <username>
# Pull the image
docker pull docker.haenerconsulting.com/virtufin/api:latest
# Pull backend service images
docker pull docker.haenerconsulting.com/virtufin/workmanager:latest
docker pull docker.haenerconsulting.com/virtufin/websocketmanager:latest
Credentials are stored in ~/.docker/config.json after login.
NuGet
Option 1 — NuGet.Config XML:
Create or edit ~/.nuget/NuGet/NuGet.Config:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="gitea" value="https://nuget.haenerconsulting.com/api/packages/virtufin/nuget/index.json" />
</packageSources>
<packageSourceCredentials>
<gitea>
<add key="Username" value="<username>" />
<add key="ClearTextPassword" value="<api-token>" />
</gitea>
</packageSourceCredentials>
</configuration>
Option 2 — dotnet CLI:
dotnet nuget add source https://nuget.haenerconsulting.com/api/packages/virtufin/nuget/index.json \
-n gitea -u <username> -p <token>
Install package:
dotnet add package Virtufin.Api.Client
PyPI
Setup pip.conf:
Linux: ~/.config/pip/pip.conf
macOS: ~/Library/Application Support/pip/pip.conf
[global]
index-url = https://pypi.haenerconsulting.com/api/packages/virtufin/pypi/
[install]
trusted-host = pypi.haenerconsulting.com
Install with authentication:
export PIP_USERNAME=<username>
export PIP_PASSWORD=<api-token>
pip install virtufin-api
NPM
Setup ~/.npmrc:
npm config set @gitea:registry https://npm.haenerconsulting.com/api/packages/virtufin/npm/
npm config set //npm.haenerconsulting.com/api/packages/virtufin/npm/:_authToken <token>
Install package:
npm install @virtufin/api
Local Docker Deployment
Pull and Run
# Pull the API gateway image
docker pull docker.haenerconsulting.com/virtufin/api:latest
# Run with configuration
docker run -p 5001:5001 \
-p 5002:5002 \
-e ServicesConfigPath=/config/services.json \
-e HttpPort=5001 \
-e GrpcPort=5002 \
-e DaprHttpPort=3500 \
-e DaprGrpcPort=50001 \
-v /path/to/services.json:/config/services.json \
docker.haenerconsulting.com/virtufin/api:latest
Ports
| Port | Protocol | Description |
|---|---|---|
| 5001 | HTTP/gRPC-Web | HTTP port (REST, Swagger, health, gRPC-Web) |
| 5002 | gRPC (h2c) | Native gRPC HTTP/2 port |
Required Environment Variables
| Variable | Default | Description |
|---|---|---|
HttpPort |
5001 | HTTP port (REST, Swagger, gRPC-Web) |
GrpcPort |
5002 | gRPC port (native gRPC h2c) |
DaprHttpPort |
3500 | Dapr sidecar HTTP port |
DaprGrpcPort |
50001 | Dapr sidecar gRPC port |
ServicesConfigPath |
config/services.json | Path to services configuration (relative to CWD; override with the ServicesConfigPath config key) |
Note:
Dapr__HttpEndpointandDapr__GrpcEndpointenvironment variables are deprecated. UseDaprHttpPortandDaprGrpcPortinstead. The deploy script sets these automatically.Important: All backend services registered in the config must implement gRPC reflection. The API Gateway discovers methods and executes calls via reflection. Services without reflection cannot be proxied.
Verify Deployment
# Health check
curl http://localhost:5001/health
# List services (requires running backend services)
curl http://localhost:5001/v1/gateway/list-services
Kubernetes Deployment
Generic Kubernetes
Prerequisites
- Kubernetes 1.28+ cluster
- kubectl configured with cluster access
- Dapr CLI installed locally
Install Dapr
dapr init -k
dapr status -k
Deploy
helm install virtufin-api ../helm \
--namespace virtufin --create-namespace \
--set api.enabled=true
Verify
kubectl get pods -n virtufin
kubectl get svc -n virtufin
kubectl logs -n virtufin -l app.kubernetes.io/name=virtufin-api
kubectl rollout status deployment/virtufin-api -n virtufin
Local Development with Colima (macOS)
Colima provides a lightweight Kubernetes cluster for local development on macOS.
Prerequisites
- macOS
- Colima installed:
brew install colima - Docker CLI (or Docker Desktop for docker context):
brew install docker
Start Colima with Kubernetes
# Start Colima with Kubernetes enabled
colima start --kubernetes
# Verify kubectl context
kubectl config current-context
# Expected: colima
# Optional: configure docker to use colima context
docker context ls
docker context use colima
Install Dapr
dapr init -k
dapr status -k
Deploy
helm install virtufin-api ../helm \
--namespace virtufin --create-namespace \
--set api.enabled=true
Verify
kubectl get pods -n virtufin
kubectl get svc -n virtufin
kubectl logs -n virtufin -l app.kubernetes.io/name=virtufin-api
kubectl rollout status deployment/virtufin-api -n virtufin
# Access the API
curl http://localhost:5001/health
Stop Colima
colima stop
Troubleshooting
- Pod stuck in Pending: Check
kubectl describe pod <pod> -n virtufinfor resource allocation issues - Dapr sidecar not starting: Ensure Kubernetes support is enabled with
colima status - Context not set: Run
kubectl config use-context colima
Podman Kubernetes
Podman Desktop or Podman CLI can provide a Kubernetes cluster for local development.
Prerequisites
- Podman Desktop (recommended) or Podman CLI v4.0+
- Linux, macOS, or Windows with WSL2
Setup with Podman Desktop
- Download and install Podman Desktop
- Open Podman Desktop → Settings → Kubernetes
- Enable "Enable Kubernetes" checkbox
- Choose single-node cluster option
- Click "Apply and Restart"
Setup with Podman CLI
# Check podman version (4.0+ required for k8s)
podman version
# Create a machine with Kubernetes enabled (Podman v4.1+)
podman machine init --kubernetes-enabled=true my-k8s
podman machine start my-k8s
# Or enable Kubernetes on existing machine
podman machine stop my-machine
podman machine set --kubernetes-enabled=true my-machine
podman machine start my-machine
Verify Context
kubectl config current-context
# Expected: podman
# If not set:
kubectl config use-context podman
Install Dapr
dapr init -k
dapr status -k
Deploy
helm install virtufin-api ../helm \
--namespace virtufin --create-namespace \
--set api.enabled=true
Verify
kubectl get pods -n virtufin
kubectl get svc -n virtufin
kubectl logs -n virtufin -l app.kubernetes.io/name=virtufin-api
kubectl rollout status deployment/virtufin-api -n virtufin
Access the API
On macOS with Colima or Podman, port forwarding may be needed:
kubectl port-forward -n virtufin svc/virtufin-api 5001:5001 5002:5002
# Then access: http://localhost:5001/health (HTTP), http://localhost:5002 (gRPC)
Stop Podman Machine
podman machine stop my-k8s
Troubleshooting
- Kubernetes not enabled: Ensure Podman Desktop settings have Kubernetes enabled
- Port conflicts: Ensure port 5001 is not in use
- Image pull issues: Verify
docker.haenerconsulting.comregistry credentials are configured
Production Considerations
Scaling
Kubernetes:
spec:
replicas: 3 # Scale API horizontally
Kubernetes Services handle backend load balancing automatically — set
host to the Service DNS name in services.json and kube-proxy distributes
calls across pods.
Non-Kubernetes (multi-instance round-robin):
For bare-metal or VM deployments, configure multiple backend instances
with the hosts array. The gateway round-robins gRPC calls across all
configured hosts:
{
"services": [{
"name": "workmanager",
"grpc": {
"hosts": [
{"host": "instance-1", "port": 25002},
{"host": "instance-2", "port": 25002}
]
}
}]
}
When hosts is omitted, the single host/port pair is used.
Resource Limits
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "1000m"
Health Checks
livenessProbe:
httpGet:
path: /health
port: 5001
initialDelaySeconds: 10
periodSeconds: 15
readinessProbe:
httpGet:
path: /health
port: 5001
initialDelaySeconds: 5
periodSeconds: 10
Security
- TLS Termination: Configure TLS at ingress level
- CORS: Restrict CORS policies in production
- Authentication: Add authentication layer if needed
- Secrets: Use Kubernetes Secrets for sensitive data
Troubleshooting
Pod Not Starting
kubectl describe pod <pod-name> -n virtufin
kubectl logs <pod-name> -n virtufin
Dapr Not Working
# Check Dapr sidecar
kubectl get pods --show-labels -n virtufin | grep dapr
# View Dapr logs
kubectl logs <pod-name> -c daprd -n virtufin
Cannot Connect to Backend Services
- Verify backend services are deployed and running
- Check DNS resolution:
kubectl exec -it <pod> -n virtufin -- nslookup <service-name> - Verify gRPC ports are accessible
- Check ConfigMap values match backend service addresses
Image Pull Failures
- Verify
regcredharborsecret exists:kubectl get secret regcredharbor -n virtufin - Recreate if missing:
kubectl create secret docker-registry regcredharbor \ --namespace=virtufin \ --docker-server=docker.haenerconsulting.com \ --docker-username=<username> \ --docker-password=<token>
Health Check Failures
- Verify API is listening on correct port (5001)
- Check service has gRPC reflection enabled (mandatory for API Gateway to function)
- Review logs for startup errors:
kubectl logs -f <pod-name> -n virtufin
Known Limitations
Dapr gRPC App Health Checks Not Supported
When using Dapr with app-protocol: grpc, the Dapr sidecar probes the application via the dapr.proto.runtime.v1.AppCallbackHealthCheck gRPC service. The Dapr .NET SDK does not currently provide a server-side implementation of this service. As a result, Dapr app health checks are disabled (dapr.io/enable-app-health-check: false).
Kubernetes liveness and readiness probes (/health endpoint) continue to handle pod health monitoring. Future improvement: implement the Dapr AppCallback gRPC service to enable Dapr's soft health check mechanism for pausing work delivery during unhealthy states.
v0.0.42 Changes
Flat JSON pubsub format
WebSocket messages published to Dapr pub/sub no longer use the
MessageEnvelope wrapper. The CloudEvent data field now contains
the raw WebSocket payload as a JSON object directly. Metadata
(websocketId, websocketUrl, timestamp, contentType) is attached
as CloudEvent extensions instead of being wrapped inside the payload.
Consumers now read data with a single json.loads() step — no base64
decoding, no .payload field indirection.
Dapr config keys
Dapr__HttpEndpoint and Dapr__GrpcEndpoint environment variables are
replaced by DaprHttpPort and DaprGrpcPort. The deploy script sets
these automatically. Existing dotnet run defaults remain 50001.
Multi-instance gRPC scaling
The gateway GrpcChannelPool supports round-robin across multiple
backend instances via the hosts array in services.json (see
Scaling section above). In Kubernetes, a single Service DNS name with
replicas is the recommended approach — hosts is for non-K8s deployments.
WorkManager Dapr subscription fix
The WorkManager now connects DaprPublishSubscribeClient to its own
Dapr sidecar port instead of the API Gateway's. Consumer groups are
correctly named workmanager rather than apigateway.