Apache HTTPS Reverse Proxy for Uvicorn (Complete Setup)

# Apache HTTPS Reverse Proxy for Uvicorn: Complete Setup Guide

Uvicorn is a fast, ASGI-based Python web server that powers frameworks like FastAPI, Starlette, and even Flask (via the ASGI adapter pattern). Developers reach for it because it is lightweight, production-capable, and easy to run behind a real web server. But Uvicorn is not designed to face the public internet directly — it does not terminate TLS, and it should not be exposed on a public port without a hardened front-end.

The standard pattern is to run Uvicorn on a local port and place Apache in front of it as a **reverse proxy**. Apache terminates HTTPS, manages certificates, and forwards plain HTTP traffic to Uvicorn on `127.0.0.1`. This guide walks through that entire setup end-to-end: installing Uvicorn under a dedicated non-root user, creating the Apache virtual host, enabling `mod_proxy` and `mod_ssl`, obtaining a Let’s Encrypt certificate with Certbot, and automating renewal.

> **Recency and scope note:** The commands below target **Debian and Ubuntu** systems that use `apt` and the `/etc/apache2/` configuration tree. On RHEL, CentOS, Rocky, or AlmaLinux, the equivalent paths are `/etc/httpd/` and the module/utility names differ (`httpd`, `dnf`, `semanage`). Always test configuration changes in a staging environment first, and keep a backup of any file you edit.

## Why Use a Reverse Proxy in Front of Uvicorn?

Running an ASGI server directly on port 443 is technically possible, but it is almost never the right architecture for production. A reverse proxy gives you several concrete benefits:

– **TLS termination:** Apache handles the cryptographic handshake and certificate renewal. Your Python application never has to deal with private keys.
– **Security isolation:** Uvicorn binds to `127.0.0.1`, so the app port is never reachable from the outside world. Only Apache exposes a public listener.
– **Request control:** Apache can enforce rate limits, rewrite rules, custom headers, and access lists before traffic ever reaches your application.
– **Process supervision:** You can restart or reload Uvicorn independently of Apache, and vice versa.

## Step 1: Install Uvicorn and Create a Dedicated User

Best practice is to run your Python application under a non-root user so that a compromise of the app process does not immediately grant root privileges. The source tutorial uses a user named `mysiteuser`; you can pick any name you like.

### 1.1 Install the Python `venv` package

On Debian/Ubuntu, the `venv` module is shipped separately from the base interpreter:

“`bash
apt install python3-venv
“`

> The source tutorial references `python-venv`. On modern systems the package is `python3-venv`. Confirm with `apt-cache search python3-venv` if the install fails.

### 1.2 Create a non-root application user

Run these as root (or with `sudo`):

“`bash
useradd -m mysiteuser
passwd mysiteuser
“`

This creates a home directory at `/home/mysiteuser` and sets a password.

### 1.3 Log in as the new user and create a virtual environment

Switch to the application user over SSH:

“`bash
ssh mysiteuser@123.123.123.123
“`

Inside the user’s home directory, create an isolated Python environment:

“`bash
python3 -m venv ./venv
“`

Then install Uvicorn into that environment:

“`bash
./venv/bin/pip install uvicorn
“`

Using a virtual environment keeps your application dependencies separate from the system Python packages and makes deployments reproducible.

## Step 2: Write a Minimal Application Module

You need a Python module that exposes an ASGI/WSGI application object. The source tutorial uses a tiny Flask example named `mymodule.py`:

“`python
#!/bin/python

from flask import Flask

app = Flask(__name__)

@app.route(‘/’)
def hello():
return ‘Hello, World!’

if __name__ == ‘__main__’:
uvicorn.run(app, host=’0.0.0.0′, port=3000)
“`

For this to run, Flask itself must also be installed in the virtual environment:

“`bash
./venv/bin/pip install flask
“`

> In a real deployment you would typically bind to `127.0.0.1` rather than `0.0.0.0` so that Uvicorn is only reachable locally — the reverse proxy will be the only thing connecting to it.

