DEV Community

Cover image for From Localhost to Cloud: Deploying a Production-Ready FastAPI Application on Microsoft Azure
Minhajul Islam Rifat
Minhajul Islam Rifat

Posted on

From Localhost to Cloud: Deploying a Production-Ready FastAPI Application on Microsoft Azure

A FastAPI application may work perfectly at 127.0.0.1:8000, but moving that application to a public, secure, and maintainable cloud environment introduces several additional layers: virtual machines, network security, process management, reverse proxying, DNS, and HTTPS.

In this walkthrough, I deploy a FastAPI application to an Ubuntu virtual machine on Microsoft Azure and build the production path around it using Gunicorn, Uvicorn, systemd, Nginx, and TLS.

The goal is not simply to make the API reachable from the internet. The goal is to understand each layer of the deployment, how the layers interact, and how to troubleshoot the system when something fails.

By the end, the request path will look like this:

Browser → Azure → Nginx → Gunicorn/Uvicorn → FastAPI → Database


The Deployment Problem

During local development, running FastAPI can be as simple as:

uvicorn app.main:app --reload

The application becomes available at:

http://127.0.0.1:8000

That is useful for development, but it is not a production architecture.

A public deployment introduces several questions:

  • How does internet traffic reach the server?
  • Which ports should be exposed?
  • Should FastAPI itself be publicly accessible?
  • What happens when the SSH session closes?
  • How does the application restart after a failure or VM reboot?
  • Where should HTTPS terminate?
  • How can a 502 Bad Gateway be isolated quickly?
  • How can each deployment layer be verified independently?

Success Criteria

  • FastAPI runs successfully on the Azure VM.
  • The application survives SSH disconnection.
  • The service automatically restarts after a process failure.
  • FastAPI listens only on the VM's loopback interface.
  • Public application traffic enters through Nginx.
  • Azure exposes only the required inbound ports.
  • A domain name resolves to the Azure public IP.
  • HTTPS protects traffic between the client and Nginx.
  • A /health endpoint allows each layer to be tested.

Architecture

The final production architecture looks like this:

FastAPI production architecture on Microsoft Azure

The request follows this path:

User / Browser
      ↓
HTTPS :443
      ↓
Internet
      ↓
Azure Public IP
      ↓
Network Security Group
      ↓
Ubuntu Azure VM
      ↓
Nginx
      ↓
127.0.0.1:8000
      ↓
Gunicorn + Uvicorn
      ↓
FastAPI
      ↓
Database

The important security boundary is that port 8000 remains private. Public traffic enters through Nginx.


1. Start with a Verifiable FastAPI Application

Before touching Azure, I first make sure the application works locally. A small health-check endpoint is especially useful.

from fastapi import FastAPI

app = FastAPI(title="Azure FastAPI Demo")


@app.get("/")
def root():
    return {
        "message": "FastAPI is running",
        "environment": "local"
    }


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

Create the environment and install the required packages:

python3 -m venv venv
source venv/bin/activate

pip install fastapi "uvicorn[standard]" gunicorn

Start the application:

uvicorn app.main:app --reload

Test it:

curl http://127.0.0.1:8000/health

Expected response:

{"status":"ok"}

I also verify /docs before moving to the cloud. This gives me a known-good starting point.


2. Create the Azure Virtual Machine

For this deployment, I use an Ubuntu 22.04 LTS virtual machine with SSH-key authentication.

Authenticate using Azure CLI:

az login

Create a resource group:

az group create \
  --name fastapi-demo-rg \
  --location eastus

Create the VM:

az vm create \
  --resource-group fastapi-demo-rg \
  --name fastapi-demo-vm \
  --image Ubuntu2204 \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

Connect to the VM:

ssh azureuser@YOUR_PUBLIC_IP

Network Security

The Network Security Group should expose only the ports required by the architecture.

Port Purpose Public Access
22 SSH administration Yes, preferably restricted
80 HTTP / certificate validation Yes
443 HTTPS application traffic Yes
8000 FastAPI upstream No

Port 8000 does not need to be internet-facing. Nginx communicates with FastAPI internally.


3. Prepare Ubuntu and Deploy the Application

Update the server:

sudo apt update && sudo apt upgrade -y

Install the required packages:

sudo apt install -y \
  python3 \
  python3-pip \
  python3-venv \
  git \
  curl \
  nginx

Clone the application:

cd ~

git clone https://github.com/USERNAME/fastapi-azure-demo.git

