Virtual Environments

This page covers using Python virtual environments with mod_wsgi.

A Python virtual environment is a self-contained directory holding a specific Python interpreter plus its own set of installed packages. Using one keeps the dependencies of a WSGI application isolated from the system Python and from other applications on the same host. It is strongly recommended that you always run a WSGI application out of a virtual environment rather than against the system Python.

A virtual environment is required when:

  • Multiple WSGI applications hosted by the same Apache need different versions of the same package.

  • Distinct mod_wsgi daemon process groups host WSGI applications for different users, and each user needs to manage their own packages.

Either python -m venv (Python’s built-in virtual-environment support) or uv venv (a fast modern alternative from the uv package manager) is suitable for creating the environment. The older virtualenv package also still works and you may still encounter it; virtualenvwrapper is no longer actively maintained but similarly works if you already use it.

How you point mod_wsgi at the virtual environment depends on the deployment shape — daemon vs. embedded mode, single vs. multiple WSGI applications. The common scenarios are covered below.

Location of the Virtual Environment

Before configuring mod_wsgi, find out the on-disk path of your virtual environment. Activate it from a shell and run:

python -c 'import sys; print(sys.prefix)'

This prints the path you will use when pointing mod_wsgi at it. The examples on this page assume virtual environments are stored under /usr/local/venvs, so a specific environment might be at:

/usr/local/venvs/example

This must be the root directory of the virtual environment — the one containing bin/ and lib/ — not the path to the python executable inside it. Pointing mod_wsgi at /usr/local/venvs/example/bin/python will not work.

The user Apache runs your code as must be able to read the virtual environment’s files. On some Linux distributions a user’s home directory is not accessible to other users, so consider locating WSGI application code and the virtual environment somewhere outside /home/<user>/ rather than relaxing the home directory’s permissions.

Virtual Environment and Python Version

The virtual environment used with mod_wsgi must have been created from the same Python installation that mod_wsgi was built against. A virtual environment cannot be used to make mod_wsgi use a different Python version, or even a different installation of the same version.

For example, you cannot make a mod_wsgi built for Python 3.10 use a virtual environment created from Python 3.12. The Python library mod_wsgi links against is baked into mod_wsgi.so at build time. mod_wsgi embeds Python directly; it does not run a python command-line program, so the choice of Python is fixed when mod_wsgi is built.

Even when the Python version matches, two installations of the same version may have been compiled with different options and the resulting ABIs can differ in subtle ways. Mixing them is not safe.

If you need to switch Python version or installation, rebuild mod_wsgi against the new Python.

Daemon Mode (Single Application)

The preferred way to set up mod_wsgi is to run each WSGI application in its own daemon process group. A typical configuration is:

WSGIDaemonProcess myapp

WSGIProcessGroup myapp
WSGIApplicationGroup %{GLOBAL}

WSGIScriptAlias / /some/path/project/myapp.wsgi

<Directory /some/path/project>
    Require all granted
</Directory>

WSGIDaemonProcess defines the daemon process group; WSGIProcessGroup selects it for this application. Because only one application runs in this daemon process group, WSGIApplicationGroup %{GLOBAL} forces it into the main Python interpreter context of each daemon process, which avoids issues with C extension modules that don’t tolerate running in Python sub-interpreters.

To use a virtual environment, add the python-home option to WSGIDaemonProcess:

WSGIDaemonProcess myapp python-home=/usr/local/venvs/myapp

All Python packages the application needs are then installed into that virtual environment.

Daemon Mode (Multiple Applications)

If multiple WSGI applications run in a single daemon process group (rather than each having its own — the recommended setup), the configuration looks something like:

WSGIDaemonProcess myapps

WSGIProcessGroup myapps

WSGIScriptAlias /myapp3 /some/path/project/myapp3.wsgi
WSGIScriptAlias /myapp2 /some/path/project/myapp2.wsgi
WSGIScriptAlias / /some/path/project/myapp1.wsgi

<Directory /some/path/project>
    Require all granted
</Directory>

Or, if mounting the directory directly:

WSGIDaemonProcess myapps

WSGIProcessGroup myapps

WSGIScriptAlias / /some/path/project/

<Directory /some/path/project>
    Require all granted
</Directory>

WSGIApplicationGroup is deliberately omitted. Without it, each WSGI application runs in its own Python sub-interpreter context inside the daemon process. Many WSGI frameworks — Django is the canonical example — do not support multiple instances of an application running in the same Python interpreter context concurrently, so per-application sub-interpreters are necessary.

If all applications can share a single virtual environment, use python-home exactly as in the single-application case:

WSGIDaemonProcess myapps python-home=/usr/local/venvs/myapps

Because the environment is shared, all applications must agree on the version of any given package.

If each application needs its own virtual environment, python-home alone is not enough — only one python-home value is allowed per daemon process group. In that case, activate the per-application virtual environment from inside the WSGI script itself.

