Configuration Guidelines
This document is a topical reference for the Apache configuration needed to host WSGI applications with mod_wsgi. It covers the mounting directives, static-file co-hosting, the daemon-mode process model, application groups, per-application configuration injection, authentication, request-body limits, and reverse-proxy/HTTPS deployment.
For a step-by-step first-time tutorial — three progressively richer
VirtualHost examples building up from basic mounting to daemon
mode — see Quick Configuration Guide instead.
If you do not need to integrate with an existing system Apache
install, the mod_wsgi-express command (installed alongside the
mod_wsgi PyPI package) generates a working configuration in the
same shape as the examples on this page and runs Apache directly.
See Getting Started for that path. This page is for cases
where you are hand-writing Apache configuration so a system Apache
instance you already operate can host the WSGI application.
The WSGIScriptAlias Directive
Configuring Apache to run WSGI applications using mod_wsgi is similar
to how Apache is configured to run CGI applications. To streamline
this, mod_wsgi provides a WSGIScriptAlias directive analogous to
Apache’s ScriptAlias: it combines the URL-to-file mapping and the
handler designation into a single directive.
The first form of WSGIScriptAlias associates a WSGI application
with a specific URL prefix:
WSGIScriptAlias /myapp /usr/local/wsgi/scripts/myapp.wsgi
The second argument must be the absolute pathname of the WSGI script file. A trailing slash should not be added when the path refers to a script file rather than a directory.
The script file must define a callable named application that
follows the WSGI specification. A minimal hello-world example:
def application(environ, start_response):
status = '200 OK'
output = b'Hello World!'
response_headers = [('Content-type', 'text/plain'),
('Content-Length', str(len(output)))]
start_response(status, response_headers)
return [output]
The callable name is fixed unless you override it via
WSGICallableObject. The script file
does not need to use a .py extension. The .wsgi convention
shown in the examples on this page is used to avoid clashing with any
pre-existing AddHandler directive that may already map .py
files to a different handler such as cgi-script. If you know
there is no such conflict, the script file can use .py like any
other Python file.
Apache access controls apply to the directory containing the WSGI
script. If the script lives outside any directory already known to
Apache, declare it with a <Directory> block:
<Directory /usr/local/wsgi/scripts>
Require all granted
</Directory>
Apply Require to <Directory> rather than <Location> —
applying access controls to a <Location> (especially /) is
not best practice and can weaken the security of the server.
Use of WSGIScriptAlias does not require explicitly enabling
ExecCGI via Options — execute permission is implied by the
directive itself, just as for ScriptAlias.
To mount a WSGI application at the root of the site:
WSGIScriptAlias / /usr/local/wsgi/scripts/myapp.wsgi
Multiple WSGIScriptAlias directives can be listed; earlier
matches take precedence. List the most specific URL prefixes first:
WSGIScriptAlias /wiki /usr/local/wsgi/scripts/mywiki.wsgi
WSGIScriptAlias /blog /usr/local/wsgi/scripts/myblog.wsgi
WSGIScriptAlias / /usr/local/wsgi/scripts/myapp.wsgi
The second form maps a URL prefix to a directory of WSGI scripts. The next path segment after the prefix selects which script in the target directory handles the request:
WSGIScriptAlias /wsgi/ /usr/local/wsgi/scripts/
Both the URL prefix and directory path must end with a trailing slash in this form.
To allow scripts to be selected without their extension appearing
in the URL, use WSGIScriptAliasMatch with a regex that captures
the script name and substitutes it back into the file path:
WSGIScriptAliasMatch ^/wsgi/([^/]+) /usr/local/wsgi/scripts/$1.wsgi
Framework integration
For most Python web frameworks the WSGI script file is something the framework already provides:
Django.
django-admin startprojectgenerates awsgi.pyat the top of the project tree that exposes a WSGIapplicationcallable. PointWSGIScriptAliasat that file directly:WSGIScriptAlias / /var/www/myproject/myproject/wsgi.py
Flask. Either create a small
.wsgiscript that imports the Flask application instance and binds it toapplication:from myapp import app as application
Then point
WSGIScriptAliasat the.wsgifile. Or, pointWSGIScriptAliasdirectly at the Flask module and use WSGICallableObject to tell mod_wsgi the callable is namedapprather thanapplication:WSGIScriptAlias / /var/www/myapp/myapp.py <Directory /var/www/myapp> <Files myapp.py> WSGICallableObject app </Files> Require all granted </Directory>
This avoids the extra shim file at the cost of having
WSGIScriptAliaspoint at a.pyfile — only do this if noAddHandlerdirective in scope already maps.pyto a different handler.Other WSGI frameworks. Whatever the framework’s documented WSGI entry-point object is, expose it as a top-level
applicationsymbol in the script file pointed at byWSGIScriptAlias.
ASGI frameworks such as FastAPI and Starlette do not natively run
on a WSGI server. They can be hosted under mod_wsgi via an
ASGI-to-WSGI shim such as a2wsgi, but the async benefits are
lost in that configuration. The recommended pattern for ASGI
applications is to run them under a dedicated ASGI server
(uvicorn, hypercorn) — optionally with Apache acting as a
reverse proxy in front, terminating TLS and serving static files.
Hosting Of Static Files
When WSGIScriptAlias mounts an application at the root of the
site, every request maps to the WSGI application — including
requests for static assets that the application does not own. Use
Alias, AliasMatch, or directory-based handler configuration
to route those requests back to Apache before the WSGI alias
matches:
Alias /robots.txt /usr/local/wsgi/static/robots.txt
Alias /favicon.ico /usr/local/wsgi/static/favicon.ico
AliasMatch /([^/]*\.css) /usr/local/wsgi/static/styles/$1
Alias /media/ /usr/local/wsgi/static/media/
<Directory /usr/local/wsgi/static>
Require all granted
</Directory>
WSGIScriptAlias / /usr/local/wsgi/scripts/myapp.wsgi
<Directory /usr/local/wsgi/scripts>
Require all granted
</Directory>
List the more specific URLs first. In practice the Alias
directive takes precedence over WSGIScriptAlias regardless of
order, but explicit ordering is good practice and makes the
intent obvious to a future reader.
Defining Process Groups
mod_wsgi can run a WSGI application in either embedded mode or daemon mode. This section covers the configuration shape; for the conceptual model, sizing guidance, and the patterns that come up in production deployments see Embedded and Daemon Mode.
In embedded mode the application runs in Python sub-interpreters
hosted inside the Apache child processes themselves. This gives the
lowest per-request overhead but has substantial drawbacks: every
code change requires a full Apache restart to pick up; the Apache
child processes’ memory footprint grows with the application; the
default Apache MPM tuning is geared for serving static files and
PHP and is rarely a good fit for a Python web application; and the
application shares its process with other Apache modules including
mod_php and any other dynamic-content modules in use.
In daemon mode mod_wsgi creates a dedicated set of processes running just the WSGI application. The Apache child processes act as proxies, forwarding requests to the daemon processes and relaying responses back. Daemon processes can be configured independently of Apache MPM tuning, can run as a different user from Apache, can be restarted without restarting Apache itself (touch the WSGI script file), and isolate the application from other Apache modules.
Daemon mode is the recommended deployment pattern for production WSGI applications. The remainder of this section assumes daemon mode.
A daemon process group is declared with WSGIDaemonProcess. WSGI
applications are delegated to a process group with
WSGIProcessGroup. A complete virtual host hosting a single WSGI
application in daemon mode, with static files served by Apache:
<VirtualHost *:80>
ServerName www.example.com
WSGIDaemonProcess myapp processes=2 threads=15 \
display-name=%{GROUP}
WSGIProcessGroup myapp
Alias /favicon.ico /usr/local/wsgi/static/favicon.ico
AliasMatch /([^/]*\.css) /usr/local/wsgi/static/styles/$1
Alias /media/ /usr/local/wsgi/static/media/
<Directory /usr/local/wsgi/static>
Require all granted
</Directory>
WSGIScriptAlias / /usr/local/wsgi/scripts/myapp.wsgi
<Directory /usr/local/wsgi/scripts>
Require all granted
</Directory>
</VirtualHost>
When Apache is started as root the daemon processes can run as
a user different from the Apache child user. The number of
processes, the threads-per-process count, and a per-process
maximum-request limit are all configurable.
A few of the more commonly used options to WSGIDaemonProcess:
user=name | user=#uid
The UNIX user name or numeric user uid the daemon processes run as. Defaults to whatever user Apache runs its child processes as (the
Userdirective). Ignored when Apache was not started asroot— in that case daemon processes run as the user Apache was started as, regardless of this option.
group=name | group=#gid
The UNIX group name or numeric group gid of the primary group the daemon processes run as. Defaults to the group from the
Groupdirective. Same root-required caveat asuser=.
processes=num
Number of daemon processes in the group. Default is one.
Note: setting
processes=1explicitly causeswsgi.multiprocessto beTruein the WSGI environment, while omitting the option entirely causeswsgi.multiprocessto beFalse. This is to allow front-end mapping mechanisms to distribute requests across multiple single-process daemon groups while still appearing multiprocess to the application. If your application requireswsgi.multiprocessto beFalse(for example, to run an interactive debugger), simply omit theprocessesoption and accept the implied default of one.
threads=num
Number of request-handling threads per daemon process. Default is 15.
maximum-requests=nnn
Number of requests a daemon process handles before it is shutdown and restarted. Useful as a safety net for accidental memory leaks in long-running applications. Default is no limit.
See also
restart-interval(wall-clock time threshold),cpu-time-limit(CPU time threshold), andinactivity-timeout(idle threshold) for sibling process-recycling triggers.
For the full set of options, see WSGIDaemonProcess.
Daemon process groups must have unique names across the server.
Two virtual hosts cannot both declare WSGIDaemonProcess myapp
... even if their other options differ.
When WSGIDaemonProcess is declared at server scope (outside any
<VirtualHost>), any virtual host can delegate to it. When
declared inside a <VirtualHost>, only WSGI applications
associated with that same virtual host can delegate to it. See
Configuration Issues for the failure modes around scoping
and naming.
A common multi-tenant setup is one daemon process group per virtual host, each running as the user that owns the application:
<VirtualHost *:80>
ServerName www.site1.com
CustomLog logs/www.site1.com-access_log common
ErrorLog logs/www.site1.com-error_log
WSGIDaemonProcess www.site1.com user=joe group=joe \
processes=2 threads=25
WSGIProcessGroup www.site1.com
...
</VirtualHost>
<VirtualHost *:80>
ServerName www.site2.com
CustomLog logs/www.site2.com-access_log common
ErrorLog logs/www.site2.com-error_log
WSGIDaemonProcess www.site2.com user=bob group=bob \
processes=2 threads=25
WSGIProcessGroup www.site2.com
...
</VirtualHost>
The argument to WSGIProcessGroup is normally the name of a
declared daemon process group. Two special expanding values are
available:
%{GLOBAL}
The process group name resolves to the empty string, which selects embedded mode rather than any daemon group. The application runs inside the Apache child processes, sharing process space with other Apache modules and running as the user Apache itself runs as.
%{ENV:variable}
The process group name resolves to the value of the named environment variable, looked up via Apache’s notes and subprocess environment data structures (and falling back to
getenv()from the Apache server process). The result must name an existing daemon process group.
Environment variables for the %{ENV} lookup can be set with
SetEnv and RewriteRule. For example, to pick a process
group from a database keyed by request URI:
RewriteEngine On
RewriteMap wsgiprocmap dbm:/etc/httpd/wsgiprocmap.dbm
RewriteRule . - [E=PROCESS_GROUP:${wsgiprocmap:%{REQUEST_URI}}]
WSGIProcessGroup %{ENV:PROCESS_GROUP}
Applying a process-recycling trigger such as maximum-requests
is recommended for any large application that depends on many
third-party packages, particularly applications that talk to a
database. Frameworks such as Django and Flask, and any application
using a long-lived connection pool, can benefit from periodic
recycling. If an application does not shut down cleanly when its
process is recycled it will be killed after the shutdown timeout
expires; if that happens regularly, run more than one process in
the group so that another process can continue serving requests
while the first restarts.
Daemon mode is not available on Windows. mod_wsgi on Windows supports only embedded mode.
Defining Application Groups
Within a process — whether an Apache child process in embedded mode or a daemon process — the WSGI application runs inside a Python sub-interpreter. The sub-interpreter is identified by an application group name. By default each WSGI application gets its own application group, which means each one gets its own sub-interpreter and its own copy of every imported Python module.
If a single process hosts many small WSGI applications and they can
safely share a Python module namespace, placing them all in the
same application group avoids the per-application memory overhead
of duplicate module imports. Use WSGIApplicationGroup:
<Directory /usr/local/wsgi/scripts>
WSGIApplicationGroup admin-scripts
Require all granted
</Directory>
The argument can be any unique name, with two special expanding values:
%{GLOBAL}
The application runs in the main Python interpreter — the one Python creates at process startup, before any sub-interpreters are spawned.
A small number of C extension modules — most commonly NumPy and SciPy, plus other modules built on the same simplified Python C API for GIL management — assume they are running in the main interpreter and misbehave inside sub-interpreters. The symptoms range from import errors to crashes once the extension is exercised. If your application uses such an extension directly or transitively, set
WSGIApplicationGroup %{GLOBAL}for it. See Configuration Issues for the full discussion.
%{ENV:variable}
The application group name resolves to the value of the named environment variable, looked up the same way as for
WSGIProcessGroup.
See WSGIApplicationGroup for the full list of expanding values and the matching rules.
As an example of using %{ENV:variable}, to group all WSGI
scripts beneath a specific mod_userdir-served user directory
into the same application group:
RewriteEngine On
RewriteCond %{REQUEST_URI} ^/~([^/]+)
RewriteRule . - [E=APPLICATION_GROUP:~%1]
<Directory /home/*/public_html/wsgi-scripts/>
Options ExecCGI
SetHandler wsgi-script
WSGIApplicationGroup %{ENV:APPLICATION_GROUP}
</Directory>
Application Configuration
To pass configuration values from the Apache configuration through
to the WSGI application, use SetEnv:
WSGIScriptAlias / /usr/local/wsgi/scripts/demo.wsgi
SetEnv demo.templates /usr/local/wsgi/templates
SetEnv demo.mailhost mailhost
SetEnv demo.debugging 0
Variables set this way appear in the WSGI environ dictionary on
each request. They are not the same as os.environ — the
process environment is unaffected by SetEnv and there is no
mod_wsgi mechanism for setting process environment variables from
Apache configuration.
For request-dependent variables, RewriteRule can be used to set
variables conditionally:
SetEnv demo.debugging 0
RewriteEngine On
RewriteCond %{REMOTE_ADDR} ^127.0.0.1$
RewriteRule . - [E=demo.debugging:1]
For configuration that SetEnv and RewriteRule cannot
express, wrap the application inside its WSGI script file and
mutate the environ dictionary before delegating to the real
application:
def _application(environ, start_response):
...
def application(environ, start_response):
if environ['REMOTE_ADDR'] == '127.0.0.1':
environ['demo.debugging'] = '1'
return _application(environ, start_response)
User Authentication
By default Apache does not pass HTTP authorisation headers through to WSGI applications, the same restriction it applies to CGI scripts. The reason is the same: passing the authorisation header through could leak credentials to a WSGI application that should not see them when Apache is performing the authentication.
Set WSGIPassAuthorization to
On to pass the Authorization HTTP request header through to
the application as the HTTP_AUTHORIZATION WSGI environment
variable. This is what you want when the WSGI application itself
implements authentication:
WSGIPassAuthorization On
When Apache (rather than the WSGI application) performs the
authentication, the WSGI application can still see the result via
the AUTH_TYPE and REMOTE_USER environment variables —
AUTH_TYPE indicates which authentication scheme Apache used,
REMOTE_USER is the authenticated login name.
Limiting Request Content
By default Apache imposes no limit on the size of a request body. A WSGI application that reads the entire request body into memory will exhaust available memory under a malicious upload, regardless of any size checks the application itself implements.
Set Apache’s LimitRequestBody to a sensible upper bound on the
request body size for the application:
LimitRequestBody 1048576
The argument is the maximum number of bytes allowed in the request
body. mod_wsgi performs the check before the WSGI application is
invoked: when the limit is exceeded mod_wsgi returns a 413
response and closes the client connection without ever calling the
application. The 413 response page is whatever Apache or the
applicable ErrorDocument directive defines.
Reverse Proxy And HTTPS Termination
A common production deployment pattern places mod_wsgi behind a
separate reverse proxy that terminates TLS, typically nginx,
HAProxy, or a managed load balancer such as AWS ALB. The proxy
adds X-Forwarded-* headers carrying the original client
information; mod_wsgi rewrites the WSGI environment from those
headers so the application sees the original client context
rather than the connection-level details between the proxy and
Apache.
The two directives that control this are WSGITrustedProxies (IP addresses or CIDR ranges of front-end proxies whose forwarded headers should be trusted) and WSGITrustedProxyHeaders (which forwarded headers to honour).
For the full picture, including the matching front-end proxy
configuration, redirect / Location-header rewriting issues,
and the equivalent mod_wsgi-express options, see
Running Behind A Reverse Proxy.
The Apache Alias Directive
WSGIScriptAlias is the recommended way to mount a WSGI
application. As an alternative, the standard Apache Alias
directive can be combined with SetHandler or AddHandler to
designate URLs as WSGI scripts. This pattern is mostly relevant
when WSGI scripts need to coexist with static files, CGI scripts,
or directory indexes in the same directory — situations the
WSGIScriptAlias form does not address.
The equivalent of:
WSGIScriptAlias /wsgi/ /usr/local/wsgi/scripts/
<Directory /usr/local/wsgi/scripts>
Require all granted
</Directory>
using Alias plus SetHandler is:
Alias /wsgi/ /usr/local/wsgi/scripts/
<Directory /usr/local/wsgi/scripts>
Options ExecCGI
SetHandler wsgi-script
Require all granted
</Directory>
The differences from WSGIScriptAlias are that Options
ExecCGI must be enabled explicitly, and the wsgi-script
handler must be designated explicitly (WSGIScriptAlias does
both implicitly).
Mixed content directories
To mix static files, CGI scripts, and WSGI applications in one
directory, use AddHandler instead of SetHandler so the
handler is selected by file extension:
Alias /wsgi/ /usr/local/wsgi/scripts/
<Directory /usr/local/wsgi/scripts>
Options ExecCGI
AddHandler cgi-script .cgi
AddHandler wsgi-script .wsgi
Require all granted
</Directory>
For whichever extensions you use, make sure no earlier
configuration applies a different handler to those same extensions
in the same context — if both cgi-script and wsgi-script
are bound to the same extension the order of the directives
determines which wins, and the wrong handler may be selected.
To allow the extension to be omitted from the URL, add Apache’s
MultiViews option and configure MultiviewsMatch to consider
handlers when matching:
<Directory /usr/local/wsgi/scripts>
Options ExecCGI MultiViews
MultiviewsMatch Handlers
AddHandler cgi-script .cgi
AddHandler wsgi-script .wsgi
Require all granted
</Directory>
This is most useful when migrating from CGI to WSGI without changing existing URLs — Apache picks the WSGI version of a resource over the CGI version when both exist.
To enable directory listings or directory indexes alongside the WSGI handler:
<Directory /usr/local/wsgi/scripts>
Options ExecCGI Indexes
DirectoryIndex index.html index.wsgi index.cgi
AddHandler cgi-script .cgi
AddHandler wsgi-script .wsgi
Require all granted
</Directory>
DirectoryIndex only works for a WSGI application that returns a
single page when the URL maps directly to the directory itself —
it is not invoked when the request URL has additional path
information beyond the directory mount point. It cannot be used to
route a complex multi-URL application.
Per-directory configuration via .htaccess
AddHandler and SetHandler can be placed in a .htaccess
file inside the directory in question, provided AllowOverride
FileInfo is set on the parent directory (or wider) and Options
ExecCGI is permitted there:
Alias /site/ /usr/local/wsgi/site/
<Directory /usr/local/wsgi/site>
AllowOverride FileInfo
Options ExecCGI MultiViews Indexes
MultiviewsMatch Handlers
Require all granted
</Directory>
The .htaccess file inside /usr/local/wsgi/site can then
contain:
DirectoryIndex index.html index.wsgi index.cgi
AddHandler cgi-script .cgi
AddHandler wsgi-script .wsgi
Note that WSGIScriptAlias itself cannot be used in
.htaccess; only the per-directory directives are valid there.
See Configuration Issues for which mod_wsgi directives are
allowed in .htaccess.
Mounting a script-extension WSGI application at the site root
When using AddHandler with WSGI scripts identified by extension,
the only way to make the application appear at the site root is via
mod_rewrite. To make site.wsgi (in the document root)
respond to every URL on the virtual host:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ /site.wsgi/$1 [QSA,PT,L]
The [PT] (pass-through) flag is required so that the rewrite is
re-resolved through the alias and handler phases.
A side effect of this rewrite is that the WSGI SCRIPT_NAME
environment variable is /site.wsgi rather than / — which
will leak into any URLs the application generates from
SCRIPT_NAME. Many frameworks expose a configuration option to
override the mount point. As a fallback, wrap the application
inside the script file to rewrite SCRIPT_NAME before delegating:
import posixpath
def _application(environ, start_response):
# The original application.
...
def application(environ, start_response):
environ['SCRIPT_NAME'] = posixpath.dirname(environ['SCRIPT_NAME'])
if environ['SCRIPT_NAME'] == '/':
environ['SCRIPT_NAME'] = ''
return _application(environ, start_response)
This is an advanced pattern; in most cases WSGIScriptAlias /
plus Alias directives for static files (described in Hosting
Of Static Files above) is the simpler and recommended approach.