Azure Container Apps

This guide shows how to deploy xlwings Lite to Azure Container Apps.

Deploying the Container

  1. Go to Container Apps in the Azure portal.

  2. + Create > + Container App

    • Select the desired Resource group (you may need to click on Create new resource group first)

    • Container app name: e.g. xlwings-lite

    • Deployment source: Container image

    • Select the desired Region

    • Select the desired Container Apps environment (you may need to click on Create new environment first)

  3. Click on Next: Container

    • Image source: Docker Hub or other registries

    • Image type: Public

    • Registry login server: docker.io

    • Image and tag: xlwings/xlwings-lite:1.0.0.0-77

    • CPU and memory: 0.5 CPU cores, 1 Gi memory

    • Environment variables: XLWINGS_LICENSE_KEY: your-license-key (if you don’t have one, get a xlwings trial license key)

  4. Click on Next: Ingress

    • Activate the checkbox for Ingress

      • Under Ingress traffic, select Accepting traffic from anywhere

      • Set Target port to 8000

    • Click on Next: Tags

    • Click on Next: Review + create

    • Click on Create

  5. You can optionally restrict access under Networking > IP Restrictions.

  6. You’ll find the Application Url under the Overview menu item of your container app. Under Application > Containers activate the Environment variables tab, then add the Application Url without the leading https://, it should look something like this: XLWINGS_HOSTNAME: <name>.<random>.<location>.azurecontainerapps.io

  7. Under Application > Scale (or Scale and replicas), set both Min replicas and Max replicas to 1 to prevent cold starts. If desired, you could set a Scale rule to set Min replicas to 0 outside business hours.

  8. Go to your Application Url. You should see the version of xlwings Lite.

Hosting Pyodide separately (for production)

The xlwings Lite container ships with only the default version of the Pyodide distribution. When you upgrade the container, that version may change — and any workbook pinned to a previous Pyodide version will need to upgrade to the new Pyodide version.

In order to prevent this, you have to host the Pyodide distribution on Azure Blob Storage and point xlwings Lite at it. That way you can keep multiple Pyodide versions available indefinitely, and container upgrades become safe.

1. Create the storage account

A ready-made Bicep template creates a Storage account and a blob container (Azure’s equivalent of a storage bucket) with public read access and CORS configured for Pyodide.

  1. Download pyodide-hosting.bicep.

  2. Open Azure Cloud Shell (or the shell icon at the top of the portal), choosing the Bash environment, and upload the file (Manage files > Upload).

  3. Set the variables used by the commands below. Use the resource group of your container app, and pick a globally-unique storage account name (3–24 lowercase letters and numbers):

    export RESOURCE_GROUP_NAME="xlwings-lite-rg"
    export STORAGE_ACCOUNT="<storage-account-name>"
    export BLOB_CONTAINER="pyodide"
    

    Note

    If you don’t know your resource group name, list them with az group list --query "[].name" -o tsv.

  4. Deploy the template:

    az deployment group create \
      --resource-group $RESOURCE_GROUP_NAME \
      --template-file pyodide-hosting.bicep \
      --parameters storageAccountName=$STORAGE_ACCOUNT
    

    Note

    To limit read access to your corporate network, pass a list of allowed IP ranges. Only do this if every user’s browser reaches Azure from a known IP range (corporate VPN / office NAT):

    az deployment group create \
      --resource-group $RESOURCE_GROUP_NAME \
      --template-file pyodide-hosting.bicep \
      --parameters storageAccountName=$STORAGE_ACCOUNT \
                   allowedIpRanges="['203.0.113.0/24','198.51.100.0/22']"
    
  5. Next, run the following command to print the pyodideBaseUrl (we will use it below under Step 3):

    az deployment group show \
      --resource-group $RESOURCE_GROUP_NAME \
      --name pyodide-hosting \
      --query properties.outputs.pyodideBaseUrl.value -o tsv
    

2. Upload a Pyodide release

In the same Cloud Shell (using the variables set above), paste the following block. This downloads a Pyodide release and uploads it to a versioned path in the blob container:

VERSION=0.27.5

curl -fL https://github.com/pyodide/pyodide/releases/download/${VERSION}/pyodide-${VERSION}.tar.bz2 | tar -xj
az storage blob upload-batch \
  --account-name $STORAGE_ACCOUNT \
  --destination $BLOB_CONTAINER \
  --destination-path v${VERSION}/full \
  --source pyodide \
  --auth-mode login \
  --overwrite
rm -rf pyodide/

This might take a few minutes.

To keep additional Pyodide versions available, repeat this step with VERSION set to the desired version. Currently supported versions are:

  • 0.27.5

3. Point xlwings Lite at the storage URL

