Configure a Linux Python app for Azure App Service

This article describes how Azure App Service runs Python apps, how you can migrate existing apps to Azure, and how you can customize the behavior of App Service when you need to. Python apps must be deployed with all the required pip modules.

The App Service deployment engine automatically activates a virtual environment and runs pip install -r requirements.txt when you deploy a Git repository or when you deploy a zip package with build automation enabled.

Note

App Service currently requires requirements.txt in your project's root directory even if you have a pyproject.toml. See Generate requirements.txt from pyproject.toml for recommended approaches.

This article provides key concepts and instructions for Python developers who use a built-in Linux container in App Service. If you've never used App Service, first complete the Python quickstart and Flask, Django, or FastAPI with PostgreSQL tutorial.

You can use either the Azure portal or the Azure CLI for configuration:

Note

Linux is the only operating system option for running Python apps in App Service. Python on Windows is no longer supported. You can, however, build your own custom Windows container image and run that in App Service. For more information, see Use a custom Docker image.

Configure the Python version

  • Azure portal: Use the General settings tab on the Configuration page, as described in Configure general settings for Linux containers.

  • Azure CLI:

    • Show the current Python version by using az webapp config show:

      az webapp config show --resource-group <resource-group-name> --name <app-name> --query linuxFxVersion
      

      Replace <resource-group-name> and <app-name> with the names that are appropriate for your web app.

    • Set the Python version by using az webapp config set:

      az webapp config set --resource-group <resource-group-name> --name <app-name> --linux-fx-version "PYTHON|3.11"
      
    • Show all Python versions that are supported in App Service by using az webapp list-runtimes:

      az webapp list-runtimes --os linux | grep PYTHON
      

What happens to outdated runtimes in App Service?

Outdated runtimes are deprecated by the maintaining organization or have significant vulnerabilities. Accordingly, they're removed from the create and configure pages in the portal. When an outdated runtime is hidden from the portal, any app that's still using that runtime continues to run.

If you want to create an app with an outdated runtime version that's no longer shown on the portal, use the Azure CLI, an ARM template, or Bicep. These deployment alternatives let you create deprecated runtimes that are removed from the portal but are still being supported.

If a runtime is fully removed from the App Service platform, your Azure subscription owner receives an email notice before the removal.

Customize build automation

Note

When Python applications are deployed with build automation, content is deployed to and served from /tmp/<uid>, not under /home/site/wwwroot. You can access this content directory by using the APP_PATH environment variable. You should write any additional files created at runtime to a location under /home or by using Bring Your Own Storage for persistence. For more information on this behavior, see Python Build Changes.

The App Service build system, called Oryx, performs the following steps when you deploy your app, if the app setting SCM_DO_BUILD_DURING_DEPLOYMENT is set to 1:

  1. Run a custom pre-build script, if that step is specified by the PRE_BUILD_COMMAND setting. (The script can itself run other Python and Node.js scripts, pip and npm commands, and Node-based tools like Yarn, for example, yarn install and yarn build.)

  2. Run pip install -r requirements.txt. The requirements.txt file must be in the project's root folder. If it's not, the build process reports the error "Could not find setup.py or requirements.txt; Not running pip install."

  3. If manage.py is found in the root of the repository (which indicates a Django app), run manage.py collectstatic. However, if the DISABLE_COLLECTSTATIC setting is true, this step is skipped.

  4. Run a custom post-build script, if that step is specified in the POST_BUILD_COMMAND setting. (Again, the script can run other Python and Node.js scripts, pip and npm commands, and Node-based tools.)

By default, the PRE_BUILD_COMMAND, POST_BUILD_COMMAND, and DISABLE_COLLECTSTATIC settings are empty.

  • To disable running collectstatic when building Django apps, set the DISABLE_COLLECTSTATIC setting to true.

  • To run pre-build commands, set the PRE_BUILD_COMMAND setting to contain either a command, such as echo Pre-build command, or a path to a script file, relative to your project root, such as scripts/prebuild.sh. All commands must use paths that are relative the project root folder.

  • To run post-build commands, set the POST_BUILD_COMMAND setting to contain either a command, such as echo Post-build command, or a path to a script file, relative to your project root, such as scripts/postbuild.sh. All commands must use paths that are relative to the project root folder.

