Set up on Azure
Marolum 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.
Marolum has no Azure Blob Storage adapter. An Azure-hosted Marolum 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
│ marolum (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:
marolum-admin-password,marolum-backends-json, andmarolum-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 Marolum's schema:
CREATE ROLE marolum LOGIN PASSWORD '<password>';
CREATE DATABASE marolum OWNER marolum;
Store the connection URL in Key Vault as marolum-postgres-url, for example
postgresql://marolum:<password>@marolum-pg.postgres.database.azure.com:5432/marolum?sslmode=require.
Connection requirements are in Database integration.
Step 2: Container Apps environment
az containerapp env create \
--name marolum-env \
--resource-group marolum-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 Marolum requires, then the application container mounts that volume at
/run/marolum.
# marolum.yaml
properties:
managedEnvironmentId: /subscriptions/.../managedEnvironments/marolum-env
configuration:
ingress:
external: true
targetPort: 8080
transport: http
allowInsecure: false
secrets:
- name: admin-password
keyVaultUrl: https://marolum-kv.vault.azure.net/secrets/marolum-admin-password
identity: /subscriptions/.../userAssignedIdentities/marolum-identity
- name: backends-json
keyVaultUrl: https://marolum-kv.vault.azure.net/secrets/marolum-backends-json
identity: /subscriptions/.../userAssignedIdentities/marolum-identity
- name: postgres-url
keyVaultUrl: https://marolum-kv.vault.azure.net/secrets/marolum-postgres-url
identity: /subscriptions/.../userAssignedIdentities/marolum-identity
template:
initContainers:
- name: marolum-init
image: ghcr.io/dekglas/marolum@sha256:<digest>
command: ['/bin/sh', '-ec']
args:
- >-
umask 077 &&
mkdir -p /run/marolum/secrets &&
chmod 700 /run/marolum/secrets &&
printf '%s\n' "$ADMIN_PASSWORD" > /run/marolum/secrets/admin-password &&
printf '%s\n' "$BACKENDS_JSON" > /run/marolum/secrets/backends.json &&
printf '%s\n' "$POSTGRES_URL" > /run/marolum/secrets/postgres-url &&
chmod 600 /run/marolum/secrets/*
env:
- name: ADMIN_PASSWORD
secretRef: admin-password
- name: BACKENDS_JSON
secretRef: backends-json
- name: POSTGRES_URL
secretRef: postgres-url
volumeMounts:
- volumeName: marolum-runtime
mountPath: /run/marolum
containers:
- name: marolum
image: ghcr.io/dekglas/marolum@sha256:<digest>
resources:
cpu: 1.0
memory: 2Gi
env:
- name: MAROLUM_LOCAL_ADMIN_PASSWORD_FILE
value: /run/marolum/secrets/admin-password
- name: MAROLUM_BACKENDS_FILE
value: /run/marolum/secrets/backends.json
- name: MAROLUM_POSTGRES_URL_FILE
value: /run/marolum/secrets/postgres-url
- name: MAROLUM_PUBLIC_ORIGIN
value: https://marolum.example.com
volumeMounts:
- volumeName: marolum-runtime
mountPath: /run/marolum
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: marolum-runtime
storageType: EmptyDir
az containerapp create \
--name marolum \
--resource-group marolum-rg \
--yaml marolum.yaml \
--user-assigned "<identity-resource-id>"
Keep maxReplicas: 1. Multiple serving replicas require an Enterprise license and the replica
settings in Database integration.
Marolum 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 marolum --resource-group marolum-rg --command "ls -ln /run/marolum/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 marolum.example.com --name marolum --resource-group marolum-rg
az containerapp hostname bind --hostname marolum.example.com --name marolum --resource-group marolum-rg \
--environment marolum-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
MAROLUM_ALLOW_HTTP unset.
Step 5: Verify
curl -fsS https://marolum.example.com/api/v1/readiness
Sign in, open Administration → Backends, and confirm each backend's diagnostic result. Then
remove the MAROLUM_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 marolum-data /dev/disk/azure/scsi1/lun0
sudo mkdir -p /srv/marolum/data
echo 'LABEL=marolum-data /srv/marolum/data ext4 defaults,nofail 0 2' | sudo tee -a /etc/fstab
sudo mount -a
sudo chown 1000:1000 /srv/marolum/data
sudo chmod 700 /srv/marolum/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/marolum/run && sudo chmod 700 /srv/marolum/run
az keyvault secret show --vault-name marolum-kv --name marolum-admin-password --query value -o tsv \
| sudo sh -c 'umask 077; cat > /srv/marolum/run/admin-password'
az keyvault secret show --vault-name marolum-kv --name marolum-backends-json --query value -o tsv \
| sudo sh -c 'umask 077; cat > /srv/marolum/run/backends.json'
sudo chown -R 1000:1000 /srv/marolum/run
sudo chmod 600 /srv/marolum/run/*
Backend credentials for S3 are covered in the next section.
Step 4: Run Marolum 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 marolum \
--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/marolum/data,target=/data \
--mount type=bind,source=/srv/marolum/run,target=/run/marolum,readonly \
--env-file /srv/marolum/aws-credentials.env \
--env MAROLUM_LOCAL_ADMIN_PASSWORD_FILE=/run/marolum/admin-password \
--env MAROLUM_BACKENDS_FILE=/run/marolum/backends.json \
--env MAROLUM_PUBLIC_ORIGIN=https://marolum.example.com \
"ghcr.io/dekglas/marolum@sha256:<digest>"
Backups, restore, and upgrades follow the
EC2 operating procedures. Azure disk snapshots
are crash-consistent; a verified backup is a copy of marolum.db taken while the container is
stopped.
Backend credentials from Azure
Marolum 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. Marolum 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 |
|---|---|---|
| Marolum managed identity | Key Vault Secrets User | The Marolum key vault |
| Marolum managed identity | AcrPull (if mirroring to ACR) | The container registry |
| Operators | Contributor | The Marolum resource group |
| Operators | Key Vault Secrets Officer | The Marolum key vault |
No Azure role grants access to the Marolum console. Console access is governed by Marolum's own accounts and roles; see Users and permissions.