In your container app, add an environment variable (Application > Containers > Environment variables):

  • Key: XLWINGS_PYODIDE_BASE_URL

  • Value: the pyodideBaseUrl from step 1 (e.g. https://<account>.blob.core.windows.net/pyodide/)

Important

The value must end with a / (needed for the Content-Security-Policy). Without it, the browser blocks pyodide.mjs with a CSP error.

After the container app redeploys, xlwings Lite will load Pyodide from Blob Storage instead of the bundled copy.

Connecting to Azure Artifacts (for production)

By default, xlwings Lite installs packages from the public PyPI. If your organization keeps its Python packages in an Azure Artifacts feed, you can point xlwings Lite at it instead by setting XLWINGS_PYPI_INDEX_URL. The container reverse-proxies the feed on the same origin (under /pypi/), so the browser never sees the feed’s credentials and there are no CORS or CSP exceptions to manage.

There are two ways to authenticate the container to the feed:

  • Managed identity (recommended): no secret is stored anywhere. The container fetches a short-lived Microsoft Entra token from the Azure platform at runtime. This is the cleaner option and the one documented in full below.

  • Personal Access Token (PAT): a token embedded in the index URL. Simpler to set up, but the token expires and must be rotated. See Alternative: Personal Access Token (PAT).

Note

This section assumes you already have a working deployment from the steps above. The feed URL is the same one you would pass to pip install --index-url, e.g. https://pkgs.dev.azure.com/<org>/<project>/_packaging/<feed>/pypi/simple/ (organization-scoped feeds omit the /<project> segment).

Managed identity

1. Enable a system-assigned managed identity

  1. Go to your container app, then Settings > Security > Identity.

    Note

    In older portal layouts the Identity blade lives directly under Settings rather than under Security. If you can’t find it, use the search box at the top of the left-hand menu and type Identity.

  2. On the System assigned tab, switch Status to On and click Save.

  3. Note the Object (principal) ID that appears — you’ll need it in the next step.

2. Grant the identity access to the feed

The managed identity is a Microsoft Entra service principal. Before you can grant it feed permissions, it usually has to be added to the Azure DevOps organization first.

  1. In Azure DevOps, go to Organization settings > Users > Add users.

  2. Set the user type to Service Principal, search by the Object (principal) ID (or the identity’s Application/Client ID), and assign an access level (e.g. Basic or Stakeholder). Click Add.

    Note

    This step is easy to miss. A brand-new managed identity often does not show up by name in the feed’s permissions dialog until it has been added to the organization here.

  3. Now open your feed > Feed settings (gear icon) > Permissions > Add users/groups, find the identity (search by name or by the Object ID), and assign the role Collaborator (shown as Feed and Upstream Reader (Collaborator)).

    Warning

    Use Collaborator, not Reader. Reader can only serve packages that are already cached in the feed. Installing a package that isn’t cached yet triggers an ingestion from the feed’s upstream source (e.g. public PyPI), which requires the Collaborator role or higher. With only Reader, uncached packages fail with a misleading Cannot find the package ... in feed (PackageNotFound) error.

3. Configure the environment variables

Go to Application > Containers > Environment variables (Edit and deploy) and add:

  • XLWINGS_PYPI_INDEX_URL: your feed URL, e.g. https://pkgs.dev.azure.com/<org>/_packaging/<feed>/pypi/simple/ (no token)

  • XLWINGS_PYPI_AUTH_MODE: azure-managed-identity

Saving creates a new revision and restarts the container.

Note

Enable the managed identity (step 1) before setting these variables. The platform only injects the identity token endpoint into the container once an identity is assigned, and xlwings Lite reads it at startup. If you set the variables first, restart the revision after enabling the identity.

4. Verify

Open https://<your-app-url>/pypi/simple/<some-package>/ in a browser (or with curl). A 200 response listing the package’s files (as an HTML page of links, or as JSON if you request it with Accept: application/vnd.pypi.simple.v1+json) means the feed connection and authentication are working. You can then add the package to your requirements.txt in the add-in.

If you get a 401/403, check the identity’s feed permissions (step 2). If a never-installed package returns a 404 with PackageNotFound, the identity likely has Reader instead of Collaborator. The container logs (Monitoring > Log stream, or az containerapp logs show) report token-fetch failures explicitly.

Alternative: Personal Access Token (PAT)

If you can’t use a managed identity, embed a PAT in the feed URL instead. Create a PAT in Azure DevOps (User settings > Personal access tokens) with the Packaging: Read scope, then set a single environment variable:

  • XLWINGS_PYPI_INDEX_URL: https://<PAT>@pkgs.dev.azure.com/<org>/_packaging/<feed>/pypi/simple/

No XLWINGS_PYPI_AUTH_MODE is needed (it defaults to PAT/basic authentication). The proxy forwards the token to the feed and strips it from everything the browser sees.

Note

As with managed identity, installing an uncached package triggers upstream ingestion, so the PAT (or the account it belongs to) needs permission to save packages from upstream sources. Prefer a token from a dedicated service account over a personal one, and rotate it before it expires.

Register the add-in with Microsoft 365 admin center

  1. In your browser, go to https://<your-hostname>/manifest, which will download xlwings-lite-manifest.xml.

  2. Go to Microsoft 365 admin center

    • Click on Show all > Settings > Integrated Apps.

    • If you have xlwings Lite installed, uninstall it first.

    • Click on Upload custom apps and select Office Add-in (App type).

    • Select Upload manifest file (.xml) from device. Click Choose File, then select the xlwings-lite-manifest.xml from the previous step.

    • Click Next, then assign the desired users.

    • Click Next and accept permission requests.

    • Click Next and Finish deployment.

The users will get the add-in to show up automatically although it may take a few hours.

Note

If you want to remove the add-in again and run into issues (“Remove apps failed. No apps were successfully removed. Please try to remove them later.”), use this legacy URL: https://admin.microsoft.com/#/Settings/AddIns

Updating

To update xlwings Lite, point your container app at the new image tag.

  1. Go to your container app, then Application > Containers.

  2. Click Edit and deploy, select the container, and update the Image and tag to:

    xlwings/xlwings-lite:1.0.0.0-77
    
  3. Click Save to create a new revision and deploy it.

Note

Normally you don’t need to update the manifest after deploying a new version of the container. This would only be required, e.g., if the URL of your container changed. In that case, the Microsoft 365 admin center offers a link to Update Add-in.