Repository navigation
Running the Images
Docker run, Docker Compose, and Kubernetes.
All containers listen on port 8080 inside the container, regardless of image. Map it wherever you like on the host.
| Endpoint | Path |
|---|---|
| Studio UI |
/ (on images containing Studio) |
| Elsa API | /elsa/api |
| Readiness | /health |
| Liveness | /alive |
docker run -d --name elsa-pro -p 8080:8080 \
-v "$(pwd)/config.json:/config/config.json:ro" \
-v elsa-data:/data \
ghcr.io/valence-works/runtime-ce-combined:latestPutting settings in a mounted config.json rather than a long list of -e flags keeps secrets out
of docker inspect and your shell history. See Configuration.
docker network create elsa
docker run -d --name elsa-server --network elsa -p 8080:8080 \
-v "$(pwd)/server-config.json:/config/config.json:ro" \
-v elsa-data:/data \
ghcr.io/valence-works/runtime-ce-server:latest
docker run -d --name elsa-studio --network elsa -p 8081:8080 \
-v "$(pwd)/studio-config.json:/config/config.json:ro" \
ghcr.io/valence-works/runtime-ce-studio:latestThe URL wiring between them depends on the Studio hosting model — see Choosing an Image.
A realistic starting point: combined image, PostgreSQL, persistent volumes, health checks.
services:
elsa:
image: valenceruntimeimages.azurecr.io/runtime-combined:3.8.0-preview.5413-build.62
ports:
- "8080:8080"
volumes:
- ./config:/config:ro
environment:
- ASPNETCORE_ENVIRONMENT=Production
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
postgres:
image: postgres:17
environment:
POSTGRES_USER: elsa
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
POSTGRES_DB: elsa
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U elsa -d elsa"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
postgres-data:./config/config.json:
{
"CShells": {
"Shells": {
"Default": {
"Features": {
"DefaultAdminUser": {
"AdminUsername": "admin",
"AdminPassword": "ChangeThisPassword123!"
},
"Identity": {
"SigningKey": "GENERATE_A_REAL_256_BIT_KEY"
},
"PostgreSqlWorkflowPersistence": {
"ConnectionString": "Host=postgres;Port=5432;Database=elsa;Username=elsa;Password=..."
},
"PostgreSqlIdentityPersistence": {
"ConnectionString": "Host=postgres;Port=5432;Database=elsa;Username=elsa;Password=..."
}
}
}
}
}
}PostgreSQL persistence requires the provider package to be loaded — see Extending with Nuplane.
docker compose up -d
docker compose logs -f elsa
depends_onwithcondition: service_healthymatters. Elsa applies migrations at startup; if the database isn't accepting connections yet, startup fails. The dependency waits for the healthcheck rather than merely for the container to exist. Note that the healthcheck here is on postgres, whose image shipspg_isready.
⚠️ Don't add ahealthcheck:to the Elsa service. A Compose healthcheck runs a command inside the container, and these images are chiselled — no shell, nocurl, nowget. The familiartest: ["CMD", "curl", "-f", "http://localhost:8080/health"]cannot execute, so the container is markedunhealthyforever, and anything withcondition: service_healthyon it waits indefinitely. That is the trade for a minimal attack surface, and it is deliberate.Probe from outside instead:
curl -f http://localhost:8080/healthon the host, your load balancer's health probe, or — in Kubernetes — anhttpGetprobe, which the kubelet performs itself and which needs nothing inside the image. See the manifest below.
services:
elsa-server:
image: valenceruntimeimages.azurecr.io/runtime-server:3.8.0-preview.5413-build.62
ports: ["8080:8080"]
volumes: ["./config/server:/config:ro"]
elsa-studio:
image: valenceruntimeimages.azurecr.io/runtime-studio:3.8.0-preview.1667-build.62
ports: ["8081:8080"]
volumes: ["./config/studio:/config:ro"]
depends_on: [elsa-server]depends_on here is the plain form, not condition: service_healthy — the Elsa service has no
healthcheck for the reason above, so waiting on one would never return. Studio tolerates the server
arriving late: in WebAssembly mode the browser calls the API, and in Blazor Server mode the call
happens per request, not at startup.
./config/studio/config.json — WebAssembly, so the URL is browser-reachable:
{
"Studio": {
"HostingModel": "WebAssembly",
"Client": { "Backend": { "Url": "http://localhost:8080/elsa/api" } }
}
}And the server needs the matching CORS origin:
{ "Elsa": { "Cors": { "AllowedOrigins": ["http://localhost:8081"] } } }For Blazor Server instead, drop the CORS entry and use a container-internal URL:
{
"Studio": { "HostingModel": "BlazorServer" },
"Backend": { "Url": "http://elsa-server:8080/elsa/api" }
}Sketch, not a production manifest — no TLS, ingress or autoscaling here.
apiVersion: apps/v1
kind: Deployment
metadata:
name: elsa
spec:
replicas: 1
selector:
matchLabels: { app: elsa }
template:
metadata:
labels: { app: elsa }
spec:
imagePullSecrets:
- name: valence-registry # created in Pulling Images
# Required. The images run as UID 1654, while emptyDir and most
# PersistentVolumes are created root-owned — without fsGroup the process
# cannot write to its own mounts and startup fails.
securityContext:
runAsNonRoot: true
runAsUser: 1654
fsGroup: 1654
containers:
- name: elsa
image: valenceruntimeimages.azurecr.io/runtime-combined:3.8.0-preview.5413-build.62
ports:
- containerPort: 8080
env:
- name: ASPNETCORE_ENVIRONMENT
value: Production
- name: CShells__Shells__Default__Features__Identity__SigningKey
valueFrom:
secretKeyRef: { name: elsa-secrets, key: signing-key }
volumeMounts:
- name: config
mountPath: /config
readOnly: true
# Extracted packages, Nuplane store state — and HOME, which is where
# ASP.NET Data Protection keeps its key ring. The packages are
# reproducible; the key ring is NOT. On this emptyDir a replacement
# pod gets new keys and existing auth cookies stop being readable,
# so users are signed out on every rollout. Fine for one replica if
# you accept that; otherwise make it a PVC or persist the key ring
# to a shared store. See the Production Checklist.
- name: runtime-cache
mountPath: /var/lib/valence-runtime
# NuGet scratch space.
- name: tmp
mountPath: /tmp
# Only needed with the default SQLite database — with an external
# database, drop this mount and the volume below. If you keep
# SQLite, this must be a PersistentVolumeClaim: an emptyDir here
# loses every workflow when the pod is rescheduled.
- name: data
mountPath: /data
readinessProbe:
httpGet: { path: /health, port: 8080 }
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet: { path: /alive, port: 8080 }
initialDelaySeconds: 30
periodSeconds: 30
resources:
requests: { memory: 512Mi, cpu: 250m }
limits: { memory: 2Gi, cpu: "2" }
volumes:
- name: config
configMap: { name: elsa-config }
- name: runtime-cache
emptyDir: {}
- name: tmp
emptyDir: {}
- name: data
persistentVolumeClaim: { claimName: elsa-data }Use /health for readiness and /alive for liveness. /health includes dependency checks, so
wiring it to liveness means a brief database blip restarts your pods — turning a recoverable
degradation into a crash loop.
Don't set replicas: 2 without first configuring distributed runtime coordination. Multiple
instances sharing a database, with no distributed locking or a message bus, will double-execute
scheduled and triggered workflows. You need a message bus (RabbitMQ or Azure Service Bus) and
distributed runtime features enabled — see Extending with Nuplane.
All of these run on the host — the images are chiselled, so there is nothing to docker exec
into.
docker logs -f elsa-pro # follow logs
curl -f http://localhost:8080/health # readiness
docker stats elsa-pro # resource use
# what config actually landed
docker inspect elsa-pro --format '{{json .Config.Env}}' | tr ',' '\n' | grep CShells# NEW_TAG is the immutable tag you are upgrading *to* — check the repository
# for the current one rather than guessing the build number.
NEW_TAG=<elsa-version>-build.<n>
docker pull valenceruntimeimages.azurecr.io/runtime-combined:$NEW_TAG
docker stop elsa-pro && docker rm elsa-pro
docker run -d --name elsa-pro ... valenceruntimeimages.azurecr.io/runtime-combined:$NEW_TAGBack up your database first. Elsa applies schema migrations on startup, and migrations are not reversible by stopping the container — rolling back the image does not roll back the schema. Read the release notes before a minor-version jump.
Next: Configuration · Persistence and Databases · Production Checklist
Valence Runtime — a product of Valence Works · Elsa Workflows is open source under the MIT License · Found an error in these docs? Open an issue.
Start here
Deploying
Going further
Commercial