Deploy FastAPI with systemd & Apache Reverse Proxy on Ubuntu

Published 2026-09-06 · How-to

Install FastAPI and Uvicorn Create systemd service * Configure Apache proxy

Install FastAPI and Uvicorn

First, make sure the system’s Python environment is clean and isolated. Ubuntu ships with Python 3, so you can create a virtual environment in the project directory and keep the dependencies separate from the OS packages.

# Create a project folder
mkdir -p /opt/fastapi-demo && cd /opt/fastapi-demo

# Set up an isolated venv
python3 -m venv venv
source venv/bin/activate

# Install FastAPI and Uvicorn
pip install --upgrade pip
pip install fastapi uvicorn

After the installation finishes, verify the import works:

>>> from fastapi import FastAPI
>>> import uvicorn
>>> print(FastAPI, uvicorn.__version__)
<class 'fastapi.applications.FastAPI'> 0.24.0

Now add a minimal “hello world” endpoint so the service has something to run:

# app/main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "FastAPI is up"}

Leave the virtual environment active while you test the server locally:

uvicorn app.main:app --host 127.0.0.1 --port 8000

If you see {"message":"FastAPI is up"} at http://127.0.0.1:8000/, the core stack is ready. The next steps will wrap this process in a systemd unit and expose it through Apache’s reverse proxy.

Create a systemd service for the FastAPI app

Place the service file in /etc/systemd/system. Using a dedicated user isolates the app from the rest of the system and avoids accidental privilege escalation.

# Create a low‑privilege user for the app
sudo useradd -r -s /usr/sbin/nologin fastapi

