Deployment and setup
Lumaft ships as one container image. It contains the browser console, the API, and the backend observer. Everything else — storage, TLS, credentials, backups — comes from the environment you deploy it into.
This page lists what every deployment needs and helps you choose an environment. The environment guides then walk through each platform step by step.
Prerequisites
The image
- One edition-neutral OCI image per release:
ghcr.io/dekglas/lumaft(canonical) anddocker.io/dekglas/lumaft(mirror). Both resolve the same digest for a given version. - Multi-architecture:
linux/amd64andlinux/arm64. - Runs as the unprivileged
nodeuser (UID/GID 1000). Supports a read-only root filesystem when/dataand/tmpare writable mounts. - Listens on port
8080. - Every published digest is keylessly signed. Verify before you pull.
There is no latest tag. Always deploy by digest, taken from the release record
(releases/<version>/release.json) in the public distribution repository. Pulling the image
needs no registry login; if you mirror it into a private registry, your platform needs that
registry's credentials instead.
New to containers? Read Quickstart first. It runs Lumaft on your own machine in about fifteen minutes and explains each moving part once, so the environment guides below make sense.
LUMAFT_VERSION=<version>
LUMAFT_DIGEST=sha256:<digest from releases/$LUMAFT_VERSION/release.json>
LUMAFT_IMAGE="ghcr.io/dekglas/lumaft@$LUMAFT_DIGEST"
cosign verify \
--certificate-identity-regexp '^https://github\.com/dekglas/application/\.github/workflows/promote-public-release\.yml@' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
"$LUMAFT_IMAGE"
docker pull "$LUMAFT_IMAGE"
Compute
The measured starting point is 1 vCPU and 2 GiB of memory for a SQLite workload of 20,000 operation summaries across 200 stacks. Measure your own peak workload before growing beyond that envelope; if read latency or cold readiness degrades, plan PostgreSQL adoption or requalify your allocation.
Storage
Lumaft keeps its durable record in one of two databases.
| Database | Edition | Replicas | Storage requirement |
|---|---|---|---|
| SQLite (default) | Community | Exactly one | An exclusive, durable, block-backed volume mounted at /data |
| Customer-operated PostgreSQL | Business | One | A PostgreSQL server you run, size, back up, and patch |
| PostgreSQL with replicas | Enterprise | Declared ceiling | The same server with a declared connection budget |
SQLite runs in WAL mode. The /data volume must therefore be:
- exclusive to one Lumaft replica;
- presented to the host as local block storage (a disk, an EBS volume, a managed disk, a VMDK);
- independent of the container's lifecycle, so replacing or scaling the compute to zero does not delete it.
The following are not supported for SQLite: the container writable layer, task-local scratch
storage, emptyDir, EFS, NFS, SMB, Azure Files, ReadWriteMany volumes, any volume shared by more
than one replica, and ECS service-managed EBS volumes (ECS deletes those with the task).
If your platform cannot provide that volume lifecycle, use PostgreSQL. See Database integration.
Secrets as files
Two inputs are read from files, not environment variables:
| File | Named by | Purpose |
|---|---|---|
| Bootstrap administrator password | LUMAFT_LOCAL_ADMIN_PASSWORD_FILE |
Creates the first administrator on first startup only |
| Backend definitions | LUMAFT_BACKENDS_FILE |
The validated JSON list of backends to observe |
Both must be direct regular files below a protected directory: absolute path, not a symlink,
parent directory not writable by group or others, file not writable by group or others, readable
by UID 1000. Kubernetes-style projected volumes that materialize secrets as symlinks are rejected.
The pattern that works on every platform is a small init step that writes the files into a
task-local volume mounted at /run/lumaft before the application container starts. Each
environment guide shows it.
Why UID 1000 keeps appearing. The process inside the container runs as the node user,
whose numeric ID is 1000. Linux checks file permissions by numeric ID, so a file you create on a
Linux host as another user is unreadable inside the container until you chown 1000:1000 it.
Docker Desktop on macOS and Windows maps ownership for you, which is why the same command works
there without the chown and fails on a Linux server with it missing. Every guide on this site
includes the chown; do not skip it on Linux.
Backend credentials
Lumaft reads S3 through the standard AWS SDK credential provider chain. Prefer the platform's
workload identity — an ECS task role, an EC2 instance profile, a web-identity token — over static
keys. The identity needs s3:ListBucket and s3:GetObject on the state namespace and nothing
else. Credentials never go in the backends file. See
Connect a backend.
Networking
| Direction | Requirement |
|---|---|
| Inbound | HTTPS, terminated by the environment (load balancer or reverse proxy), forwarded to the container on 8080 |
| Outbound | HTTPS to the object store, to the container registry at deploy time, and to the licensing service for connected Business or Enterprise licenses |
Lumaft itself does not terminate TLS. Any deployment reachable off the loopback interface must sit
behind TLS; the default session cookie is __Host-lumaft-session with the Secure attribute, and
browsers will not retain it over plain HTTP. LUMAFT_ALLOW_HTTP=true exists only for a browser on
the same machine reaching 127.0.0.1.
Health endpoints
| Endpoint | Auth | Meaning |
|---|---|---|
/api/v1/health |
Public | Process liveness |
/api/v1/readiness |
Public | Returns 200 only after a bounded database query succeeds; 503 otherwise |
/api/v1/storage-health |
Administrator | Write probe, free space, database file sizes, last durable write |
Readiness never contacts a configured backend, so a bad S3 credential does not take the service
out of rotation. Use readiness to route traffic and liveness to restart a process. The image's
built-in HEALTHCHECK calls readiness every 30 seconds.
Choose an environment
| Environment | Best for | Database | Guide |
|---|---|---|---|
| Amazon EC2 | The simplest AWS shape: one instance, one volume, one container | SQLite or PostgreSQL | Set up on AWS EC2 |
| Amazon ECS on EC2 | Teams already running ECS who want SQLite on a retained EBS volume | SQLite or PostgreSQL | Set up on AWS ECS |
| Amazon ECS on Fargate | Teams who want no hosts to manage and can run PostgreSQL | PostgreSQL (Business) | Set up on AWS ECS |
| Kubernetes | Clusters with a block StorageClass (EKS, AKS, GKE, on-premises) | SQLite or PostgreSQL | Set up on Kubernetes |
| Azure Container Apps | Azure-native serverless containers | PostgreSQL (Business) | Set up on Azure |
| Azure Virtual Machines | Azure with SQLite on a managed disk | SQLite or PostgreSQL | Set up on Azure |
| VMware vSphere | On-premises and private cloud, including air-gapped sites | SQLite or PostgreSQL | Set up on VMware |
On AWS, Deploy with Pulumi stands up either the EC2 or the ECS shape from one program, and IAM setup on AWS covers the roles every AWS shape needs. Every VM-based shape ends on Docker Compose and systemd.
Two rules decide the column that matters most:
- SQLite needs an exclusive local block volume. EC2, ECS on EC2, Kubernetes with a block StorageClass, Azure VMs, and vSphere can provide one. Fargate and Container Apps cannot.
- PostgreSQL adoption is a Business capability. Continuing to run an adopted database never depends on the license, but starting a new installation on PostgreSQL, or migrating one to it, does.
Configuration reference
All settings are environment variables. Files named by a setting must meet the file rules above. The complete list, including licensing, federated sign-in, capture, and audit settings, is the Configuration reference; the tables below cover what a first deployment needs.
Core
| Variable | Default | Purpose |
|---|---|---|
PORT |
8080 |
Listening port |
LUMAFT_SQLITE_PATH |
/data/lumaft.db |
SQLite database path; parent must exist with mode 0700 |
LUMAFT_BACKENDS_FILE |
unset | Backend definitions; unset starts with no backends |
LUMAFT_LOCAL_ADMIN_PASSWORD_FILE |
unset | Bootstrap administrator password; remove after first startup |
LUMAFT_LOCAL_ADMIN_USERNAME |
admin |
Bootstrap administrator username, set before first startup |
LUMAFT_LOCAL_ADMIN_PASSWORD_ROTATE |
false |
One-shot lost-password rotation; see Users and permissions |
LUMAFT_PUBLIC_ORIGIN |
unset | Externally reachable HTTPS origin; required for OIDC and SAML |
LUMAFT_ALLOW_HTTP |
false |
Loopback-only evaluation exception; never set off the loopback interface |
Database
| Variable | Default | Purpose |
|---|---|---|
LUMAFT_POSTGRES_URL |
unset | PostgreSQL connection URL for a fresh installation |
LUMAFT_POSTGRES_URL_FILE |
unset | Same, read from a protected file (preferred) |
LUMAFT_REPLICAS_MAX |
1 |
Maximum simultaneous replicas (Enterprise, PostgreSQL only) |
LUMAFT_POSTGRES_POOL_MAX |
1 |
Pooled connections per replica |
LUMAFT_POSTGRES_CONNECTION_HEADROOM |
10 |
Connections reserved for your other clients |
LUMAFT_MIGRATION_WAIT_SECONDS |
300 |
How long a replica waits for the migrating replica |
LUMAFT_SCHEMA_UPGRADE_APPLY |
unset | Authorizes one named gated migration; remove afterwards |
LUMAFT_SCHEMA_UPGRADE_RECOVERY_DIRECTORY |
beside the database | Where the pre-migration recovery point is written |
Licensing
| Variable | Purpose |
|---|---|
LUMAFT_LICENSING_SERVICE_URL |
Connected licensing endpoint for Business and Enterprise |
LUMAFT_CONNECTED_LICENSE_DIR |
Local connected-license state directory |
LUMAFT_OFFLINE_SITE_LICENSE_FILE |
Enterprise Advanced Security offline site license |
Optional capture and audit
| Variable | Purpose |
|---|---|
LUMAFT_TRANSCRIPT_KEY_FILE |
32-byte base64 key for transcript encryption, outside /data |
LUMAFT_TRANSCRIPT_REDACTION_FILE |
Additional redaction rules (JSON, ≤ 64 KiB) |
LUMAFT_STATE_KEY_FILE |
Key for opt-in whole-state retention |
LUMAFT_AUDIT_SINK_URL, LUMAFT_AUDIT_SINK_ID, LUMAFT_AUDIT_SINK_SECRET_FILE |
Enterprise external audit sink |
Federated sign-in variables (LUMAFT_OIDC_<KEY>_*, LUMAFT_SAML_<KEY>_*,
LUMAFT_TRUSTED_PROXY_<KEY>_*) are documented in
Users and permissions.
The reference docker run, annotated
Every environment guide is a variation of this command. Read it once here; the guides then only show what changes.
mkdir -p run && chmod 700 run
printf '%s\n' "$(openssl rand -base64 24)" > run/admin-password # 12–4096 characters
cp backends.json run/backends.json # see Connect a backend
chmod 600 run/*
sudo chown -R 1000:1000 run # Linux hosts only
docker volume create lumaft-data
docker run --name lumaft \
--detach \
--init \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--publish 127.0.0.1:8080:8080 \
--mount type=volume,source=lumaft-data,target=/data \
--mount type=bind,source="$PWD/run",target=/run/lumaft,readonly \
--env LUMAFT_LOCAL_ADMIN_PASSWORD_FILE=/run/lumaft/admin-password \
--env LUMAFT_BACKENDS_FILE=/run/lumaft/backends.json \
--env AWS_ACCESS_KEY_ID --env AWS_SECRET_ACCESS_KEY --env AWS_SESSION_TOKEN \
--env LUMAFT_ALLOW_HTTP=true \
"$LUMAFT_IMAGE"
curl -fsS http://127.0.0.1:8080/api/v1/readiness
| Line | What it does | If you drop it |
|---|---|---|
--init |
Runs a minimal init process so signals reach Lumaft and it can shut down cleanly | docker stop may wait for the timeout and leave WAL sidecars behind |
--read-only + --tmpfs /tmp |
Makes the image's filesystem immutable; only /data and /tmp are writable |
The container still runs, but with a larger writable surface than it needs |
--publish 127.0.0.1:8080:8080 |
Exposes port 8080 on the loopback interface only | Publishing on 0.0.0.0 exposes plain HTTP to the network |
--mount type=volume … /data |
A Docker-managed volume for the database, independent of the container | The database is lost when the container is removed |
--mount type=bind … /run/lumaft |
The directory holding the password and backends files, read-only | Startup refuses: neither file can be found |
--env AWS_* |
Passes your shell's AWS credentials through to the container (the name alone forwards the value) | Readiness stays green, but the backend diagnostic fails and no stacks appear |
--env LUMAFT_ALLOW_HTTP=true |
Lets a browser on this machine keep the session cookie over http://127.0.0.1 |
Sign-in appears to succeed and immediately returns you to the sign-in screen |
LUMAFT_ALLOW_HTTP=true is for this loopback-only shape. Every network-reachable deployment
terminates TLS in front of the container and leaves it unset. Never mount the Docker socket into
the container and never run it privileged.
After the container starts
- Confirm
/api/v1/readinessreturns200. - Sign in as the bootstrap administrator and change nothing else yet.
- Unset
LUMAFT_LOCAL_ADMIN_PASSWORD_FILEand securely delete the password file. - Open Administration → Backends and confirm each backend's diagnostic result.
- Take your first cold backup once the first observation cycle has committed.
Then follow Your first hour to work through the first-run readiness checklist and find your way around the console.
Production checklist
- Image pulled by verified digest, never by tag
-
/dataon an exclusive block volume, or PostgreSQL configured - Password file and backends file mounted as direct regular files, mode
0600, owned by UID 1000 - Read-only backend credentials with tested least privilege
- TLS terminated in front of the container;
LUMAFT_ALLOW_HTTPunset - Readiness wired into the load balancer; liveness wired into the restart policy
- Cold backup procedure tested, including a restore
- Storage-health alerts on
writeProbe.outcome,dataDirectory.freeBytes, andlastDurableWrite