cd fastapi-azure-demo

Create the virtual environment:

python3 -m venv venv
source venv/bin/activate

Install dependencies:

pip install -r requirements.txt

4. Problem: FastAPI Works Inside the VM but Not Publicly

One of the first deployment issues I encountered was that FastAPI responded correctly inside the virtual machine, but the application could not be reached from outside Azure.

If the following command works:

curl http://127.0.0.1:8000/health

then the FastAPI process itself may not be the problem.

A tempting solution would be to expose port 8000 publicly. I deliberately avoid that architecture.

Instead, FastAPI remains bound to:

127.0.0.1:8000

and Nginx becomes the public entry point on ports 80 and 443.


5. Run FastAPI with Gunicorn and Uvicorn

gunicorn app.main:app \
  --workers 2 \
  --worker-class uvicorn.workers.UvicornWorker \
  --bind 127.0.0.1:8000

The important part is:

app.main:app
  • app.main identifies the Python module.
  • app identifies the FastAPI application object.

A wrong module path can result in:

ModuleNotFoundError

Test the application again:

curl http://127.0.0.1:8000/health

6. Keep the API Alive with systemd

Starting Gunicorn manually means the application is still tied to an interactive process. I use systemd to manage it.

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

[Service]
User=azureuser
Group=www-data
WorkingDirectory=/home/azureuser/fastapi-azure-demo
Environment="PATH=/home/azureuser/fastapi-azure-demo/venv/bin"

ExecStart=/home/azureuser/fastapi-azure-demo/venv/bin/gunicorn \
    app.main:app \
    --workers 2 \
    --worker-class uvicorn.workers.UvicornWorker \
    --bind 127.0.0.1:8000

Restart=always

[Install]
WantedBy=multi-user.target

Reload systemd:

sudo systemctl daemon-reload

Enable and start FastAPI:

sudo systemctl enable fastapi
sudo systemctl start fastapi

Check status:

sudo systemctl status fastapi

View live logs:

sudo journalctl -u fastapi -f

7. Put Nginx in Front of FastAPI

Create the configuration:

sudo nano /etc/nginx/sites-available/fastapi

Example configuration:

server {
    listen 80;

    server_name api.example.com;

    location / {
        proxy_pass http://127.0.0.1:8000;

        proxy_http_version 1.1;

        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;
    }
}

Enable the site:

sudo ln -s \
  /etc/nginx/sites-available/fastapi \
  /etc/nginx/sites-enabled/fastapi

Remove the default configuration if it is no longer required:

sudo rm /etc/nginx/sites-enabled/default

Test Nginx:

sudo nginx -t

Reload Nginx:

sudo systemctl reload nginx

8. Understanding 502 Bad Gateway

A 502 Bad Gateway means Nginx received the request but could not successfully communicate with the upstream FastAPI application.

502 Bad Gateway FastAPI Nginx troubleshooting flow

1. Check the FastAPI service

sudo systemctl status fastapi

2. Inspect application logs

sudo journalctl -u fastapi -n 100 --no-pager

3. Test the upstream directly

curl http://127.0.0.1:8000/health

4. Validate Nginx

sudo nginx -t

5. Inspect Nginx errors

sudo tail -f /var/log/nginx/error.log

The most important test is:

curl http://127.0.0.1:8000/health

If this fails, investigate:

  • FastAPI
  • Gunicorn
  • systemd
  • Python dependencies
  • Module path

If it succeeds, investigate:

  • Nginx configuration
  • proxy_pass
  • DNS / TLS
  • Azure networking

Gunicorn and Nginx must agree on the same upstream address.

Gunicorn:

--bind 127.0.0.1:8000

Nginx:

proxy_pass http://127.0.0.1:8000;

9. Connect DNS

Map the domain to the Azure public IP using an A record:

api.example.com → AZURE_PUBLIC_IP

Verify DNS:

dig api.example.com

or:

nslookup api.example.com

I verify DNS before requesting a TLS certificate. This keeps DNS and certificate problems separate during troubleshooting.


10. Enable HTTPS

Install Certbot:

sudo apt install -y \
  certbot \
  python3-certbot-nginx

Request the certificate:

sudo certbot --nginx -d api.example.com

Test certificate renewal:

sudo certbot renew --dry-run

Verify production:

curl https://api.example.com/health

Expected response:

{"status":"ok"}

11. Think in Layers, Not Individual Errors

The most useful lesson from this deployment was learning to treat the system as independently testable layers.

