Core Panel

How to Deploy FastAPI on Ubuntu with Gunicorn, Uvicorn, and Nginx

CP
Written by Core Panel Team Product and documentation team · Published Sep 26, 2026 · 12 min read
How to Deploy FastAPI on Ubuntu with Gunicorn, Uvicorn, and Nginx
On this page · 5 sections

FastAPI makes it quick to build an API, but a development command is not a production platform. A dependable public deployment needs supervised application workers, a reverse proxy, HTTPS, protected configuration, health checks, observability, and a release process you can reverse.

This guide deploys a FastAPI application on Ubuntu with Gunicorn managing Uvicorn workers, Nginx handling public traffic, and systemd supervising the service. It is a practical single-server baseline that can be extended as traffic and reliability requirements grow.

Production architecture

The request path will be:

  1. DNS sends the API domain to the Ubuntu server.
  2. Nginx terminates HTTPS, applies request limits, and forwards traffic.
  3. Gunicorn manages one or more ASGI worker processes.
  4. Uvicorn workers run the FastAPI application.
  5. systemd starts the service at boot and restarts failed processes.
  6. The application connects to PostgreSQL, Redis, or other private services.

Each layer has one clear responsibility, which makes failures easier to isolate.

Prerequisites

  • Ubuntu 22.04 or 24.04 with a non-root sudo user
  • A domain or subdomain pointing to the server
  • A FastAPI project with a locked dependency file
  • A production database and tested backup plan
  • SSH access, firewall rules, and a deployment rollback path

Update the operating system and install the base packages:

sudo apt update
sudo apt upgrade -y
sudo apt install -y python3-venv python3-pip nginx git

1. Create a dedicated service user

Do not run the API as root. Create a restricted account and application directory:

sudo adduser --system --group --home /srv/fastapi fastapi
sudo mkdir -p /srv/fastapi/app
sudo chown -R fastapi:fastapi /srv/fastapi

A separate identity limits file access and keeps ownership predictable across releases.

2. Install the application in a virtual environment

Fetch the reviewed code, create an isolated environment, and install pinned dependencies:

sudo -u fastapi git clone https://example.com/your/api.git /srv/fastapi/app
sudo -u fastapi python3 -m venv /srv/fastapi/venv
sudo -u fastapi /srv/fastapi/venv/bin/pip install --upgrade pip
sudo -u fastapi /srv/fastapi/venv/bin/pip install -r /srv/fastapi/app/requirements.txt

The runtime should include FastAPI, Gunicorn, Uvicorn, and the maintained Uvicorn worker package used by your project:

sudo -u fastapi /srv/fastapi/venv/bin/pip install gunicorn uvicorn uvicorn-worker

Pin versions in your dependency workflow and test upgrades outside production.

3. Confirm the application entry point

Assume the project exposes app from main.py:

from fastapi import FastAPI

app = FastAPI()

@app.get("/health")
def health():
    return {"status": "ok"}

The Gunicorn target is therefore main:app. If your module lives inside a package, use its full import path, such as src.main:app.

A real health endpoint should verify only the dependencies required to accept traffic. Keep it fast, avoid exposing internal details, and use a separate deeper readiness check when appropriate.

4. Store secrets outside the repository

Create a protected environment file:

sudo install -m 600 -o fastapi -g fastapi /dev/null /etc/fastapi.env
sudo nano /etc/fastapi.env

Add the settings your application expects:

APP_ENV=production
SECRET_KEY=replace-with-a-long-random-value
DATABASE_URL=postgresql://api_user:[email protected]:5432/api_db
ALLOWED_ORIGINS=https://app.example.com

Do not commit secrets to Git or place them in a world-readable service file. Restrict database roles to the permissions the application needs.

5. Test the application locally

Before creating a service, run it on the loopback interface:

sudo -u fastapi bash -c 'set -a; source /etc/fastapi.env; set +a; cd /srv/fastapi/app && /srv/fastapi/venv/bin/uvicorn main:app --host 127.0.0.1 --port 8000'

From another session, verify the health endpoint:

curl --fail http://127.0.0.1:8000/health

Stop the temporary process after the test. If imports fail, confirm the working directory, module path, environment variables, and virtual environment.

6. Create a systemd service

Create /etc/systemd/system/fastapi.service:

[Unit]
Description=FastAPI application
After=network.target

[Service]
User=fastapi
Group=www-data
WorkingDirectory=/srv/fastapi/app
EnvironmentFile=/etc/fastapi.env
RuntimeDirectory=fastapi
RuntimeDirectoryMode=0755
ExecStart=/srv/fastapi/venv/bin/gunicorn \
    --workers 3 \
    --worker-class uvicorn_worker.UvicornWorker \
    --bind unix:/run/fastapi/app.sock \
    --access-logfile - \
    --error-logfile - \
    --timeout 60 \
    main:app
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
KillSignal=SIGTERM
PrivateTmp=true
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target

Worker count is workload-specific. More workers increase concurrency but also consume memory and database connections. Measure CPU, memory, latency, and downstream capacity before changing it.

Load and start the unit:

sudo systemctl daemon-reload
sudo systemctl enable --now fastapi
sudo systemctl status fastapi
sudo journalctl -u fastapi -n 100 --no-pager

The separate uvicorn-worker package avoids relying on the older worker module bundled with Uvicorn. Check the package and Gunicorn documentation used by your locked dependency versions before upgrading.