### 2.1 Start Uvicorn

Launch the server with the reload flag (useful during development; remove `–reload` for production):

“`bash
./venv/bin/uvicorn mymodule:app –reload
“`

Here `mymodule` is the filename (without the `.py` extension) and `app` is the application object inside it. By default Uvicorn listens on `127.0.0.1:8000`; you can override host and port with `–host 127.0.0.1 –port 3000`.

For production you would normally run Uvicorn behind a process manager such as `systemd` or `supervisor` rather than in an interactive shell, so it survives reboots and crashes.

## Step 3: Install and Configure Apache

If Apache is not already installed:

“`bash
apt install apache2
“`

Apache’s per-site configuration files live under `/etc/apache2/sites-available/`. Create a new virtual host file, for example `/etc/apache2/sites-available/mysite.conf`.

A minimal starting point is an empty port-80 virtual host that you will fill in during the integration step:

“`apache


“`

Enable the site and restart Apache:

“`bash
a2ensite mysite.conf
systemctl restart apache2
“`

## Step 4: Enable the Proxy and SSL Modules

Apache needs several modules to act as a TLS-terminating reverse proxy. Enable them with:

“`bash
a2enmod proxy proxy_http ssl headers rewrite
systemctl restart apache2
“`

The source tutorial also mentions installing `libapache2-mod-wsgi-py3`:

“`bash
apt-get install libapache2-mod-wsgi-py3
“`

> **Note:** `mod_wsgi` is only required if you intend to have Apache serve a WSGI application directly (embedding Python in the Apache worker). When you are reverse-proxying to Uvicorn over HTTP, Apache does not need `mod_wsgi` at all — it simply forwards the request. Install it only if your architecture mixes both approaches.

## Step 5: Configure Apache as a Reverse Proxy with SSL

Edit `mysite.conf` to turn the virtual host into an HTTPS front-end. The following block is adapted from the source tutorial’s configuration:

“`apache

SSLEngine On
SSLProxyEngine On
SSLProxyVerify none
SSLProxyCheckPeerCN off
SSLProxyCheckPeerName off
SSLCertificateFile “/opt/ssl/server.crt”
SSLCertificateKeyFile “/opt/ssl/server.key”

ProxyRequests Off
ProxyPreserveHost On

ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/

“`

What each directive does:

– **`SSLEngine On`** — Enables TLS on this virtual host.
– **`SSLProxyEngine On`** — Allows Apache to use SSL when proxying to an SSL backend (not strictly required when proxying to plain HTTP on localhost, but harmless to include).
– **`SSLProxyVerify none` / `SSLProxyCheckPeerCN off` / `SSLProxyCheckPeerName off`** — Disables upstream certificate verification. This is appropriate when proxying to a local, non-TLS backend. If you ever proxy to an HTTPS upstream, you should re-enable verification.
– **`ProxyRequests Off`** — Prevents Apache from acting as a forward (open) proxy. Always leave this off for reverse-proxy setups.
– **`ProxyPreserveHost On`** — Forwards the original `Host` header to Uvicorn so your app sees the real domain name.
– **`ProxyPass / http://127.0.0.1:3000/`** — Forwards every request path to Uvicorn on port 3000.
– **`ProxyPassReverse`** — Rewrites `Location`, `Content-Location`, and `URI` headers in responses so redirects generated by the backend point at the public URL.

The certificate paths (`/opt/ssl/server.crt` and `/opt/ssl/server.key`) must point to wherever you store your TLS certificate and private key. Make sure the `apache2`/`www-data` user can read them.

Test the configuration for syntax errors before reloading:

“`bash
apachectl configtest
systemctl reload apache2
“`

## Step 6: Obtain a Let’s Encrypt Certificate with Certbot

Rather than managing certificates by hand, the source tutorial uses **Certbot** to obtain a free certificate from Let’s Encrypt. Install it into the same virtual environment:

