Skip to content
Lumaft documentation contents
Lumaft documentation

Set up on Azure

Deploy Lumaft to Azure Container Apps with PostgreSQL, or to an Azure VM with SQLite.

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 (/23 or 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, and lumaft-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.io needs 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.

ScreenshotAzure portal Container App overview showing the application URL, one running revision at 1 replica, ingress enabled on target port 8080, and the bound custom domain

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_B2ms or 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_ID and AWS_SECRET_ACCESS_KEY in Key Vault and inject them as environment variables (Container Apps secretRef, or an --env-file with mode 0600 on 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_FILE and AWS_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 endpoint at 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.