Production Checklist¶
Before deploying to production, there’s a few things that you should check for security, performance, and branding reasons.
Taskpane¶
Replace the example task pane with your own. If you don’t have any meaningful content, just leave it empty by providing the following:
{% extends "base.html" %}
{% block content %}
<div class="container-fluid pt-3 ps-3">
<h1>Name of your Add-in</h1>
</div>
{% endblock content %}
Store this under app/templates/taskpane.html
and update the name
argument under app/routers/taskpane.py
to "taskpane.html"
.
Settings¶
Make sure that the environment is set to
"prod"
. This disables hotreload and will prevent unhandled exceptions to be shown in Excel.xlwings.XlwingsError
continue to be shown:XLWINGS_ENVIRONMENT="prod"
Manifest: replace all xlwings references & icons with your own name & icons. Currently, you’ll need to do this both in
app/templates/manifest.xml
as well as in a few settings:XLWINGS_FUNCTIONS_NAMESPACE="YOUR_NAME" XLWINGS_PROJECT_NAME="Your Name"
Disable all libraries that you don’t use. Candidates are:
XLWINGS_ENABLE_ALPINEJS_CSP=false XLWINGS_ENABLE_HTMX=false XLWINGS_ENABLE_SOCKETIO=false XLWINGS_ENABLE_BOOTSTRAP=false
Disable the examples:
XLWINGS_ENABLE_EXAMPLES=false
If you want to publish the add-in to the public Excel add-in store (“App Source”), you need to set this to
true
. This will load the Office.js JS library from Microsoft’s CDN as required by Microsoft:XLWINGS_CDN_OFFICEJS=true
Make sure that the log level is not on
"DEBUG"
as this can log sensitive tokens:XLWINGS_LOG_LEVEL="INFO"
Unless you have good reasons not to, enable the HTTP security response headers via:
XLWINGS_ADD_SECURITY_HEADERS=true
Enable authentication if appropriate, e.g., when using Microsoft Entra ID:
XLWINGS_AUTH_PROVIDERS=["entraid"]
Configure CORS properly. If you don’t use the Office Scripts integration and don’t use custom functions in Excel on the web, disable CORS:
XLWINGS_CORS_ALLOW_ORIGINS=""
If you use custom functions with Excel on the web, configure CORS as follows:
XLWINGS_CORS_ALLOW_ORIGINS=["your.domain.com"]
If you use Office Scripts as your integration, you currently need to allow all origins:
XLWINGS_CORS_ALLOW_ORIGINS=["*"]
If you don’t use Excel on the web, set this to
false
to get more restrictive HTTP security response headers:XLWINGS_ENABLE_EXCEL_ONLINE=false
License key¶
When trying out xlwings, you should set your trial key as XLWINGS_LICENSE_KEY
. Once you enroll in a paid plan, you can either use your developer license key directly or create a deploy key first.
Developer key: this is the key that you will be provided with after purchase. Developer keys are valid for one or more developers (depending on your plan) and expire after 1 year, which means you’ll have to update the license key each year.
Deploy key: A deploy key doesn’t expire but is bound to a specific version of xlwings, which means that you need to generate a new deploy key every time you update xlwings. Note that you can’t generate deploy keys with a trial license.
If you have your developer license key set as XLWINGS_DEVELOPER_KEY
env var in your build environment, it will install the deploy key directly in the Docker image when building the docker file with the following --build-arg
:
docker build --build-arg XLWINGS_DEVELOPER_KEY=${XLWINGS_DEVELOPER_KEY} .
If you want to create a deploy key manually, you fist need to activate your developer license like this:
xlwings license update -k YOUR_LICENSE_KEY
Then you can generate deploy keys (make sure that the xlwings version is the same as the one used in production):
xlwings license deploy
Note
xlwings licenses keys are verified offline (i.e., no telemetry/license server involved).
Workers¶
Note
If you are using serverless functions such as Azure functions or AWS Lambda, you don’t need to care about the number of workers, as these platforms handle this dynamically based on the number of incoming requests.
With XLWINGS_ENVIRONMENT="dev"
, you’re running a single worker, i.e., process. To take advantage of multiple CPU cores and to be able to serve more traffic, you want to run multiple workers in production. If you have long running functions that are CPU-bound, multiple workers will also mean that the app won’t get blocked for everyone while such a long running function is processing.
Each worker runs an own instance of the xlwings Server app, and so with each additional worker, your memory requirements will increase. The exact number of workers depends on the amount of your traffic and the nature of your functions. Gunicorn, which is the HTTP server recommended for production, suggests a maximum of 2-4 workers per CPU-core. A pragmatic way of finding the right amount of workers is to start with a low number, say 2-4, then increase the number of workers up to a maximum of 4 workers per core if your users encounter performance issues.
You can have a look at docker-compose.prod.yaml
to see the gunicorn command with the workers
argument:
gunicorn app.main:main_app
--bind 0.0.0.0:8000
--access-logfile -
--workers 2
--timeout 30
--worker-class uvicorn.workers.UvicornWorker
Container-based platforms such as Kubernetes or Render.com allow you to scale the number of containers instead of the number of workers inside the container. Both options should work equally well, but for low traffic applications, scaling the number of containers on platforms such as Render.com will be much more expensive.
Redis¶
If you are using one of the following features, you need to use a Redis database or a compatible service such as Valkey.
Streaming functions: Redis connects the app workers with the Socket.io service via its pub-sub functionality. To use Redis, provide the following setting (for more info, see
.env
file):XLWINGS_SOCKETIO_MESSAGE_QUEUE_URL=...
Object handles: Redis acts as an object cache that is shared across all app workers. To use Redis, provide the following setting (for more info, see
.env
file):XLWINGS_OBJECT_CACHE_URL=...
You can install Redis, make it part of your Docker compose stack, or use a hosted service. For reference, see docker-compose.prod.yaml
.
Socket.io¶
In production, Socket.io is only required for Streaming functions and the experimental utils.trigger_script()
. You may also decide to use Socket.io for realtime functionality in your task pane.
Since Socket.io is a stateful protocol, you must run it with exactly 1 worker. This means that you have to run it as a separate process, which connects to the app workers via Redis. Even if you are running it with only 1 worker, it can scale to thousands of concurrent connections.
However, to avoid blocking Python’s event loop, the Socket.io server shouldn’t manage any long-running tasks, such as slow, CPU-bound functions. Often, this isn’t an issue as streaming functions are primarily used to stream data from external services (e.g., market data). As long as you can query these external services via an async HTTP request or similar (e.g., using httpx
or aiohttp
), you’re good!
Note
Even if you don’t use Socket.io in production, you should leave XLWINGS_ENABLE_SOCKETIO=true
for your development environment as there, it’s responsible for hot-reloading the Office.js frontend code with every code change.
Timeout¶
Under normal circumstances, HTTP requests time out if they do not receive a response within a certain time frame. When deploying xlwings Server, you usually have to deal with a timeout on two levels:
gunicorn: gunicorn serves the Python app and has a default timeout of 30 seconds. If you want to increase it to e.g., 60 seconds, provide the option
--timeout 60
in the gunicorn command, see e.g., deployment/docker-compose.prod.yaml.Reverse proxy/load balancer: in front of gunicorn, you usually have a reverse proxy, such as nginx. For Kubernetes or fully managed solutions like Azure functions, you usually deal with a load balancer. They all have their own timeouts, so you might need to adjust it for it to be at least as long as the gunicorn timeout. For example, for nginx, the default timeout is 60 seconds and can be adjusted using the
proxy_read_timeout 60s;
directive.
HTTP caching¶
Often, HTTP servers such as nginx or Cloudflare will add caching headers to the static files served. This means that when you deploy a new version of your server, users may still use the previous version of that file. To prevent this:
Run python scripts/build_static_files.py
as part of your deployment process. This will add content hashes to the file names and will therefore bust the caching when the content changes. Note that this is already done in the Dockerfile
.
This, however, will have no effect on files requested by the manifest.xml
, which is especially critical for xlwings Lite, where every file is a static file. For xlwings Lite deployments, make sure that the following files don’t allow caching by adding the the Cache-Control: public, max-age=0, must-revalidate
header:
- .../xlwings/custom-functions-code.js
- .../xlwings/custom-functions-meta.json
- .../xlwings/taskpane.html
When you host your xlwings Lite app on Cloudflare Pages, you can achieve this by going to Your Domain
> Caching
> Configuration
> Browser Cache TTL
setting to Respect Existing Headers
. You also have to include a file called _headers
in the root of your deployed directory with the following content:
/xlwings/custom-functions-code.js
Cache-Control: public, max-age=0, must-revalidate
/xlwings/custom-functions-meta.json
Cache-Control: public, max-age=0, must-revalidate
Note
The image URLs in the manifest.xml
(for the icons) must not prevent caching, as on Windows, caching is required to properly display the icons in the ribbon. This means that when you need to change the icons, you will need to release a new version of manifest.xml
with changed URLs to your icons.