Repository navigation
Production Checklist
Everything that must change between "it runs on my laptop" and "it runs in production".
Images now run non-root (UID 1654) on a chiselled base — no shell, no package manager, 11 OS packages instead of 112. Vulnerability scanning runs as a build gate before publication, and every image is signed and ships an SBOM and build provenance. This checklist covers what you control.
- Signing key generated and set.
openssl rand -base64 32. Never ship the placeholder — it is publicly known, and anyone with it can mint valid tokens for your instance. - Admin password changed from the default
password. - Secrets injected from a secret store, not baked into an image or committed to git.
- No secrets on the command line — they land in shell history and
docker inspect. - Distinct keys per environment. Staging must not share a signing key with production.
- A real database, not SQLite. → Persistence and Databases
- Both workflow and identity persistence configured.
- Backups running, and a restore actually tested.
- Connection pool sized for real concurrency.
- TLS terminated at a reverse proxy or ingress. The containers speak plain HTTP.
- CORS restricted to specific origins. Never
*. - API not publicly exposed unless you need it to be. Consider Blazor Server Studio so the API stays internal → Choosing an Image
-
Http:HttpActivityOptions:BaseUrlset to the externally reachable URL, if workflows expose HTTP endpoints.
- Pinned to an immutable tag —
<elsa-version>-build.<n>, or a digest. Neverlatest. → Pulling Images - Nuplane packages pinned to exact versions, so a restart isn't an unplanned upgrade.
-
AutomaticReconciliationdisabled, so package changes follow your deploy schedule. - Registry credentials stored as secrets. Your subscription token is pull-only by construction; keep it in a secret store, never in a repository.
- Image signature verified before deployment, and the signing identity checked. → Pulling Images
- Health probes wired:
/healthfor readiness,/alivefor liveness. Not the other way around —/healthchecks dependencies, so using it for liveness turns a brief database blip into a crash loop. - Resource limits set. Unbounded memory plus a runaway workflow evicts its neighbours.
- Logs shipped somewhere that survives container replacement.
- Restart policy set (
unless-stopped, or a Kubernetes controller).
The images emit OpenTelemetry traces, metrics and structured logs. Point them at a collector:
-e OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
-e OTEL_SERVICE_NAME=elsa-productionWorth alerting on: workflow execution failure rate, execution latency, /health failures, database
connection pool exhaustion.
Before running more than one replica:
- Shared database — never SQLite
- Message bus configured (RabbitMQ or Azure Service Bus)
- Distributed runtime features enabled
- Durable scheduling (Quartz), not in-memory
⚠️ Multiple replicas without distributed coordination execute scheduled and triggered workflows more than once. This is not a performance problem, it is a correctness one, and it will not be obvious in testing. → Extending with Nuplane
- Admin permissions narrowed from
["*"]where practical - Studio not publicly reachable unless it needs to be
- A plan for rotating the signing key (it invalidates active sessions)
- Back up the database first. Migrations run at startup and rolling the image back does not roll the schema back.
- Read the release notes.
- Test in a non-production environment on a copy of production data.
- Know your rollback path — including what to do about a migrated schema.
- Upgrade one minor version at a time.
What Valence Works provides: non-root images on a minimal chiselled base, CVE scanning as a build gate that runs before publication, a published SBOM and build provenance, and cosign-signed images — all live now, on both the Community and paid lines. A committed security patch cadence is what the paid tiers add on top. → Pulling Images for how to verify a signature yourself.
Everything the application writes lives outside /app, on three paths with different natures — worth
mounting differently:
| Path | Holds | Mount as |
|---|---|---|
/data |
The default SQLite database | A real volume / PVC, if you use SQLite. Back it up |
/var/lib/valence-runtime |
Extracted packages, Nuplane store state and lock file — and HOME, so the Data Protection key ring |
See below |
/tmp |
NuGet scratch space | tmpfs |
On an external database, /data is unused — with the SQLite features disabled nothing opens
/data/elsa.db, so you can skip that mount entirely.
/var/lib/valence-runtime is not purely a cache. The packages and store state are
reproducible, but HOME lives here too, which is where ASP.NET Data Protection keeps its key ring.
On an emptyDir, a replacement pod gets a new key ring and every previously issued
authentication cookie becomes unreadable — users are signed out on each rollout, and with more than
one replica they are signed out unpredictably. Choose deliberately:
-
Single replica, sign-outs on deploy acceptable →
emptyDiris fine. -
Anything else → back it with a persistent volume, or configure Data Protection to persist its
keys to a shared store and keep the
emptyDir.
Supported. Mount the three paths above writable — /data only if you use SQLite — and the root
filesystem can be read-only:
docker run --read-only \
-v elsa-data:/data -v elsa-runtime:/var/lib/valence-runtime --tmpfs /tmp \
-v "$(pwd)/packages:/app/packages:ro" ...A package directory may be mounted :ro, and needs no particular ownership.
Verified on the server and combined images: read-only root, --network none, :ro packages — full
package set loaded, no filesystem violations, serving normally.
Studio needs no writable mounts at all — it hosts no Elsa runtime, so it has no package loader
and no database. It does still register ASP.NET Data Protection, which wants to write a key ring
under HOME; on a read-only root that falls back to in-memory keys, so plan for the same sign-out
behaviour described above.
There is also no container HEALTHCHECK, because a chiselled image has no shell or curl to run
one. Use an orchestrator HTTP probe against /health and /alive — Kubernetes httpGet needs
nothing inside the image.
What remains yours: TLS, network boundaries, secret management, database security, access control, backups, and everything about the workflows you author.
Report suspected vulnerabilities privately — see Editions and Support. Please don't use the public issue tracker for these.
These run on the host. The images are chiselled — no shell, no printenv, no curl — so
anything of the shape docker exec <container> printenv fails with executable file not found.
Inspect from outside instead.
# What config actually landed, and is the signing key still the placeholder?
docker inspect elsa --format '{{json .Config.Env}}' | tr ',' '\n' | grep -i -E 'cshells|signingkey'
# Running as non-root? Expect 1654.
docker inspect <image> --format '{{.Config.User}}'
# Pinned to a digest?
docker inspect --format='{{index .RepoDigests 0}}' <image>
# Healthy?
curl -f https://elsa.example.com/healthNext: Troubleshooting · Editions and Support
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