Python and Apache don’t naturally speak the same language out of the box, and I remember being confused early on about why my Flask app wouldn’t just “run” when I dropped it into the DocumentRoot. The missing piece is WSGI — the interface that lets Apache hand off requests to Python code. In this guide, I’ll walk through installing and configuring Python with Apache using mod_wsgi, the most common and reliable approach.
Understanding How Python Works with Apache
Python web frameworks like Flask and Django follow the WSGI (Web Server Gateway Interface) specification, a standard that defines how a web server communicates with a Python application. Apache doesn’t understand WSGI natively, so it needs a bridge: mod_wsgi.
mod_wsgi embeds a Python interpreter inside Apache (or runs it in separate daemon processes) and handles translating HTTP requests into calls your Python application understands, then converts the application’s response back into an HTTP response.
There are two operating modes:
- Embedded mode – Python runs inside Apache’s own worker processes. Simpler but less isolated.
- Daemon mode – Python runs in separate, dedicated processes managed by mod_wsgi. Recommended for production because it isolates your app from Apache’s process lifecycle and allows independent restarts.
Prerequisites
- A Linux server with Apache 2.4+ installed
- Root or sudo access
- Python 3.8+ installed (
python3 --version) - pip and venv available
- A Python web application (Flask or Django example used below)
Step 1: Install Python and Development Tools
Debian/Ubuntu:
sudo apt update
sudo apt install python3 python3-pip python3-venv python3-dev apache2 apache2-dev libapache2-mod-wsgi-py3
CentOS/RHEL:
sudo dnf install python3 python3-pip python3-devel httpd httpd-devel
sudo dnf install python3-mod_wsgi
Step 2: Enable mod_wsgi
Debian/Ubuntu:
sudo a2enmod wsgi
sudo systemctl restart apache2
Verify it loaded:
apache2ctl -M | grep wsgi
CentOS/RHEL:
Ensure this line exists in your Apache configuration:
LoadModule wsgi_module modules/mod_wsgi.so
Then restart:
sudo systemctl restart httpd
Step 3: Set Up a Python Virtual Environment
Isolating dependencies in a virtual environment is essential for production stability:
sudo mkdir -p /var/www/myflaskapp
cd /var/www/myflaskapp
python3 -m venv venv
source venv/bin/activate
pip install flask
Step 4: Create a Simple Flask Application
nano /var/www/myflaskapp/app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def home():
return "Hello from Flask running on Apache with mod_wsgi!"
if __name__ == "__main__":
app.run()
Step 5: Create the WSGI Entry Point
nano /var/www/myflaskapp/myflaskapp.wsgi
#!/usr/bin/python3
import sys
import logging
logging.basicConfig(stream=sys.stderr)
sys.path.insert(0, "/var/www/myflaskapp/")
from app import app as application
The variable must be named application — this is the convention mod_wsgi looks for.
Step 6: Configure the Apache Virtual Host
ServerName myflaskapp.com
WSGIDaemonProcess myflaskapp python-home=/var/www/myflaskapp/venv python-path=/var/www/myflaskapp
WSGIProcessGroup myflaskapp
WSGIScriptAlias / /var/www/myflaskapp/myflaskapp.wsgi
Require all granted
ErrorLog ${APACHE_LOG_DIR}/myflaskapp_error.log
CustomLog ${APACHE_LOG_DIR}/myflaskapp_access.log combined
WSGIDaemonProcess with python-home pointing to the virtual environment ensures Apache uses your isolated Python packages rather than the system-wide installation.
Enable the site and restart:
sudo a2ensite myflaskapp.conf
sudo apachectl configtest
sudo systemctl restart apache2
Visit http://myflaskapp.com — you should see the Flask response.
Configuring Django with Apache
Django follows a similar pattern but with a slightly different WSGI entry point structure, since Django generates wsgi.py automatically.
Step 1: Set Up the Django Project
cd /var/www
python3 -m venv django_env
source django_env/bin/activate
pip install django
django-admin startproject myproject
Step 2: Configure the Apache Virtual Host
ServerName mydjangoapp.com
WSGIDaemonProcess myproject python-home=/var/www/django_env python-path=/var/www/myproject
WSGIProcessGroup myproject
WSGIScriptAlias / /var/www/myproject/myproject/wsgi.py
Require all granted
Alias /static/ /var/www/myproject/static/
Require all granted
ErrorLog ${APACHE_LOG_DIR}/mydjangoapp_error.log
CustomLog ${APACHE_LOG_DIR}/mydjangoapp_access.log combined
Don’t forget to run collectstatic before going live:
python manage.py collectstatic
Real-World Use Cases
- Data-driven web dashboards – Python’s data science ecosystem (pandas, matplotlib) pairs naturally with Flask/Django apps serving visualizations.
- Internal admin tools – Django’s built-in admin panel is a common choice for internal business applications deployed behind Apache.
- REST APIs – Flask or Django REST Framework backends serving mobile or JavaScript frontend applications.
- Machine learning model serving – Lightweight Flask apps wrapping ML models for inference, deployed behind Apache for SSL and load balancing.
Common Mistakes to Avoid
- Forgetting
python-homein WSGIDaemonProcess – Without it, Apache uses the system Python, missing your virtual environment’s packages, leading toModuleNotFoundError. - Naming the WSGI callable incorrectly – It must be named
application, notapp, in the.wsgifile (even though your Flask variable can be namedappinsideapp.py, the imported alias in the wsgi file must beapplication). - Running the dev server in production –
app.run()or Django’srunserverare not production-grade; always use mod_wsgi (or Gunicorn behind a proxy) instead. - Mixing Python 2 and Python 3 mod_wsgi packages – Ensure you install
libapache2-mod-wsgi-py3specifically, not the Python 2 version. - Incorrect file permissions – The Apache user needs read access to your application directory and virtual environment.
Security Best Practices
- Set
DEBUG = Falsein Django’s settings for production; leaving debug mode on exposes sensitive stack traces and environment details. - Store secrets (
SECRET_KEY, database credentials) in environment variables, not hardcoded in source files. - Restrict
ALLOWED_HOSTSin Django to your actual domain(s). - Run WSGIDaemonProcess under a dedicated, non-privileged user rather than the default Apache user where isolation matters.
- Keep Python, Flask/Django, and all dependencies updated; use
pip list --outdatedand tools likepip-auditregularly.
Performance Optimization Tips
- Use daemon mode (not embedded mode) for better process isolation and easier scaling.
- Tune
processesandthreadsinWSGIDaemonProcessbased on your workload:
WSGIDaemonProcess myflaskapp python-home=/var/www/myflaskapp/venv processes=4 threads=2
- Serve static files directly through Apache (
Aliasdirectives) rather than through the Python application. - Combine with the compression and caching techniques from our other Apache guides.
- Consider an alternative like Gunicorn or uWSGI behind Apache’s
mod_proxyfor very high-concurrency workloads, as some teams find this scales more predictably than mod_wsgi.
Troubleshooting
500 Internal Server Error with no clear message:
- Check
error.log:sudo tail -f /var/log/apache2/myflaskapp_error.log. mod_wsgi logs Python tracebacks here.
ModuleNotFoundError for installed packages:
- Confirm
python-homepoints to the correct virtual environment and that the package was installed inside that venv, not globally.
Static files return 404 in Django:
- Run
collectstaticand verify theAlias /static/path matchesSTATIC_ROOTinsettings.py.
Changes to code not reflecting:
- mod_wsgi daemon processes cache the application; restart Apache or touch the
.wsgifile to force a reload:
touch /var/www/myflaskapp/myflaskapp.wsgi
Frequently Asked Questions
Do I need mod_wsgi, or can I use Gunicorn instead? Both work. mod_wsgi embeds Python directly into Apache’s process management. Gunicorn runs as a separate process, with Apache acting as a reverse proxy (similar to the Rails/Puma pattern). Gunicorn behind a proxy is increasingly common for containerized deployments; mod_wsgi remains popular for traditional VPS setups already standardized on Apache.
Can I run multiple Python apps on the same Apache server? Yes, using separate WSGIDaemonProcess groups and virtual hosts, each with its own virtual environment and process isolation.
Why does my app work with python app.py but not through Apache? The built-in Flask/Django development server behaves differently from mod_wsgi’s process model — typically the issue is either the python-home path, file permissions, or a missing environment variable that was set locally but not in Apache’s environment.
Is mod_wsgi actively maintained? Yes, mod_wsgi continues to be maintained and is a stable, production-proven choice for serving Python WSGI applications through Apache.
Summary and Key Takeaways
- Python applications communicate with Apache through the WSGI interface, bridged by mod_wsgi.
- Always use a virtual environment and point
WSGIDaemonProcessto it viapython-home. - Daemon mode is preferred over embedded mode for production isolation and stability.
- Django requires additional static file configuration (
collectstatic+Alias) that Flask doesn’t need out of the box. - Never run Flask’s or Django’s built-in development server in production — mod_wsgi (or Gunicorn behind a proxy) is the correct approach.
- Check the Apache error log first whenever something goes wrong; Python tracebacks are logged there.