7. Configure Nginx as the reverse proxy

Create /etc/nginx/sites-available/fastapi:

server {
    listen 80;
    listen [::]:80;
    server_name api.example.com;

    client_max_body_size 10m;

    location / {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        proxy_read_timeout 60s;
        proxy_pass http://unix:/run/fastapi/app.sock;
    }
}

Enable the site, validate the full Nginx configuration, and reload:

sudo ln -s /etc/nginx/sites-available/fastapi /etc/nginx/sites-enabled/fastapi
sudo nginx -t
sudo systemctl reload nginx

If the API uses WebSockets or server-sent events, add the required connection headers and tune timeouts for those endpoints rather than applying very long timeouts everywhere.

8. Configure proxy awareness safely

FastAPI and Uvicorn must interpret forwarded headers only from trusted proxies. In this architecture, Nginx and the application share one server, so keep the application socket private and trust only the local proxy path.

Use forwarded-header settings supported by the exact Uvicorn worker version you deploy. Avoid accepting proxy headers from arbitrary public clients because they can falsify scheme or client-address information.

9. Enable HTTPS

After DNS resolves and port 80 is reachable, install Certbot and request a certificate:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d api.example.com
sudo certbot renew --dry-run

Confirm that HTTP redirects to HTTPS and monitor automatic renewal. Consider HSTS only after verifying every relevant hostname works correctly over HTTPS.

10. Apply firewall and access controls

Expose only the services that must be public. A typical host firewall allows SSH from controlled sources plus HTTP and HTTPS:

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status

Do not expose the Uvicorn port, Unix socket, database, or Redis publicly. If private services live on other hosts, use a private network and tightly scoped rules.

11. Set CORS deliberately

CORS is a browser policy, not an authentication mechanism. Allow only the front-end origins, methods, and headers the application requires. Avoid combining wildcard origins with credentials.

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.example.com"],
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE"],
    allow_headers=["Authorization", "Content-Type"],
)

Authentication, authorization, rate limits, and input validation remain necessary even when CORS is restrictive.

12. Add request limits and timeouts

Set upload limits, upstream timeouts, and application timeouts from real endpoint behavior. Protect expensive routes with authentication and rate limits at the appropriate layer.

Keep request timeouts shorter than the outer proxy timeout so the application can stop work and return a controlled response. Move long-running tasks to a background queue instead of holding an HTTP request open.

13. Run database migrations safely

Apply migrations as a release step rather than when every worker starts:

sudo -u fastapi bash -c 'set -a; source /etc/fastapi.env; set +a; cd /srv/fastapi/app && /srv/fastapi/venv/bin/alembic upgrade head'

Review the migration plan, take a backup, and test both forward and rollback behavior. Prefer backward-compatible schema changes when old and new workers may overlap during a deployment.

14. Monitor the service

Track signals from every layer:

  • Nginx request rate, status codes, upstream latency, and rejected requests
  • Gunicorn worker restarts, timeouts, and queue pressure
  • Application request duration and error rate by route
  • Database connection use, query latency, locks, and storage growth
  • CPU, memory, swap, disk latency, free space, and network errors
  • Health-check success and TLS certificate expiry

Use structured logs with a request or trace identifier. Never log access tokens, passwords, session cookies, or sensitive request bodies.

15. Use a repeatable release workflow

  1. Build and test a locked release.
  2. Back up data and confirm the recovery path.
  3. Install the release into a new versioned directory.
  4. Run tests and security checks with production-like configuration.
  5. Review and apply compatible database migrations.
  6. Switch the active release and restart the service.
  7. Check health, logs, latency, and key API flows.
  8. Roll back quickly if validation fails.

A versioned directory plus a current symlink makes code rollback faster. Database rollback still needs separate planning.

Troubleshooting common FastAPI deployment problems

502 Bad Gateway

Check the service, socket, and Nginx error log:

sudo systemctl status fastapi
sudo ls -la /run/fastapi/app.sock
sudo journalctl -u fastapi -n 100 --no-pager
sudo tail -n 100 /var/log/nginx/error.log

Import or module errors

Verify the working directory, virtual environment, installed dependencies, Python import path, and the module:app target.

Wrong HTTPS URLs or client addresses

Confirm Nginx sends forwarded headers and that the application trusts them only from the intended proxy.

Workers restart under traffic

Inspect timeout logs, memory use, blocking calls, database pool limits, and slow external dependencies. Adding workers can worsen an exhausted downstream service.

Manage Python APIs from one control plane

Manual deployment makes every layer visible. As application count grows, teams benefit from a consistent place to manage runtimes, domains, certificates, files, databases, logs, backups, access, and server health.

Core Panel for Python hosting provides that operational view in a browser. Review the Node.js and Python application guide, SSL/TLS documentation, and logs and traffic guide. For a WSGI counterpart, see the Django deployment tutorial.

Final takeaway

A production FastAPI deployment is a chain of controlled responsibilities: Nginx protects the public edge, Gunicorn manages Uvicorn workers, systemd supervises the service, and your release process keeps changes observable and reversible.

Start with the smallest architecture that satisfies the workload, measure it under realistic traffic, and scale only after identifying the actual bottleneck.

Related posts

Stay in the loop

New posts and release notes, delivered to your inbox. No spam.

Ready to switch?
14-day free trial
Get Started