Set up on Azure
Lumaft runs anywhere a Linux container runs. On Azure the decision that matters is storage:
| Path | Database | Edition | When to choose it |
|---|---|---|---|
| Azure Container Apps | Azure Database for PostgreSQL | Business | You want serverless containers and can run PostgreSQL |
| Azure Virtual Machine | SQLite on a managed disk | Community | You want the simplest shape with no database server |
Container Apps persistent storage is Azure Files, an SMB share. SQLite's WAL mode is not supported on network filesystems, so Container Apps deployments must use PostgreSQL.
Lumaft has no Azure Blob Storage adapter. An Azure-hosted Lumaft observes Pulumi state in Amazon S3 or in an S3-compatible object store you operate. See Backend credentials from Azure.
Path A: Azure Container Apps
Architecture
Internet ──443──▶ Container Apps ingress (managed TLS)
│ :8080
┌────────┴──────────────┐ VNet-integrated environment
│ lumaft (1 replica) │──── NAT Gateway ──▶ S3 / registry / licensing
│ init: secret files │
└────────┬──────────────┘
│ private endpoint
┌────────┴──────────────┐
│ Azure Database for │
│ PostgreSQL Flexible │
└───────────────────────┘
Before you begin
- A resource group and a virtual network with two delegated subnets: one for the Container Apps
environment (
/23or larger) and one for PostgreSQL private access. - A NAT Gateway attached to the Container Apps subnet, so outbound traffic has a stable public IP you can allow in an S3 bucket policy.
- An Azure Key Vault holding three secrets:
lumaft-admin-password,lumaft-backends-json, andlumaft-postgres-url. - A user-assigned managed identity with the Key Vault Secrets User role on that vault, and AcrPull if you mirror the image into Azure Container Registry.
- The verified image digest. See Deployment and setup.
The image is public; pulling from
ghcr.ioneeds no credential. - Your backend details. See Find your backend details.
Step 1: PostgreSQL
Create an Azure Database for PostgreSQL Flexible Server with private access in the PostgreSQL subnet. Create a dedicated database and a role that can manage Lumaft's schema:
CREATE ROLE lumaft LOGIN PASSWORD '<password>';
CREATE DATABASE lumaft OWNER lumaft;
Store the connection URL in Key Vault as lumaft-postgres-url, for example
postgresql://lumaft:<password>@lumaft-pg.postgres.database.azure.com:5432/lumaft?sslmode=require.
Connection requirements are in Database integration.
Step 2: Container Apps environment
az containerapp env create \
--name lumaft-env \
--resource-group lumaft-rg \
--location eastus \
--infrastructure-subnet-resource-id "<container-apps-subnet-id>" \
--enable-workload-profiles
Step 3: The container app
The app runs an init container that writes the three secret files into an ephemeral volume with
the ownership Lumaft requires, then the application container mounts that volume at
/run/lumaft.
# lumaft.yaml
properties:
managedEnvironmentId: /subscriptions/.../managedEnvironments/lumaft-env
configuration:
ingress:
external: true
targetPort: 8080
transport: http
allowInsecure: false
secrets:
- name: admin-password
keyVaultUrl: https://lumaft-kv.vault.azure.net/secrets/lumaft-admin-password
identity: /subscriptions/.../userAssignedIdentities/lumaft-identity
- name: backends-json
keyVaultUrl: https://lumaft-kv.vault.azure.net/secrets/lumaft-backends-json
identity: /subscriptions/.../userAssignedIdentities/lumaft-identity
- name: postgres-url
keyVaultUrl: https://lumaft-kv.vault.azure.net/secrets/lumaft-postgres-url
identity: /subscriptions/.../userAssignedIdentities/lumaft-identity
template:
initContainers:
- name: lumaft-init
image: ghcr.io/dekglas/lumaft@sha256:<digest>
command: ['/bin/sh', '-ec']
args:
- >-
umask 077 &&
mkdir -p /run/lumaft/secrets &&
chmod 700 /run/lumaft/secrets &&
printf '%s\n' "$ADMIN_PASSWORD" > /run/lumaft/secrets/admin-password &&
printf '%s\n' "$BACKENDS_JSON" > /run/lumaft/secrets/backends.json &&
printf '%s\n' "$POSTGRES_URL" > /run/lumaft/secrets/postgres-url &&
chmod 600 /run/lumaft/secrets/*
env:
- name: ADMIN_PASSWORD
secretRef: admin-password
- name: BACKENDS_JSON
secretRef: backends-json
- name: POSTGRES_URL
secretRef: postgres-url
volumeMounts:
- volumeName: lumaft-runtime
mountPath: /run/lumaft
containers:
- name: lumaft
image: ghcr.io/dekglas/lumaft@sha256:<digest>
resources:
cpu: 1.0
memory: 2Gi
env:
- name: LUMAFT_LOCAL_ADMIN_PASSWORD_FILE
value: /run/lumaft/secrets/admin-password
- name: LUMAFT_BACKENDS_FILE
value: /run/lumaft/secrets/backends.json
- name: LUMAFT_POSTGRES_URL_FILE
value: /run/lumaft/secrets/postgres-url
- name: LUMAFT_PUBLIC_ORIGIN
value: https://lumaft.example.com
volumeMounts:
- volumeName: lumaft-runtime
mountPath: /run/lumaft
probes:
- type: Startup
httpGet: { path: /api/v1/readiness, port: 8080 }
failureThreshold: 30
periodSeconds: 2
- type: Readiness
httpGet: { path: /api/v1/readiness, port: 8080 }
periodSeconds: 30
- type: Liveness
httpGet: { path: /api/v1/health, port: 8080 }
periodSeconds: 30
scale:
minReplicas: 1
maxReplicas: 1
volumes:
- name: lumaft-runtime
storageType: EmptyDir
az containerapp create \
--name lumaft \
--resource-group lumaft-rg \
--yaml lumaft.yaml \
--user-assigned "<identity-resource-id>"
Keep maxReplicas: 1. Multiple serving replicas require an Enterprise license and the replica
settings in Database integration.
Lumaft validates that each file is a direct regular file, not writable by group or others,
below a directory that is not writable by group or others. The init container runs as the
image's node user (UID 1000), so the files it writes are owned correctly as long as the
ephemeral volume is writable by that user. Confirm it after the first revision starts:
az containerapp exec --name lumaft --resource-group lumaft-rg --command "ls -ln /run/lumaft/secrets"
Every file must show owner 1000 and mode -rw-------. If the init container fails with a
permission error on mkdir, the platform mounted the volume read-only for non-root users; open
a support case with that output, and in the meantime run the VM path below.
Step 4: Custom domain and TLS
az containerapp hostname add --hostname lumaft.example.com --name lumaft --resource-group lumaft-rg
az containerapp hostname bind --hostname lumaft.example.com --name lumaft --resource-group lumaft-rg \
--environment lumaft-env --validation-method CNAME
Container Apps issues and renews a managed certificate. Ingress terminates TLS and forwards HTTP
to port 8080, which is the HTTPS origin the Secure session cookie needs. Leave
LUMAFT_ALLOW_HTTP unset.
Step 5: Verify
curl -fsS https://lumaft.example.com/api/v1/readiness
Sign in, open Administration → Backends, and confirm each backend's diagnostic result. Then
remove the LUMAFT_LOCAL_ADMIN_PASSWORD_FILE environment variable and the admin-password
secret in a new revision.
Path B: Azure Virtual Machine
This path mirrors Set up on AWS EC2 with Azure primitives.
Step 1: Network security group
| Direction | Protocol | Port | Source / destination | Purpose |
|---|---|---|---|---|
| Inbound | TCP | 443 | Your allowed ranges | Browser and runner traffic |
| Inbound | TCP | 80 | Internet (optional) |
ACME HTTP-01 challenges only |
| Outbound | TCP | 443 | Internet |
S3, registry, licensing, ACME |
Do not open 8080 or 22. Use Azure Bastion for shell access.
Step 2: Virtual machine and data disk
- Size:
Standard_B2msor larger (2 vCPU, 8 GiB). - Image: Ubuntu LTS. Install Docker Engine.
- Data disk: a separate Premium SSD managed disk, encrypted at rest, attached as a data disk. A managed disk is presented to the VM as a local block device, which satisfies the SQLite storage contract.
sudo mkfs.ext4 -L lumaft-data /dev/disk/azure/scsi1/lun0
sudo mkdir -p /srv/lumaft/data
echo 'LABEL=lumaft-data /srv/lumaft/data ext4 defaults,nofail 0 2' | sudo tee -a /etc/fstab
sudo mount -a
sudo chown 1000:1000 /srv/lumaft/data
sudo chmod 700 /srv/lumaft/data
Step 3: Secret files and backend credentials
Assign a system-assigned managed identity to the VM and grant it Key Vault Secrets User on
your vault. Install the Azure CLI on the VM
(curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash on Ubuntu), then fetch the secrets with
the identity and write them as protected files:
az login --identity
sudo mkdir -p /srv/lumaft/run && sudo chmod 700 /srv/lumaft/run
az keyvault secret show --vault-name lumaft-kv --name lumaft-admin-password --query value -o tsv \
| sudo sh -c 'umask 077; cat > /srv/lumaft/run/admin-password'
az keyvault secret show --vault-name lumaft-kv --name lumaft-backends-json --query value -o tsv \
| sudo sh -c 'umask 077; cat > /srv/lumaft/run/backends.json'
sudo chown -R 1000:1000 /srv/lumaft/run
sudo chmod 600 /srv/lumaft/run/*
Backend credentials for S3 are covered in the next section.
Step 4: Run Lumaft and terminate TLS
Run the same container command as the EC2 guide, adding an environment file for the S3
credentials, and put Caddy or nginx in front on 443:
sudo docker run --name lumaft \
--detach --restart unless-stopped --init --read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--publish 127.0.0.1:8080:8080 \
--mount type=bind,source=/srv/lumaft/data,target=/data \
--mount type=bind,source=/srv/lumaft/run,target=/run/lumaft,readonly \
--env-file /srv/lumaft/aws-credentials.env \
--env LUMAFT_LOCAL_ADMIN_PASSWORD_FILE=/run/lumaft/admin-password \
--env LUMAFT_BACKENDS_FILE=/run/lumaft/backends.json \
--env LUMAFT_PUBLIC_ORIGIN=https://lumaft.example.com \
"ghcr.io/dekglas/lumaft@sha256:<digest>"
Backups, restore, and upgrades follow the
EC2 operating procedures. Azure disk snapshots
are crash-consistent; a verified backup is a copy of lumaft.db taken while the container is
stopped.
Backend credentials from Azure
Lumaft reads S3 through the AWS SDK credential provider chain. An Azure identity cannot call Amazon S3 directly, so choose one of:
- Static keys, scoped and rotated. Create an IAM user with only the read-only state policy
from Connect a backend. Store
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEYin Key Vault and inject them as environment variables (Container AppssecretRef, or an--env-filewith mode0600on a VM). Rotate on a schedule and revoke the previous key. - Workload identity federation. Register Microsoft Entra ID as an OIDC identity provider in
AWS IAM and let the identity assume a read-only role. The SDK reads
AWS_WEB_IDENTITY_TOKEN_FILEandAWS_ROLE_ARN; you supply a process that keeps the token file fresh. No static key exists. - An S3-compatible store you operate. Point the backend's
endpointat it and grant the equivalent list and read operations. Lumaft never needs a bucket-write permission.
Restrict the bucket policy to the NAT Gateway's public IP in addition to the identity's permissions.
Azure IAM summary
| Principal | Role | Scope |
|---|---|---|
| Lumaft managed identity | Key Vault Secrets User | The Lumaft key vault |
| Lumaft managed identity | AcrPull (if mirroring to ACR) | The container registry |
| Operators | Contributor | The Lumaft resource group |
| Operators | Key Vault Secrets Officer | The Lumaft key vault |
No Azure role grants access to the Lumaft console. Console access is governed by Lumaft's own accounts and roles; see Users and permissions.