# 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](https://portal.azure.com/#browse/Microsoft.App%2FcontainerApps).
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.104`
   - 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](https://www.xlwings.org/trial))
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. 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.
7. Go to your **Application Url**. You should see the version of xlwings Lite.

## Connecting to Azure Artifacts (optional)

By default, xlwings Lite installs packages from the public PyPI. If your organization keeps its Python packages in an [Azure Artifacts](https://learn.microsoft.com/en-us/azure/devops/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](https://admin.microsoft.com/)
   - Click on [`Show all` > `Settings` > `Integrated Apps`](https://admin.microsoft.com/#/Settings/IntegratedApps).
   - 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](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:
   ```text
   xlwings/xlwings-lite:1.0.0.104
   ```
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**.