If your virtual environment was created with uv venv or with virtualenv, it includes an activate_this.py script that performs a complete activation: it adds the virtual environment’s site-packages to the front of sys.path, sets sys.prefix to the virtual environment root, and sets VIRTUAL_ENV in the process environment. At the top of the WSGI script, before any other imports:

python_home = '/usr/local/venvs/myapp1'

activate_this = python_home + '/bin/activate_this.py'
exec(open(activate_this).read(), dict(__file__=activate_this))

Set python_home differently for each application’s WSGI script.

If your virtual environment was created with python -m venv, no activate_this.py script is provided and you must add the site-packages directory to sys.path manually:

python_home = '/usr/local/venvs/myapp1'

import sys
import site

python_version = '.'.join(map(str, sys.version_info[:2]))
site_packages = python_home + '/lib/python%s/site-packages' % python_version
site.addsitedir(site_packages)

Whichever activation method is used, the underlying Python installation remains in view — anything installed against it is still importable from the WSGI application. This can lead to surprises: a missing entry in your requirements.txt may not produce an ImportError if the package happens to be installed against the underlying Python.

To prevent this, still set python-home on WSGIDaemonProcess but point it at an empty virtual environment which has no packages installed:

WSGIDaemonProcess myapps python-home=/usr/local/venvs/empty

This makes the underlying Python that empty virtual environment rather than your system Python, so per-application activations cleanly override it.

For the manual site.addsitedir() path the empty-environment trick is also needed for sys.path ordering: site.addsitedir() adds entries to the end of sys.path, so anything installed into the python-home virtual environment would otherwise take precedence over the per-application virtual environment. Keeping the python-home virtual environment empty keeps that ordering harmless. activate_this.py already adds entries to the front of sys.path, so this concern does not apply to that path.

Where possible, prefer giving each WSGI application its own daemon process group (the previous section); that avoids in-script activation entirely.

Embedded Mode (Single Application)

Running a single WSGI application in embedded mode is similar to the daemon-mode-single-application case but without the WSGIDaemonProcess and WSGIProcessGroup directives:

WSGIScriptAlias / /some/path/project/myapp.wsgi

WSGIApplicationGroup %{GLOBAL}

<Directory /some/path/project>
    Require all granted
</Directory>

WSGIApplicationGroup %{GLOBAL} still forces the application into the main Python interpreter context of each Apache worker process, for the same reason as before.

To point at a virtual environment in embedded mode, use the WSGIPythonHome directive:

WSGIPythonHome /usr/local/venvs/myapp

Note that WSGIPythonHome applies to the whole Apache instance, not to a single VirtualHost. If your WSGI application is configured inside a VirtualHost, the WSGIPythonHome directive must still go at the server-config level, outside any VirtualHost block.

Embedded Mode (Multiple Applications)

Running multiple WSGI applications in embedded mode mirrors the multiple-applications-in-one-daemon-process-group case. Each application runs in its own Python sub-interpreter context to avoid the framework-multiplicity issue.

Mounting each application explicitly:

WSGIScriptAlias /myapp3 /some/path/project/myapp3.wsgi
WSGIScriptAlias /myapp2 /some/path/project/myapp2.wsgi
WSGIScriptAlias / /some/path/project/myapp1.wsgi

<Directory /some/path/project>
    Require all granted
</Directory>

Or mounting a directory of WSGI scripts:

WSGIScriptAlias / /some/path/project/

<Directory /some/path/project>
    Require all granted
</Directory>

If all applications share a single virtual environment, use WSGIPythonHome to point at it:

WSGIPythonHome /usr/local/venvs/myapps

As before, WSGIPythonHome must be at the server-config level, outside any VirtualHost block.

If each application needs its own virtual environment, activate it from the WSGI script using the site.addsitedir() approach shown earlier for daemon mode, and set WSGIPythonHome to an empty virtual environment so the underlying Python’s site-packages does not interfere.

Adding Additional Module Directories

The python-home option to WSGIDaemonProcess and the WSGIPythonHome directive are the right way to point at a virtual environment. They are not for adding other directories to Python’s module search path.

If you do need to add other directories — for example a directory containing application modules that aren’t installed as a package — use python-path for daemon mode:

WSGIDaemonProcess myapp python-path=/some/path/project

This is added in addition to python-home.

For embedded mode, use the WSGIPythonPath directive:

WSGIPythonPath /some/path/project

This is added in addition to WSGIPythonHome.

Either form accepts multiple directories, separated by : on UNIX-like systems and ; on Windows.

If you are activating a virtual environment from inside a WSGI script and need additional directories, modify sys.path directly in the WSGI script.

A note on legacy practice: python-path and WSGIPythonPath were sometimes used to bolt the site-packages directory of a virtual environment onto Python’s search path. Don’t do that — use the python-home / WSGIPythonHome mechanism above instead.