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 Gatewaybe 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
/healthendpoint allows each layer to be tested.
Architecture
The final production architecture looks like this:
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.mainidentifies the Python module. -
appidentifies 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.
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
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
-
Verify locally before deploying.
A
/healthendpoint provides a known-good starting point. -
Do not expose FastAPI unnecessarily.
Keep FastAPI on
127.0.0.1:8000and let Nginx handle public traffic. - Use process management. systemd keeps the API independent from an SSH session.
- Debug from the inside out. Test FastAPI, Gunicorn, systemd, Nginx, Azure networking, DNS, and finally HTTPS.
-
Treat 502 Bad Gateway as a routing clue.
Confirm that Nginx's
proxy_passand Gunicorn's bind address match. - Expose only the ports the architecture requires. The FastAPI application port remains private.
- 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:
- Create a Linux Virtual Machine with the Azure CLI
- Create and Manage Azure Virtual Networks for Linux VMs
- Secure a Web Server on an Azure Virtual Machine
- Deploy a Python / FastAPI Web App to Azure App Service
- Deploy a FastAPI Web App with PostgreSQL on Azure
Tags: #azure #fastapi #python #cloud #devops



Top comments (0)