For information about other settings that customize build automation, see Oryx configuration.

For information about accessing the build and deployment logs, see Access deployment logs.

For more information on how App Service runs and builds Python apps in Linux, see How Oryx detects and builds Python apps.

Note

The PRE_BUILD_SCRIPT_PATH and POST_BUILD_SCRIPT_PATH settings are identical to PRE_BUILD_COMMAND and POST_BUILD_COMMAND and are supported for legacy purposes.

A setting named SCM_DO_BUILD_DURING_DEPLOYMENT, if it contains true or 1, triggers an Oryx build that happens during deployment. The setting is true when you deploy by using Git, the Azure CLI command az webapp up, and Visual Studio Code.

Note

Always use relative paths in all pre-build and post-build scripts because the build container in which Oryx runs is different from the runtime container in which the app runs. Never rely on the exact placement of your app project folder within the container (for example, that it's placed under site/wwwroot).

Generate requirements.txt from pyproject.toml

Currently, App Service doesn't directly support pyproject.toml. If you use tools like Poetry or uv, the recommended approach is to generate a compatible requirements.txt file before deployment in your project's root:

Using Poetry

Generate requirements.txt by using Poetry with the export plugin:


poetry export -f requirements.txt --output requirements.txt --without-hashes

Using uv

Generate requirements.txt by using uv:


uv export --format requirements-txt --no-hashes --output-file requirements.txt

Migrate existing applications to Azure

You can redeploy existing web applications to Azure as follows:

  1. Source repository. Maintain your source code in a suitable repository, like GitHub, which enables you to set up continuous deployment later in this process.

    • Your requirements.txt file must be at the root of your repository if you want App Service to automatically install the necessary packages.
  2. Database. If your app depends on a database, create the necessary resources on Azure as well.

  3. App Service resources. Create a resource group, App Service plan, and App Service web app to host your application. You can create these resources easily by running the Azure CLI command az webapp up. Or you can create and deploy resources as shown in the Flask, Django, or FastAPI with PostgreSQL tutorial. Replace the names of the resource group, App Service plan, and web app with names that are suitable for your application.

  4. Environment variables. If your application requires any environment variables, create equivalent App Service application settings. These App Service settings appear to your code as environment variables, as described in Access environment variables.

  5. App startup. Review the section Container startup process later in this article for information about how App Service attempts to run your app. App Service uses the Gunicorn web server by default. Gunicorn must be able to find your app object or wsgi.py folder. If you need to, you can Customize the startup command.

  6. Continuous deployment. Set up continuous deployment from GitHub Actions, Bitbucket, or Azure Repos as described in the article Continuous deployment to Azure App Service. Or set up continuous deployment from local Git as described in the article Local Git deployment to Azure App Service.

  7. Custom actions. To perform actions within the App Service container that hosts your app, such as Django database migrations, you can connect to the container by using SSH. For an example of running Django database migrations, see Tutorial: Deploy a Django web app with PostgreSQL.

    • When using continuous deployment, you can perform those actions by using post-build commands, as described earlier in the Customize build automation section.

With these steps completed, you should be able to commit changes to your source repository and have those updates automatically deployed to App Service.

Production settings for Django apps

For a production environment like App Service, Django apps should follow the guidance in Django's Deployment checklist.

The following table describes the production settings that are relevant to Azure. These settings are defined in the app's setting.py file.

Django setting Instructions for Azure
SECRET_KEY Store the value in an App Service setting, as described in Access app settings as environment variables. You can alternatively store the value as a secret in Azure Key Vault.
DEBUG Create a DEBUG setting on App Service with the value 0 (false), and then load the value as an environment variable. In your development environment, create a DEBUG environment variable with the value 1 (true).
ALLOWED_HOSTS In production, Django requires that you include the app's URL in the ALLOWED_HOSTS array of settings.py. You can retrieve this URL at runtime by using the code os.environ['WEBSITE_HOSTNAME']. App Service automatically sets the WEBSITE_HOSTNAME environment variable to the app's URL.
DATABASES Define settings in App Service for the database connection and load them as environment variables to populate the DATABASES dictionary. You can alternatively store the values (especially the username and password) as Key Vault secrets.

Serve static files for Django apps

If your Django web app includes static front-end files, first follow the instructions on managing static files in the Django documentation.

For App Service, you then make the following modifications:

  1. Consider using environment variables (for local development) and app settings (when deploying to the cloud) to dynamically set the Django STATIC_URL and STATIC_ROOT variables. For example:

    STATIC_URL = os.environ.get("DJANGO_STATIC_URL", "/static/")
    STATIC_ROOT = os.environ.get("DJANGO_STATIC_ROOT", "./static/")    
    

    DJANGO_STATIC_URL and DJANGO_STATIC_ROOT can be changed as necessary for your local and cloud environments. For example, if the build process for your static files places them in a folder named django-static, you can set DJANGO_STATIC_URL to /django-static/ in order to avoid using the default.

  2. If you have a pre-build script that generates static files in a different folder, include that folder in the Django STATICFILES_DIRS variable so that Django's collectstatic process finds them. For example, if you run yarn build in your front-end folder and Yarn generates a build/static folder containing static files, include that folder as shown here:

    FRONTEND_DIR = "path-to-frontend-folder" 
    STATICFILES_DIRS = [os.path.join(FRONTEND_DIR, 'build', 'static')]    
    

    In this code, FRONTEND_DIR is used to build a path to where a build tool like Yarn is run. You can again use an environment variable and app setting if you want to.

  3. Add whitenoise to your requirements.txt file. WhiteNoise (whitenoise.evans.io) is a Python package that makes it simple for a production Django app to serve its own static files. WhiteNoise serves the files that are found in the folder specified by the Django STATIC_ROOT variable.

  4. In your settings.py file, add the following line for WhiteNoise:

    STATICFILES_STORAGE = ('whitenoise.storage.CompressedManifestStaticFilesStorage')
    
  5. Modify the MIDDLEWARE and INSTALLED_APPS lists to include WhiteNoise:

    MIDDLEWARE = [                                                                   
        'django.middleware.security.SecurityMiddleware',
        # Add WhiteNoise middleware after the security middleware.                             
        'whitenoise.middleware.WhiteNoiseMiddleware',
        # Other values follow.
    ]
    
    INSTALLED_APPS = [
        "whitenoise.runserver_nostatic",
        # Other values follow.
    ]
    

Serve static files for Flask apps

If your Flask web app includes static front-end files, first follow the instructions in managing static files in the Flask documentation. For an example of serving static files in a Flask application, see the sample Flask application on GitHub.

To serve static files directly from a route on your application, you can use the send_from_directory method:

from flask import send_from_directory

@app.route('/reports/<path:path>')
def send_report(path):
    return send_from_directory('reports', path)

Container characteristics

When deployed to App Service, Python apps run within a Linux Docker container that's defined in the App Service Python GitHub repository. You can find the image configurations in the version-specific directories.

This container has the following characteristics:

  • Apps are run by the Gunicorn WSGI HTTP Server with the extra arguments --bind=0.0.0.0 --timeout 600.

  • By default, the base container image includes only the Flask web framework, but the container supports other frameworks that are WSGI-compliant and compatible with Python 3.6 and later, such as Django.

  • To install other packages, such as Django, create a requirements.txt file in the root of your project that specifies your direct dependencies. App Service then installs those dependencies automatically when you deploy your project.

    The requirements.txt file must be in the project root or dependencies won't be installed. If this file isn't in the root, the build process reports the error "Could not find setup.py or requirements.txt; Not running pip install." If you encounter this error, check the location of your requirements file.

  • App Service automatically defines an environment variable named WEBSITE_HOSTNAME that contains the web app's URL, such as msdocs-hello-world.chinacloudsites.cn. It also defines WEBSITE_SITE_NAME, which contains the name of your app, such as msdocs-hello-world.

  • npm and Node.js are installed in the container so you can run Node-based build tools, such as Yarn.

Container startup process

During startup, the App Service on Linux container runs the following steps:

  1. Use a custom startup command, if one is provided.
  2. Check for the existence of a Django app, and start Gunicorn for it if one is detected.
  3. Check for the existence of a Flask app, and start Gunicorn for it if one is detected.
  4. If no other app is found, start a default app that's built into the container.

The following sections provide extra details for each option.

Django app

For Django apps, App Service looks for a file named wsgi.py in your app code, and then runs Gunicorn by using the following command:

# <module> is the name of the folder that contains wsgi.py
gunicorn --bind=0.0.0.0 --timeout 600 <module>.wsgi

If you want more specific control over the startup command, use a custom startup command, replace <module> with the name of folder that contains wsgi.py, and add a --chdir argument if that module isn't in the project root. For example, if your wsgi.py is located under knboard/backend/config from your project root, use the arguments --chdir knboard/backend config.wsgi.

To enable production logging, add the --access-logfile and --error-logfile parameters, as shown in the examples for custom startup commands.

Flask app

For Flask, App Service looks for a file named application.py or app.py and starts Gunicorn as follows:

# If application.py
gunicorn --bind=0.0.0.0 --timeout 600 application:app

# If app.py
gunicorn --bind=0.0.0.0 --timeout 600 app:app

If your main app module is contained in a different file, use a different name for the app object. If you want to provide other arguments to Gunicorn, use a custom startup command.

Default behavior

If the App Service doesn't find a custom command, a Django app, or a Flask app, it runs a default read-only app, located in the opt/defaultsite folder and shown in the following image.

If you deployed code and still see the default app, see Troubleshooting - App doesn't appear.

Screenshot of the default App Service on Linux web page.

Customize startup command

You can control the container's startup behavior by providing either a custom startup command or multiple commands in a startup command file. A startup command file can use whatever name you choose, like startup.sh, startup.cmd, or startup.txt.

All commands must use paths that are relative to the project root folder.

To specify a startup command or command file:

  • Azure portal. Under Settings in the left pane of the app's page, select Configuration, and then select General settings. In the Startup Command box, enter either the full text of your startup command or the name of your startup command file. Then select Save to apply the changes. See Configure general settings for Linux containers.

  • Azure CLI. Use the az webapp config set command with the --startup-file parameter to set the startup command or file:

    az webapp config set --resource-group <resource-group-name> --name <app-name> --startup-file "<custom-command>"
    

    Replace <custom-command> with either the full text of your startup command or the name of your startup command file.

App Service ignores any errors that occur when processing a custom startup command or file, and then continues its startup process by looking for Django and Flask apps. If you don't see the behavior you expect, verify that your startup command or file is error-free and that a startup command file is deployed to App Service along with your app code. You can also check the diagnostic logs for more information. And you can check the app's Diagnose and solve problems page on the Azure portal.

Example startup commands

  • Added Gunicorn arguments: The following example adds the --workers=4 argument to a Gunicorn command line for starting a Django app:

    # <module-path> is the relative path to the folder that contains the module
    # that contains wsgi.py. <module> is the name of the folder that contains wsgi.py.
    gunicorn --bind=0.0.0.0 --timeout 600 --workers=4 --chdir <module_path> <module>.wsgi
    

    For more information, see Running Gunicorn. If you're using autoscale rules to scale your web app up and down, you should also dynamically set the number of Gunicorn workers by using the NUM_CORES environment variable in your startup command. For example, --workers $((($NUM_CORES*2)+1)). For more information on setting the recommended number of Gunicorn workers, see the Gunicorn FAQ.

  • Enable production logging for Django: Add the --access-logfile '-' and --error-logfile '-' arguments to the command line:

    # '-' for the log files means stdout for --access-logfile and stderr for --error-logfile.
    gunicorn --bind=0.0.0.0 --timeout 600 --workers=4 --chdir <module_path> <module>.wsgi --access-logfile '-' --error-logfile '-'
    

    These logs will appear in the App Service log stream.

    For more information, see Gunicorn logging.

  • Custom Flask main module: By default, App Service assumes that a Flask app's main module is application.py or app.py. If your main module uses a different name, you must customize the startup command. For example, if you have a Flask app whose main module is hello.py and the Flask app object in that file is named myapp, this is the command:

    gunicorn --bind=0.0.0.0 --timeout 600 hello:myapp
    

    If your main module is in a subfolder, such as website, specify that folder with the --chdir argument:

    gunicorn --bind=0.0.0.0 --timeout 600 --chdir website hello:myapp
    
  • Use a non-Gunicorn server: To use a different web server, such as aiohttp, use the appropriate command as the startup command or in the startup command file:

    python3.7 -m aiohttp.web -H localhost -P 8080 package.module:init_func
    

Access app settings as environment variables

App settings are values that are stored in the cloud specifically for your app, as described in Configure app settings. These settings are available to your app code as environment variables and accessed via the standard os.environ pattern.

For example, if you create an app setting called DATABASE_SERVER, the following code retrieves that setting's value:

db_server = os.environ['DATABASE_SERVER']

Detect HTTPS session

In App Service, TLS/SSL termination happens at the network load balancers, so all HTTPS requests reach your app as unencrypted HTTP requests. If your app logic needs to check if the user requests are encrypted or not, inspect the X-Forwarded-Proto header.

if 'X-Forwarded-Proto' in request.headers and request.headers['X-Forwarded-Proto'] == 'https':
# Do something when HTTPS is used.

Popular web frameworks enable you to access the X-Forwarded-* information in your standard app pattern. For example, in Django you can use SECURE_PROXY_SSL_HEADER to configure Django to use the X-Forwarded-Proto header.

Access diagnostic logs

You can access the console logs generated from inside the container.

First, turn on container logging by running the following command:

az webapp log config --name <app-name> --resource-group <resource-group-name> --docker-container-logging filesystem

Replace <app-name> and <resource-group-name> with the names appropriate for your web app.

Once container logging is turned on, run the following command to see the log stream:

az webapp log tail --name <app-name> --resource-group <resource-group-name>

If you don't see console logs immediately, check again in 30 seconds.

To stop log streaming at any time, type Ctrl+C.

You can also inspect the log files in a browser at https://<app-name>.scm.chinacloudsites.cn/api/logs/docker.

To access logs in the Azure portal, select Monitoring > Log stream in the left pane for your app.

Access deployment logs

When you deploy your code, App Service performs the build process described earlier, in the Customize build automation section. Because the build runs in its own container, build logs are stored separately from the app's diagnostic logs.

Use the following steps to access the deployment logs:

  1. On the Azure portal page for your web app, select Deployment > Deployment Center in the left pane.
  2. On the Logs tab, select the Commit ID for the most recent commit.
  3. On the Log details page that appears, select the Show Logs link that appears next to Running oryx build.

Build issues, like incorrect dependencies in requirements.txt and errors in pre-build or post-build scripts, appear in these logs. Errors also appear if your requirements file isn't named requirements.txt or doesn't appear in the root folder of your project.

Open SSH session in a browser

To make open a direct SSH session with your container, your app should be running.

Paste the following URL into your browser and replace <app-name> with your app name:

https://<app-name>.scm.chinacloudsites.cn/webssh/host

If you're not yet authenticated, you're required to authenticate with your Azure subscription to connect. Once authenticated, you see an in-browser shell, where you can run commands inside your container.

SSH connection

Note

Any changes you make outside the /home directory are stored in the container itself and don't persist beyond an app restart.

To open a remote SSH session from your local machine, see Open SSH session from remote shell.

When you're successfully connected to the SSH session, you should see the message "SSH CONNECTION ESTABLISHED" at the bottom of the window. If you see errors like "SSH_CONNECTION_CLOSED" or a message stating that the container is restarting, an error might be preventing the app container from starting. See Troubleshooting for information about investigating possible issues.

URL rewrites

When deploying Python applications on App Service for Linux, you might need to handle URL rewrites within your application. This method is particularly useful for ensuring that specific URL patterns are redirected to the correct endpoints without relying on external web server configurations. For Flask applications, you can use URL processors and custom middleware to achieve this. In Django applications, the URL dispatcher enables efficient management of URL rewrites.

App doesn't appear

  • You see the default app after deploying your own app code. The default app appears because you either haven't deployed your app code to App Service or because App Service failed to find your app code and ran the default app instead.

    • Restart the app, wait 20 seconds, and then check the app again.

    • Use SSH to connect directly to the App Service container and verify that your files exist under site/wwwroot. If your files don't exist, take the following steps:

      1. Create an app setting named SCM_DO_BUILD_DURING_DEPLOYMENT with a value of 1, redeploy your code, wait a few minutes, and then try to access the app again. For more information on creating app settings, see Configure an App Service app in the Azure portal.
      2. Review your deployment process, check the deployment logs, correct any errors, and redeploy the app.
    • If your files exist, App Service wasn't able to identify your startup file. Ensure that your app is structured as App Service expects for Django or Flask, or use a custom startup command.

  • You see the message "Service Unavailable" in the browser. The browser timed out waiting for a response from App Service. This indicates that App Service started the Gunicorn server but the app itself didn't start. This condition could indicate that the Gunicorn arguments are incorrect or that there's an error in the app code.

    • Refresh the browser, especially if you're using the lowest pricing tiers in your App Service plan. The app might take longer to start up when you use free tiers, for example, and become responsive after you refresh the browser.

    • Verify that your app is structured as App Service expects for Django or Flask, or use a custom startup command.

    • Examine the app log stream for error messages. The logs will show any errors in the app code.

Could not find setup.py or requirements.txt

  • The log stream shows "Could not find setup.py or requirements.txt; Not running pip install." The Oryx build process failed to find your requirements.txt file.

    • Connect to the web app's container via SSH and verify that requirements.txt is named correctly and exists directly under site/wwwroot. If it doesn't exist, make sure the file exists in your repository and is included in your deployment. If it exists in a separate folder, move it to the root.

ModuleNotFoundError when app starts

If you see an error like ModuleNotFoundError: No module named 'example', Python couldn't find one or more of your modules when the application started. This error most often occurs if you deploy your virtual environment with your code. Virtual environments aren't portable, so a virtual environment shouldn't be deployed with your application code. Instead, let Oryx create a virtual environment and install your packages on the web app by creating an app setting, SCM_DO_BUILD_DURING_DEPLOYMENT, and setting it to 1. This setting forces Oryx to install your packages whenever you deploy to App Service. For more information, see this article on virtual environment portability.

Database is locked

When attempting to run database migrations with a Django app, you might see "sqlite3. OperationalError: database is locked." The error indicates that your application is using a SQLite database, for which Django is configured by default, rather than using a cloud database like Azure Database for PostgreSQL.

Check the DATABASES variable in the app's settings.py file to ensure that your app is using a cloud database instead of SQLite.

If you're encountering this error with the sample in Tutorial: Deploy a Django web app with PostgreSQL, check that you completed the steps in Verify connection settings.

Other issues

  • Passwords don't appear in the SSH session when typed: For security reasons, the SSH session keeps your password hidden when you type. The characters are being recorded, however, so type your password as usual and select Enter when you're done.

  • Commands in the SSH session appear to be cut off: The editor might not be word-wrapping commands, but they should still run correctly.

  • Static assets don't appear in a Django app: Ensure that you have enabled the WhiteNoise module.

  • You see the message "Fatal SSL Connection is Required": Check any usernames and passwords used to access resources (such as databases) from within the app.