FastAPI
   ↓
Gunicorn / Uvicorn
   ↓
systemd
   ↓
Nginx
   ↓
Azure Networking
   ↓
DNS
   ↓
TLS / HTTPS

If:

curl http://127.0.0.1:8000/health

fails, I do not start changing DNS.

If the local health check succeeds but the public domain fails, I move outward to Nginx, Azure networking, DNS, and TLS.

This changes troubleshooting from guessing into isolation.


12. Common Failure Modes

Symptom First Place to Check Likely Direction
ModuleNotFoundError Working directory / environment Verify app.main:app and dependencies
502 Bad Gateway Local health request Check Gunicorn, systemd, port and proxy_pass
Address already in use Listening processes Stop duplicate process
Certificate failure DNS and port 80 Verify DNS, Nginx and Azure networking
Public URL unreachable Azure NSG Verify ports 80 and 443
API stops after logout Process management Run through systemd

For port conflicts:

sudo ss -tulpn | grep :8000

13. Deployment Workflow After Initial Setup

Connect to the VM:

ssh azureuser@YOUR_PUBLIC_IP

Pull the latest application:

cd ~/fastapi-azure-demo
git pull origin main

Activate the environment:

source venv/bin/activate

Update dependencies:

pip install -r requirements.txt

Restart FastAPI:

sudo systemctl restart fastapi

Verify production:

curl https://api.example.com/health

For larger production workflows, possible next steps include:

  • GitHub Actions
  • Azure DevOps
  • Azure Container Apps
  • Azure App Service
  • Azure Container Registry
  • Automated deployment pipelines

14. Final Production Architecture

Final FastAPI Azure production request path

The important boundary is that the internet never needs direct access to the FastAPI application server.

Public traffic terminates at Nginx, while FastAPI remains private.


15. Azure VM or Azure App Service?

A virtual machine is useful when I need:

  • Operating-system-level control
  • Direct Nginx configuration
  • Custom services
  • Custom networking
  • Control over process management
  • A traditional Linux server environment

The trade-off is operational responsibility.

With a VM, I am responsible for more of the environment: operating system updates, service configuration, Nginx, TLS, process management, and infrastructure maintenance.

For applications where managed hosting and simpler deployment are more important than operating-system control, Azure App Service may be a better option.


16. What I Would Improve Next

Automated Deployment

Replace manual Git pulls and service restarts with CI/CD.

Centralized Monitoring

Add application and infrastructure observability so failures can be detected before users report them.

Secrets Management

Move production secrets and sensitive configuration out of application files and into an appropriate secrets-management workflow.

Database Hardening

Use an appropriately managed or isolated database architecture for production workloads.

Backup and Recovery

Define a recovery strategy for application data and configuration.

Scaling

A single VM is simple, but availability and scaling requirements may eventually justify managed or container-based infrastructure.


Key Takeaways

  1. Verify locally before deploying. A /health endpoint provides a known-good starting point.
  2. Do not expose FastAPI unnecessarily. Keep FastAPI on 127.0.0.1:8000 and let Nginx handle public traffic.
  3. Use process management. systemd keeps the API independent from an SSH session.
  4. Debug from the inside out. Test FastAPI, Gunicorn, systemd, Nginx, Azure networking, DNS, and finally HTTPS.
  5. Treat 502 Bad Gateway as a routing clue. Confirm that Nginx's proxy_pass and Gunicorn's bind address match.
  6. Expose only the ports the architecture requires. The FastAPI application port remains private.
  7. Choose the Azure service based on operational needs. A VM provides control, while managed Azure services can reduce infrastructure maintenance.

Closing Thoughts

Deploying FastAPI to Azure taught me that cloud deployment is less about running one command and more about understanding the path a request follows.

A request to:

https://api.example.com

passes through multiple independent systems before it reaches the Python application.

Once I started testing those systems separately, deployment problems became much easier to understand.

Instead of asking:

Why is my API not working?

I can ask:

  • Is FastAPI healthy?
  • Is Gunicorn listening?
  • Is systemd running the process?
  • Can Nginx reach the upstream?
  • Does Azure allow the request?
  • Does DNS resolve correctly?
  • Is HTTPS configured correctly?

That layered troubleshooting approach is the most reusable lesson from the entire deployment.


Learn More on Microsoft Learn

To explore the Azure concepts used in this deployment in more detail, check out the following official Microsoft Learn resources:


Tags: #azure #fastapi #python #cloud #devops

Top comments (0)