“`bash
./venv/bin/pip install certbot
“`

Then request a certificate. The source tutorial uses a manual DNS challenge:

“`bash
./venv/bin/certbot certonly -d mysite.conf
–manual –preferred-challenges dns
–server https://acme-v02.api.letsencrypt.org/directory
–config-dir /opt/ssl –work-dir /opt/ssl –logs-dir /opt/ssl
“`

A few important points:

– **Replace `-d mysite.conf` with your actual domain name** (for example, `-d example.com -d www.example.com`). The `-d` flag expects a domain, not a filename.
– **`–manual –preferred-challenges dns`** requires you to publish a `_acme-challenge` TXT record during issuance. This is useful when you cannot run an HTTP-01 challenge, but it is interactive and not ideal for automated renewal.
– **`–config-dir /opt/ssl`** stores the issued certificates under `/opt/ssl` instead of the default `/etc/letsencrypt`.

After issuance, update your Apache `SSLCertificateFile` and `SSLCertificateKeyFile` paths to point at the Let’s Encrypt files (for example `/opt/ssl/live/example.com/fullchain.pem` and `/opt/ssl/live/example.com/privkey.pem`) and reload Apache.

## Step 7: Automate Certificate Renewal

Let’s Encrypt certificates expire after 90 days. The source tutorial adds a cron job to re-run the Certbot command monthly. Open the user crontab:

“`bash
crontab -e
“`

Add the renewal entry (the source example runs at midnight on the first of each month):

“`cron
0 0 1 * * ./venv/bin/certbot certonly -d mysite.conf
–manual –preferred-challenges dns
–server https://acme-v02.api.letsencrypt.org/directory
–config-dir /opt/ssl –work-dir /opt/ssl –logs-dir /opt/ssl
“`

> **Caveat:** Because this uses `–manual` with a DNS challenge, renewal is **not** fully automatic — it will pause and wait for you to set the TXT record. For unattended renewal, switch to a DNS plugin (such as `certbot-dns-cloudflare`) that can update records via API, or use the HTTP-01 challenge with the `–webroot` or `–apache` authenticator. You should also append a `–deploy-hook` command to reload Apache after a successful renewal:

“`cron
–deploy-hook “systemctl reload apache2”
“`

## Troubleshooting Checklist

If your site returns an error after following the steps, check these in order:

1. **Is Uvicorn actually running?** On the app host, `curl http://127.0.0.1:3000/` should return your app’s response. If not, Uvicorn is down or bound to the wrong host/port.
2. **Are the Apache proxy modules loaded?** Run `apache2ctl -M | grep proxy` and confirm `proxy_module` and `proxy_http_module` appear.
3. **Does the config parse?** `apache2ctl configtest` reports syntax errors and missing modules.
4. **Do the certificate paths exist and are they readable?** Apache will refuse to start if it cannot read the key file. Check permissions on `/opt/ssl`.
5. **Is the firewall open on 443?** On cloud providers, remember both the OS firewall (`ufw`, `iptables`, `firewalld`) and the provider’s security group.

## Wrapping Up

Putting Apache in front of Uvicorn gives you a production-ready stack: Uvicorn handles the ASGI workload locally, Apache terminates HTTPS and manages certificates, and the two communicate over a loopback connection that is invisible to the outside world. The pieces — `mod_proxy`, `mod_ssl`, Certbot, and a cron renewal job — are all well-established and widely documented, which makes this architecture easy to operate and debug.

Once the basic proxy is working, the natural next steps are to supervise Uvicorn with `systemd`, add a `systemd` socket or `gunicorn -k uvicorn.workers.UvicornWorker` for multi-process scaling, and tighten the Apache configuration with security headers and HTTP/2. Those refinements build directly on the foundation described above.

*This guide is based on a tutorial originally published March 31, 2025. Package names, module behavior, and Let’s Encrypt workflows can change between releases — verify against the current official documentation for your distribution before deploying to production.*