Django API Apps on Windows IIS

How to set up Windows Server IIS to serve Django 3.0.3(+) and Python 3.8

This is a guide on setting up Python/Django apps to run on a Windows server using IIS as the webserver. I'll go over the specifics below. We're starting things off with the following assumptions:

  1. Windows Server is installed somewhere and running with a static IP and domain name all set.
  2. Server SSL Certificate has already been provisioned and set up. (Optional but extremely recommended to run HTTPS)
  3. (not specifically necessary) any SSO setup/shibboleth stuff has already been set up. (This is if you want to leverage SSO login, etc.)
  4. Everything is running 64-bit architecture.

Python

Install the latest Python version using the GUI Windows x64 installer downloaded from the python.org. As of writing, the latest version available is 3.8.2.

Make the following settings changes to the Python installation (we're going for a minimal installation with just the Python core and a few niceties):

  1. Check option for “Add Python 3.8 to PATH”
  2. Click “Customize Installation”
  3. Deselect all options except pip.
  4. click Next
  5. Check “Install for all users”
  6. Deselect “Create shortcuts for installed applications”
  7. Check “Add Python to environment variables”
  8. Check “Precompile standard library” (not specifically necessary, but doesn't hurt anything)
  9. NO “debugging symbols” or “debug binaries” (this is supposed to be a prod environment, after all)
  10. Change the Installation directory: C:\Python38\
  11. Install.

Virtualenv

Once Python's installation is complete open an administrative terminal/Powershell window (winkey+x, a) and complete the following:

Note: If any of the following commands come back with something like “command not found” double-check that C:\Python38\ and C:\Python38\Scripts\ are in the system PATH environment variable (run $Env:Path in powershell). If you had the terminal window before installing Python, close and re-open it. Or just add the Python directories to the system PATH manually.

  1. upgrade pip

python -m pip install --upgrade pip

  1. install virtualenv

pip install virtualenv

IIS Setup / Prerequisites

IIS needs to be installed, with the CGI option. Once that is installed there should be a directory C:\inetpub\wwwroot\

Method 1

  1. Open “Windows Features” (search for “Windows Features” > “Turn Windows Features on or off” should be the result or Run (winkey+r) > optionalfeatures.exe – If that doesn't do anything, try Method 2 Below.
  2. Select IIS feature, with the additional options highlighted as in the below image.
    • CGI
    • HTTP Redirection
    • Request Monitor
  3. OK
Selected IIS features in Windows Features dialog

Windows Features

options may vary, features available may vary, etc. etc.

Method 2

  1. Open the Server Manager
  2. Click “Manage”
  3. Click “Add Roles and Features”
  4. Go through the wizard and ensure that all the features listed in Method 1 are selected, specifically IIS services and especially CGI, HTTP Redirection, and Request Monitor. The items are the same as above in Method 1, but are organized a little differently.
Opening Server Manager and Adding Roles or Features

Server Manager Screenshot

Select Manage, then Add Roles and Features, then the wizard shown at 3️⃣ should show

Test

Finally, test that the IIS server installation worked and that you can browse to http://localhost on a browser, and that you get the default IIS page.

Set Up Django Application Directory and Virtual Environment for Python

Here's where we set up the application folder that will host our Django application and all the required Python libraries to support that application without installing anything globally. This will make sure that if there's any other Python apps that need to run on the server or be served by the server, there won't be dependency version conflicts.

  1. Create an application folder to host your application. I wanted my app to be served from <webserver_root_url>/app so I put my application folder at c:\inetpub\wwwroot\app\. NOTE this does not necessarily have to be in your inetpub/wwwroot folder, it's just a bit easier to do it this way.
  2. Open an elevated console within the \app directory (typically will need to be elevated to do things in this directory because security stuff)
  3. Create a virtual environment with virtualenv

> virtualenv venv

  1. This should create a directory called “venv” in \app\.
  2. Activate the virtual environment so that any Python or Pip commands work against it instead of the global Python environment.

> .\venv\Scripts\activate

  1. Copy in your Django application, including your requirements.txt file.
  2. Install python dependencies from requirements:

> pip install -r requirements.txt

  1. We'll need the latest wfastcgi python package too (in case it's not in your requirements, since it's not needed to run the development server):

pip install wfastcgi

Note: if you run into problems with wfastcgi not working, or are getting errors like “the fastcgi process exited unexpectedly” then try to force wfastcgi to upgrade:

pip install wfastcgi --upgrade

MySQL Client Python Wheels (precompiled binary for Windows)

Pip doesn't install mysqlclient correctly on Windows, at least not that I've been able to manage. The easiest way to get correct MySQL clients for Windows systems is from Christoph Gohlke, Laboratory for Fluorescence Dynamics, University of California, Irvine. Download the appropriate mysqlclient package for Windows there and then move that to the server. Install with

pip install <path to .whl file>

Set Up Django Site in IIS

IIS has specific requirements around how a site is set up, in order for it to work properly. Specifically, each site must have at least 1 application and each application must have at least 1 virtual directory. This page from Microsoft Docs has detailed information on the requirements for a site to publish correctly on IIS.

  1. Open IIS manager (winkey+r) Run > inetmgr
  2. Select the Server and from the main page, double-click “FastCGI Settings”
  3. “Add Application”
  4. Fill out the settings dialog accordingly:
    • “Full Path”: Where your virtual environment's python.exe lives (such as C:\inetpub\wwwroot\app\venv\Scripts\python.exe)
    • “Arguments”: path to wfastcgi.py which should also be in the virtual environment directory: C:\inetpub\wwwroot\app\venv\Lib\site-packages\wfastcgi.py
    • In the “FastCGI Settings” section, under “General > Environment Variables” click on the “(Collection)” line, then on the little ellipsis ("[…]") button, which will allow entering Environment Variables specific to the runtime environment that Django will be running in when a request comes into the web server.
      • In the “EnvironmentVariables Collection Editor” window:
      • Add: Name: DJANGO_SETTINGS_MODULE Value: whatever matches up to your setting in wsgi.py. For me this was server.environment
      • Add: Name: PYTHONPATH Value: C:\inetpub\wwwroot\app
      • Add: Name: WSGI_HANDLER Value: django.core.wsgi.get_wsgi_application()
      • If you want WSGI Logging: Add: Name: WSGI_LOG Value: wherever you want logs to be written. I put: C:\inetpub\logs\WSGI\app.log (this file can get verbose, consider removing this once you've made sure the application is working well)
        • ⚠ WARNING - READ THIS ⚠: You must make sure that the local server's worker processes have write permission on this file or it's directory. If you do not, wfastcgi/python will crash out and IIS will throw 500 server errors. I spent days fighting with this. The easiest fix is to manually create the file C:\inetpub\logs\WSGI\app.log and then edit the security permissions on that file, granting full write permission to the local server group “IIS_IUSRS”.

This should correctly set up the environment for FastCGI to be able to run the Django application (assuming that the paths above match to where you're working from). Note (1): For DJANGO_SETTINGS_MODULE I used server.environment - this matches my environment, since I have /app/server/environment.py and environment.py lists out which server settings should be loaded. Note (2): All of the above settings for Environment Variables are case sensitive.

  1. Close the Environment Variables window and the FastCGI Settings windows.
  2. On the left-hand pane of IIS Manager, under “connections” where the server we're working on, expand the server, and under “Default Web Site", there should be a listing of directories that are in wwwroot\. Here, we'll convert \app\ into an application (right-click on the directory, then select “convert to application”) - Click OK on the “Add Application” window that pops up.
  3. Open Handler Mappings for the application
  4. Click “Add module mapping” and enter the following settings:
    • Path: *
    • Module (dropdown): “FastCgiModule
    • Executable: type in: C:\inetpub\wwwroot\app\venv\Scripts\python.exe|C:\inetpub\wwwroot\app\venv\Lib\site-packages\wfastcgi.py
    • Name: “Django Handler” or “Python FastCGI handler” or whatever - it's just a friendly name for the mapping.
    • Click “Request Restrictions
      • Deselect “Invoke handler only if… mapped to:”
      • Verbs: All verbs
      • Access: Script
    • Click OK
    • Click OK
    • When a popup asks “Do you want to create a FastCGI Application for this executable?” click “No” as that has already been handled / set up.
  5. The handler should now show in the list of Handler Mappings.
  6. Click “View Ordered List…” on the right, and move the newly created handler to the top of the list. This will ensure that the python handler is the first one considered for all requests to this application.

Restart your IIS website and it should now be working where the Django application should be reachable at http://localhost/app/ (assuming your Django site has a page listed there).

IIS restart commands:
> iisreset /stop
> net start e3svc

Configure Django and IIS Static Files

The Django development server automatically handles serving static files when working on your computer, but now that it's in a production environment, we need to collect the static files to a directory and then tell IIS to serve them from that directory. Most details on serving static files, as well as handling additional details should be found on Django's documentation site: static files deployment.

Settings

Django's settings need to be modified to include the options STATIC_ROOT and STATIC_URL.

STATIC_ROOT is used to tell Django's collectstatic command where in the filesystem to place the found static files. This location could, in theory, be anywhere on the filesystem, but it's good practice to keep these files in a location that makes sense in terms of compartmentalization and context. I put my files inside the project folder next to manage.py.

1
2
3
# settings.py or prod.py or wherever your production settings may be...
STATIC_ROOT = '/inetpub/wwwroot/app/static/' # Windows - assumes C as root; don't have to explicitly say "C:"
STATIC_URL = '/app/static/'

Move Files

Run the collectstatic management command from the project directory.

Activate the virtualenv:

.\venv\scripts\activate

Run the command:

python manage.py collectstatic

Say “yes” to the prompt from the collectstatic management command to confirm the directory you want to copy static files to.

Set Up a Virtual Directory for IIS to Serve the Static Files

IIS needs to know where these files are located and how to serve them up when browsers request them. THe name of a virtual directory must match the value of the STATIC_URL setting in Django's settings.py, absent the beginning and trailing slashes. For this sample, the url is app/static.

  1. Open IIS Manager
  2. On the left pane, under “Connections” expand the server’s tree
  3. Expand the “Sites” folder
  4. Right-Click the web site your app lives in (for me, I put everything in “default web site”)
  5. Click “Add Virtual Directory”
  6. Enter the following values:

Alias: static
Physical Path: C:\inetpub\wwwroot\app\static

static directory in the IIS directory tree

static virtual directory in IIS

Your "static" folder should now have a shortcut-looking icon on it, as shown here.

Configure Handler Mappings for Static Files

  1. Select the “static” virtual directory
  2. Open “Handler Mappings”
  3. On the right side, select “View Ordered List…”
  4. Move the “StaticFile” handler to the top of the list by selecting it, then on the right under “Actions” click “Move Up” until the handler is above all others. If IIS warns you about diverging from inheriting settings, click OK - this is what we want to do.

At this point Django app(s) should be available and serving from IIS at /app or /app/admin from your webserver, with all the static assets and CSS loaded properly. If not, go back over the Static Files settings, and make sure that the static assets collected by collectstatic correctly found and placed all the files you're relying on in the correct location.

Shibboleth / SSO / REMOTE_USER

⚠ Warning: This section describes some elements of setting up SSO login / security for your sites and server. I don't claim to be an authority on setting up Shibboleth or configurations, or the security implications of any of the settings and configuration in this section. Please review all the relevant security documentation / best practices for Shibboleth, Django, and/or IIS. This setup will use session authentication from a trusted Identity Provider (IDP) with Shibboleth in this case being set up as a Service Provider (SP).

IIS / Shibboleth (as Service Provider / SP)

The Shibboleth service needs to be installed and configured on the webserver. Once installed and configured, the path to the API / App / Site must be listed in shibboleth's configuration file Shibboleth2.xml. By default this file can be found in C:\opt\shibboleth-sp\etc\shibboleth\. The relevant path(s) to change for shibboleth to protect a url path on the webserver is listed in the section noted below (note: Shibboleth2.xml tags shown are truncated).

1
2
3
4
5
6
<SPConfig>
  <RequestMapper>
    <RequestMap>
      <Host name="your.server.fqdn">
        <Path name="app" authType="shibboleth" requireSession="true"/>
        ...

REMOTE_USER / Shibboleth Setting

In the same Shibboleth2.xml file, set the REMOTE_USER setting to whatever is provided by the associated identity provider. The setting can be changed as one of the attributes on the ApplicationDefaults element in the configuration file:

1
2
3
4
5
6
<SPConfig>
  ...
  <ApplicationDefaults entityID="Something"
    REMOTE_USER="some SAML element to set to - Django can then use this for auth"
    cipherSuites="some list of cipher suites">
    ...

Setting the REMOTE_USER environment variable allows Django to read the logged in SSO username (or some other SAML value) and use that to see who is logged in in the session.

Once any changes have been saved to Shibboleth2.xml, the Shibboleth service on the server needs to be restarted. Open Services (Run -> services.msc) -> select “Shibboleth Daemon (Default)” -> Restart.

Django Configuration

A few things need to be added to middleware and authentication backends to enable use of the remote user environment variable set by shibboleth in IIS for purposes of authenticating users to Django / Apps.

In prod.py (or wherever production settings are stored, like settings.py) add the following to allow reading of REMOTE_USER from the request:

1
2
3
4
5
6
7
8
9
MIDDLEWARE = MIDDLEWARE + [
    # ADDDED REMOTE USER MIDDLEWARE FOR SHIBBOLETH AUTH
    'django.contrib.auth.middleware.PersistentRemoteUserMiddleware',
]

AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.RemoteUserBackend',
    'django.contrib.auth.backends.ModelBackend', #Fallback
]

Note that in this case PersistentRemoteUserMiddleware is appended to the end of the MIDDLEWARE list, which is imported from base.py. If your configuration has other middleware that depends on specific ordering, then this solution may not be optimal for all cases.

There are also two flavors of Remote User Middleware - the generic and the .Persistent... variety. For more details on use see Authenticating using REMOTE_USER.

Thanks and Resources

Below is a listing of all the tabs I had open for reference when figuring this out and writing this.

Article Changes

2020/04/24

  • Added this changelog because this article is getting revised a lot
  • Added notes on MySQL client and resource for precompiled wheels from uci.edu
  • Added more details on Shibboleth configuration steps to leverage preexisting SSO infrastructure for setting REMOTE_USER