# Copy the virtual environment into the user’s home (optional)
sudo mkdir -p /var/www/fastapi-demo
sudo cp -r /opt/fastapi-demo/* /var/www/fastapi-demo/
sudo chown -R fastapi:fastapi /var/www/fastapi-demo

Now write the unit file. The ExecStart line launches Uvicorn from the virtual environment, binding to a Unix socket that Apache will forward to.

sudo tee /etc/systemd/system/fastapi.service > /dev/null <<'EOF'
[Unit]
Description=FastAPI application
After=network.target

[Service]
User=fastapi
Group=fastapi
WorkingDirectory=/var/www/fastapi-demo
Environment="PATH=/var/www/fastapi-demo/venv/bin"
ExecStart=/var/www/fastapi-demo/venv/bin/uvicorn main:app \
    --uds /run/fastapi.sock \
    --workers 4 \
    --log-level info

Restart=on-failure
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target
EOF

Reload systemd, enable the service, and start it:

sudo systemctl daemon-reload
sudo systemctl enable --now fastapi.service

Verify that the socket appears and the process is running:

sudo ss -xl | grep fastapi.sock
sudo systemctl status fastapi.service

If the service fails, inspect the logs with journalctl -u fastapi.service. Once the socket is live, Apache can proxy to /run/fastapi.sock without exposing a raw port.

Configure Apache as a reverse proxy

Enable the required Apache modules and create a dedicated virtual host that forwards traffic to the Uvicorn socket. Start by loading the proxy stack:

sudo a2enmod proxy proxy_http proxy_balancer lbmethod_byrequests
sudo systemctl restart apache2

Next, add a site definition under /etc/apache2/sites-available/fastapi.conf. The file points Apache at the Unix socket created by the systemd service (see the previous section). Using a socket avoids an extra TCP hop and keeps the connection local.

<VirtualHost *:80>
    ServerName api.example.com

    # Forward all requests to the FastAPI socket
    ProxyPreserveHost On
    ProxyPass / unix:/run/fastapi.sock|http://localhost/
    ProxyPassReverse / unix:/run/fastapi.sock|http://localhost/

    # Optional: serve static files directly
    Alias /static /var/www/fastapi-demo/static
    <Directory /var/www/fastapi-demo/static>
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/fastapi_error.log
    CustomLog ${APACHE_LOG_DIR}/fastapi_access.log combined
</VirtualHost>

Activate the configuration and disable the default site:

sudo a2ensite fastapi.conf
sudo a2dissite 000-default.conf
sudo systemctl reload apache2

At this point Apache listens on port 80 and proxies every request to the Uvicorn process managed by systemd. Verify the setup with curl -I http://api.example.com/. A 200 OK response confirms the reverse proxy is operational. If you need HTTPS, replace the <VirtualHost :80> block with <VirtualHost :443> and add the usual SSLEngine on directives, then reload Apache again.

Enable and start the systemd service

After placing fastapi.service in /etc/systemd/system, tell systemd to read the new file and then bring the unit up. The sequence is deliberately simple so you can repeat it on any server without hunting for hidden steps.

# Reload unit files so systemd sees the new service definition
sudo systemctl daemon-reload

# Enable the service to start automatically at boot
sudo systemctl enable fastapi.service

# Start it right now
sudo systemctl start fastapi.service

# Verify it’s running and listening on the socket you defined
sudo systemctl status fastapi.service

If the service fails to start, journalctl -u fastapi.service -b will show the most recent logs, which is invaluable during the first deployment. A common pitfall is an incorrect WorkingDirectory or a missing ExecStart path; double‑check that the virtual‑environment activation script is referenced explicitly, e.g. ExecStart=/opt/fastapi-demo/venv/bin/uvicorn main:app --uds /run/fastapi.sock.

For production stability add a restart policy in the unit file:

[Service]
Restart=on-failure
RestartSec=5

With the service enabled and started, the socket file appears under /run/fastapi.sock. Apache’s reverse‑proxy configuration can now forward HTTP traffic to this Unix socket, completing the deployment pipeline.

Test the deployment and troubleshoot common issues

After the services are up, verify the end‑to‑end flow with a simple request against the public URL.

# Replace example.com with your domain or IP
curl -I http://example.com/health

A 200 OK and the JSON payload you defined in the FastAPI route confirm that Apache successfully proxied to Uvicorn. If the request hangs or returns 502 Bad Gateway, start digging with the service managers.

# Check that the systemd unit is active
systemctl status fastapi.service

# Show the last lines of the app’s journal
journalctl -u fastapi.service -n 20 --no-pager

Typical failures:

SymptomLikely causeQuick fix
502 Bad GatewayApache cannot reach the Unix socketEnsure the socket path in fastapi.service matches the one in fastapi.conf; set chmod 660 and chown to the fastapi user.
Permission denied (socket)Socket owned by root or wrong groupAdd Group=fastapi and User=fastapi in the service file, then systemctl daemon-reload && systemctl restart fastapi.
No response from /healthUvicorn process crashed or never startedLook for traceback in journalctl; missing dependencies in the venv are a common culprit—activate the venv and run uvicorn app:app manually.
Apache returns 404VirtualHost not enabled or mis‑nameda2ensite fastapi.conf && systemctl reload apache2.
Finally, confirm that the firewall allows port 80/443:

sudo ufw status | grep '80\|443'

If the ports are blocked, open them with ufw allow http and ufw allow https. Once all checks pass, the deployment is ready for production traffic.

FAQ

How do I create a systemd service file for a FastAPI application?

Create a unit file in /etc/systemd/system, e.g., fastapi.service. Set Description, After=network.target, and specify User and Group. In the [Service] section, use ExecStart=/usr/bin/uvicorn main:app --host 127.0.0.1 --port 8000 --workers 4 and set Restart=on-failure. Optionally add EnvironmentFile=/etc/default/fastapi for env vars. Finally, reload systemd (systemctl daemon-reload), enable the service (systemctl enable fastapi), and start it (systemctl start fastapi). Check status with systemctl status fastapi.

What Apache configuration is needed to reverse‑proxy to a FastAPI app running on localhost:8000?

Enable proxy modules: a2enmod proxy proxy_http. Then create a VirtualHost (e.g., /etc/apache2/sites-available/fastapi.conf) with ServerName yourdomain.com. Inside, add ProxyPreserveHost On, ProxyPass / http://127.0.0.1:8000/, and ProxyPassReverse / http://127.0.0.1:8000/. Optionally set RequestHeader set X-Forwarded-Proto "https" for HTTPS. Enable the site (a2ensite fastapi) and reload Apache (systemctl reload apache2). This forwards all incoming traffic to the FastAPI process managed by systemd.

How can I serve HTTPS with Apache while proxying to FastAPI?

Obtain an SSL certificate (Let's Encrypt via certbot is common). After installing certbot, run certbot --apache -d yourdomain.com. Certbot will configure the <VirtualHost *:443> block with SSLCertificateFile and SSLCertificateKeyFile directives. Keep the ProxyPass and ProxyPassReverse lines inside this block. Ensure SSLProxyEngine on is set to allow Apache to forward HTTPS traffic to the backend. Restart Apache to apply changes. The client connections are encrypted, while the internal proxy to FastAPI can remain HTTP.

What environment variables should I set for a FastAPI service managed by systemd?

Typical variables include APP_ENV=production, LOG_LEVEL=info, and any secret keys (e.g., DATABASE_URL, REDIS_URL). Place them in a separate file like /etc/default/fastapi, each on its own line (KEY=value). In the service file, add EnvironmentFile=/etc/default/fastapi under the [Service] section. This keeps credentials out of the unit file and allows easy updates without editing the service definition. Remember to secure the file permissions (chmod 640) and restrict ownership to root.

How do I troubleshoot a FastAPI service that starts via systemd but returns 502 Bad Gateway from Apache?

First, verify the FastAPI process is listening on the expected port: sudo netstat -tulpn | grep 8000 or ss -ltnp. Check systemd logs (journalctl -u fastapi -f) for errors. Ensure the ExecStart command matches the actual entry point (module:app). Confirm Apache's proxy modules are enabled and the VirtualHost points to the correct IP/port. Look at Apache error logs (/var/log/apache2/error.log) for connection refusals. If SELinux/AppArmor is active, ensure it permits Apache to connect to the local port.

Related reading