<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Minhajul Islam Rifat</title>
    <description>The latest articles on DEV Community by Minhajul Islam Rifat (@rifat_dev).</description>
    <link>https://dev.to/rifat_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1983135%2F62f52bef-c5fd-4df0-8cd0-ed4e42699b7b.jpg</url>
      <title>DEV Community: Minhajul Islam Rifat</title>
      <link>https://dev.to/rifat_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rifat_dev"/>
    <language>en</language>
    <item>
      <title>From Localhost to Cloud: Deploying a Production-Ready FastAPI Application on Microsoft Azure</title>
      <dc:creator>Minhajul Islam Rifat</dc:creator>
      <pubDate>Mon, 05 Oct 2026 12:20:13 +0000</pubDate>
      <link>https://dev.to/rifat_dev/from-localhost-to-cloud-deploying-a-production-ready-fastapi-application-on-microsoft-azure-4pe</link>
      <guid>https://dev.to/rifat_dev/from-localhost-to-cloud-deploying-a-production-ready-fastapi-application-on-microsoft-azure-4pe</guid>
      <description>&lt;p&gt;
A FastAPI application may work perfectly at &lt;code&gt;127.0.0.1:8000&lt;/code&gt;,
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.
&lt;/p&gt;

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

&lt;p&gt;
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.
&lt;/p&gt;

&lt;p&gt;
By the end, the request path will look like this:
&lt;/p&gt;

&lt;p&gt;
&lt;strong&gt;Browser → Azure → Nginx → Gunicorn/Uvicorn → FastAPI → Database&lt;/strong&gt;
&lt;/p&gt;





&lt;h2&gt;The Deployment Problem&lt;/h2&gt;

&lt;p&gt;During local development, running FastAPI can be as simple as:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;uvicorn app.main:app --reload&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The application becomes available at:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;http://127.0.0.1:8000&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
That is useful for development, but it is not a production architecture.
&lt;/p&gt;

&lt;p&gt;A public deployment introduces several questions:&lt;/p&gt;

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





&lt;h2&gt;Success Criteria&lt;/h2&gt;

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





&lt;h2&gt;Architecture&lt;/h2&gt;

&lt;p&gt;
The final production architecture looks like this:
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp0s5vwbpdqkx90t6l6wa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp0s5vwbpdqkx90t6l6wa.png" alt="FastAPI production architecture on Microsoft Azure" width="800" height="1200"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;The request follows this path:&lt;/p&gt;

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

&lt;p&gt;
The important security boundary is that port &lt;code&gt;8000&lt;/code&gt; remains
private. Public traffic enters through Nginx.
&lt;/p&gt;





&lt;h2&gt;1. Start with a Verifiable FastAPI Application&lt;/h2&gt;

&lt;p&gt;
Before touching Azure, I first make sure the application works locally.
A small health-check endpoint is especially useful.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;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"}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Create the environment and install the required packages:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;python3 -m venv venv
source venv/bin/activate

pip install fastapi "uvicorn[standard]" gunicorn&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Start the application:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;uvicorn app.main:app --reload&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Test it:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;curl http://127.0.0.1:8000/health&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Expected response:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{"status":"ok"}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
I also verify &lt;code&gt;/docs&lt;/code&gt; before moving to the cloud.
This gives me a known-good starting point.
&lt;/p&gt;





&lt;h2&gt;2. Create the Azure Virtual Machine&lt;/h2&gt;

&lt;p&gt;
For this deployment, I use an Ubuntu 22.04 LTS virtual machine with
SSH-key authentication.
&lt;/p&gt;

&lt;p&gt;Authenticate using Azure CLI:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;az login&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Create a resource group:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;az group create \
  --name fastapi-demo-rg \
  --location eastus&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Create the VM:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;az vm create \
  --resource-group fastapi-demo-rg \
  --name fastapi-demo-vm \
  --image Ubuntu2204 \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Connect to the VM:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ssh azureuser@YOUR_PUBLIC_IP&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Network Security&lt;/h3&gt;

&lt;p&gt;
The Network Security Group should expose only the ports required by the
architecture.
&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Port&lt;/th&gt;
      &lt;th&gt;Purpose&lt;/th&gt;
      &lt;th&gt;Public Access&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;22&lt;/td&gt;
      &lt;td&gt;SSH administration&lt;/td&gt;
      &lt;td&gt;Yes, preferably restricted&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;80&lt;/td&gt;
      &lt;td&gt;HTTP / certificate validation&lt;/td&gt;
      &lt;td&gt;Yes&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;443&lt;/td&gt;
      &lt;td&gt;HTTPS application traffic&lt;/td&gt;
      &lt;td&gt;Yes&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;8000&lt;/td&gt;
      &lt;td&gt;FastAPI upstream&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;
Port &lt;code&gt;8000&lt;/code&gt; does not need to be internet-facing.
Nginx communicates with FastAPI internally.
&lt;/p&gt;





&lt;h2&gt;3. Prepare Ubuntu and Deploy the Application&lt;/h2&gt;

&lt;p&gt;Update the server:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Install the required packages:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo apt install -y \
  python3 \
  python3-pip \
  python3-venv \
  git \
  curl \
  nginx&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Clone the application:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;cd ~

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

cd fastapi-azure-demo&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Create the virtual environment:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;python3 -m venv venv
source venv/bin/activate&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Install dependencies:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;pip install -r requirements.txt&lt;/code&gt;&lt;/pre&gt;





&lt;h2&gt;4. Problem: FastAPI Works Inside the VM but Not Publicly&lt;/h2&gt;

&lt;p&gt;
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.
&lt;/p&gt;

&lt;p&gt;
If the following command works:
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;curl http://127.0.0.1:8000/health&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
then the FastAPI process itself may not be the problem.
&lt;/p&gt;

&lt;p&gt;
A tempting solution would be to expose port &lt;code&gt;8000&lt;/code&gt; publicly.
I deliberately avoid that architecture.
&lt;/p&gt;

&lt;p&gt;
Instead, FastAPI remains bound to:
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;127.0.0.1:8000&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
and Nginx becomes the public entry point on ports
&lt;code&gt;80&lt;/code&gt; and &lt;code&gt;443&lt;/code&gt;.
&lt;/p&gt;





&lt;h2&gt;5. Run FastAPI with Gunicorn and Uvicorn&lt;/h2&gt;

&lt;pre&gt;&lt;code&gt;gunicorn app.main:app \
  --workers 2 \
  --worker-class uvicorn.workers.UvicornWorker \
  --bind 127.0.0.1:8000&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The important part is:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;app.main:app&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;code&gt;app.main&lt;/code&gt; identifies the Python module.&lt;/li&gt;
  &lt;li&gt;
&lt;code&gt;app&lt;/code&gt; identifies the FastAPI application object.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
A wrong module path can result in:
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ModuleNotFoundError&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Test the application again:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;curl http://127.0.0.1:8000/health&lt;/code&gt;&lt;/pre&gt;





&lt;h2&gt;6. Keep the API Alive with systemd&lt;/h2&gt;

&lt;p&gt;
Starting Gunicorn manually means the application is still tied to an
interactive process. I use systemd to manage it.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;[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&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Reload systemd:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo systemctl daemon-reload&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Enable and start FastAPI:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo systemctl enable fastapi
sudo systemctl start fastapi&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Check status:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo systemctl status fastapi&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;View live logs:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo journalctl -u fastapi -f&lt;/code&gt;&lt;/pre&gt;





&lt;h2&gt;7. Put Nginx in Front of FastAPI&lt;/h2&gt;

&lt;p&gt;Create the configuration:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo nano /etc/nginx/sites-available/fastapi&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Example configuration:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;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;
    }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Enable the site:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo ln -s \
  /etc/nginx/sites-available/fastapi \
  /etc/nginx/sites-enabled/fastapi&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Remove the default configuration if it is no longer required:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo rm /etc/nginx/sites-enabled/default&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Test Nginx:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo nginx -t&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Reload Nginx:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo systemctl reload nginx&lt;/code&gt;&lt;/pre&gt;





&lt;h2&gt;8. Understanding 502 Bad Gateway&lt;/h2&gt;

&lt;p&gt;
A &lt;code&gt;502 Bad Gateway&lt;/code&gt; means Nginx received the request but
could not successfully communicate with the upstream FastAPI application.
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftcf4ofstkc335gt9d38p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftcf4ofstkc335gt9d38p.png" alt="502 Bad Gateway FastAPI Nginx troubleshooting flow" width="800" height="400"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;h3&gt;1. Check the FastAPI service&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;sudo systemctl status fastapi&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;2. Inspect application logs&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;sudo journalctl -u fastapi -n 100 --no-pager&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;3. Test the upstream directly&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;curl http://127.0.0.1:8000/health&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;4. Validate Nginx&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;sudo nginx -t&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;5. Inspect Nginx errors&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;sudo tail -f /var/log/nginx/error.log&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
The most important test is:
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;curl http://127.0.0.1:8000/health&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
If this fails, investigate:
&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;FastAPI&lt;/li&gt;
  &lt;li&gt;Gunicorn&lt;/li&gt;
  &lt;li&gt;systemd&lt;/li&gt;
  &lt;li&gt;Python dependencies&lt;/li&gt;
  &lt;li&gt;Module path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
If it succeeds, investigate:
&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Nginx configuration&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;proxy_pass&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;DNS / TLS&lt;/li&gt;
  &lt;li&gt;Azure networking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
Gunicorn and Nginx must agree on the same upstream address.
&lt;/p&gt;

&lt;p&gt;Gunicorn:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;--bind 127.0.0.1:8000&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Nginx:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;proxy_pass http://127.0.0.1:8000;&lt;/code&gt;&lt;/pre&gt;





&lt;h2&gt;9. Connect DNS&lt;/h2&gt;

&lt;p&gt;
Map the domain to the Azure public IP using an A record:
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;api.example.com → AZURE_PUBLIC_IP&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Verify DNS:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;dig api.example.com&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;or:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;nslookup api.example.com&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
I verify DNS before requesting a TLS certificate. This keeps DNS and
certificate problems separate during troubleshooting.
&lt;/p&gt;





&lt;h2&gt;10. Enable HTTPS&lt;/h2&gt;

&lt;p&gt;Install Certbot:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo apt install -y \
  certbot \
  python3-certbot-nginx&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Request the certificate:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo certbot --nginx -d api.example.com&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Test certificate renewal:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo certbot renew --dry-run&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Verify production:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;curl https://api.example.com/health&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Expected response:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{"status":"ok"}&lt;/code&gt;&lt;/pre&gt;





&lt;h2&gt;11. Think in Layers, Not Individual Errors&lt;/h2&gt;

&lt;p&gt;
The most useful lesson from this deployment was learning to treat the
system as independently testable layers.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;FastAPI
   ↓
Gunicorn / Uvicorn
   ↓
systemd
   ↓
Nginx
   ↓
Azure Networking
   ↓
DNS
   ↓
TLS / HTTPS&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
If:
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;curl http://127.0.0.1:8000/health&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
fails, I do not start changing DNS.
&lt;/p&gt;

&lt;p&gt;
If the local health check succeeds but the public domain fails, I move
outward to Nginx, Azure networking, DNS, and TLS.
&lt;/p&gt;

&lt;p&gt;
This changes troubleshooting from guessing into isolation.
&lt;/p&gt;





&lt;h2&gt;12. Common Failure Modes&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Symptom&lt;/th&gt;
      &lt;th&gt;First Place to Check&lt;/th&gt;
      &lt;th&gt;Likely Direction&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;

  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code&gt;ModuleNotFoundError&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Working directory / environment&lt;/td&gt;
      &lt;td&gt;Verify &lt;code&gt;app.main:app&lt;/code&gt; and dependencies&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;td&gt;&lt;code&gt;502 Bad Gateway&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Local health request&lt;/td&gt;
      &lt;td&gt;Check Gunicorn, systemd, port and proxy_pass&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;td&gt;&lt;code&gt;Address already in use&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Listening processes&lt;/td&gt;
      &lt;td&gt;Stop duplicate process&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;td&gt;Certificate failure&lt;/td&gt;
      &lt;td&gt;DNS and port 80&lt;/td&gt;
      &lt;td&gt;Verify DNS, Nginx and Azure networking&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;td&gt;Public URL unreachable&lt;/td&gt;
      &lt;td&gt;Azure NSG&lt;/td&gt;
      &lt;td&gt;Verify ports 80 and 443&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;td&gt;API stops after logout&lt;/td&gt;
      &lt;td&gt;Process management&lt;/td&gt;
      &lt;td&gt;Run through systemd&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For port conflicts:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo ss -tulpn | grep :8000&lt;/code&gt;&lt;/pre&gt;





&lt;h2&gt;13. Deployment Workflow After Initial Setup&lt;/h2&gt;

&lt;p&gt;Connect to the VM:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ssh azureuser@YOUR_PUBLIC_IP&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Pull the latest application:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;cd ~/fastapi-azure-demo
git pull origin main&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Activate the environment:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;source venv/bin/activate&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Update dependencies:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;pip install -r requirements.txt&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Restart FastAPI:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo systemctl restart fastapi&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Verify production:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;curl https://api.example.com/health&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
For larger production workflows, possible next steps include:
&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;GitHub Actions&lt;/li&gt;
  &lt;li&gt;Azure DevOps&lt;/li&gt;
  &lt;li&gt;Azure Container Apps&lt;/li&gt;
  &lt;li&gt;Azure App Service&lt;/li&gt;
  &lt;li&gt;Azure Container Registry&lt;/li&gt;
  &lt;li&gt;Automated deployment pipelines&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;14. Final Production Architecture&lt;/h2&gt;

&lt;p&gt;
  &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frzmr28js9dmx1731zifa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frzmr28js9dmx1731zifa.png" alt="Final FastAPI Azure production request path" width="800" height="1200"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
The important boundary is that the internet never needs direct access
to the FastAPI application server.
&lt;/p&gt;

&lt;p&gt;
&lt;strong&gt;
Public traffic terminates at Nginx, while FastAPI remains private.
&lt;/strong&gt;
&lt;/p&gt;





&lt;h2&gt;15. Azure VM or Azure App Service?&lt;/h2&gt;

&lt;p&gt;
A virtual machine is useful when I need:
&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Operating-system-level control&lt;/li&gt;
  &lt;li&gt;Direct Nginx configuration&lt;/li&gt;
  &lt;li&gt;Custom services&lt;/li&gt;
  &lt;li&gt;Custom networking&lt;/li&gt;
  &lt;li&gt;Control over process management&lt;/li&gt;
  &lt;li&gt;A traditional Linux server environment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
The trade-off is operational responsibility.
&lt;/p&gt;

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

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





&lt;h2&gt;16. What I Would Improve Next&lt;/h2&gt;

&lt;h3&gt;Automated Deployment&lt;/h3&gt;

&lt;p&gt;
Replace manual Git pulls and service restarts with CI/CD.
&lt;/p&gt;

&lt;h3&gt;Centralized Monitoring&lt;/h3&gt;

&lt;p&gt;
Add application and infrastructure observability so failures can be
detected before users report them.
&lt;/p&gt;

&lt;h3&gt;Secrets Management&lt;/h3&gt;

&lt;p&gt;
Move production secrets and sensitive configuration out of application
files and into an appropriate secrets-management workflow.
&lt;/p&gt;

&lt;h3&gt;Database Hardening&lt;/h3&gt;

&lt;p&gt;
Use an appropriately managed or isolated database architecture for
production workloads.
&lt;/p&gt;

&lt;h3&gt;Backup and Recovery&lt;/h3&gt;

&lt;p&gt;
Define a recovery strategy for application data and configuration.
&lt;/p&gt;

&lt;h3&gt;Scaling&lt;/h3&gt;

&lt;p&gt;
A single VM is simple, but availability and scaling requirements may
eventually justify managed or container-based infrastructure.
&lt;/p&gt;





&lt;h2&gt;Key Takeaways&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;strong&gt;Verify locally before deploying.&lt;/strong&gt;
    A &lt;code&gt;/health&lt;/code&gt; endpoint provides a known-good starting point.
  &lt;/li&gt;

  &lt;li&gt;
    &lt;strong&gt;Do not expose FastAPI unnecessarily.&lt;/strong&gt;
    Keep FastAPI on &lt;code&gt;127.0.0.1:8000&lt;/code&gt; and let Nginx handle
    public traffic.
  &lt;/li&gt;

  &lt;li&gt;
    &lt;strong&gt;Use process management.&lt;/strong&gt;
    systemd keeps the API independent from an SSH session.
  &lt;/li&gt;

  &lt;li&gt;
    &lt;strong&gt;Debug from the inside out.&lt;/strong&gt;
    Test FastAPI, Gunicorn, systemd, Nginx, Azure networking, DNS,
    and finally HTTPS.
  &lt;/li&gt;

  &lt;li&gt;
    &lt;strong&gt;Treat 502 Bad Gateway as a routing clue.&lt;/strong&gt;
    Confirm that Nginx's &lt;code&gt;proxy_pass&lt;/code&gt; and Gunicorn's bind
    address match.
  &lt;/li&gt;

  &lt;li&gt;
    &lt;strong&gt;Expose only the ports the architecture requires.&lt;/strong&gt;
    The FastAPI application port remains private.
  &lt;/li&gt;

  &lt;li&gt;
    &lt;strong&gt;Choose the Azure service based on operational needs.&lt;/strong&gt;
    A VM provides control, while managed Azure services can reduce
    infrastructure maintenance.
  &lt;/li&gt;
&lt;/ol&gt;





&lt;h2&gt;Closing Thoughts&lt;/h2&gt;

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

&lt;p&gt;
A request to:
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;https://api.example.com&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
passes through multiple independent systems before it reaches the Python
application.
&lt;/p&gt;

&lt;p&gt;
Once I started testing those systems separately, deployment problems
became much easier to understand.
&lt;/p&gt;

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;blockquote&gt;
Why is my API not working?
&lt;/blockquote&gt;

&lt;p&gt;I can ask:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Is FastAPI healthy?&lt;/li&gt;
  &lt;li&gt;Is Gunicorn listening?&lt;/li&gt;
  &lt;li&gt;Is systemd running the process?&lt;/li&gt;
  &lt;li&gt;Can Nginx reach the upstream?&lt;/li&gt;
  &lt;li&gt;Does Azure allow the request?&lt;/li&gt;
  &lt;li&gt;Does DNS resolve correctly?&lt;/li&gt;
  &lt;li&gt;Is HTTPS configured correctly?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
That layered troubleshooting approach is the most reusable lesson from
the entire deployment.
&lt;/p&gt;





&lt;h2&gt;Learn More on Microsoft Learn&lt;/h2&gt;

&lt;p&gt;
To explore the Azure concepts used in this deployment in more detail,
check out the following official Microsoft Learn resources:
&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;a href="https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-cli?wt.mc_id=studentamb_491762" rel="noopener noreferrer"&gt;
      Create a Linux Virtual Machine with the Azure CLI
    &lt;/a&gt;
  &lt;/li&gt;

  &lt;li&gt;
    &lt;a href="https://learn.microsoft.com/en-us/azure/virtual-machines/linux/tutorial-virtual-network?wt.mc_id=studentamb_491762" rel="noopener noreferrer"&gt;
      Create and Manage Azure Virtual Networks for Linux VMs
    &lt;/a&gt;
  &lt;/li&gt;

  &lt;li&gt;
    &lt;a href="https://learn.microsoft.com/en-us/azure/virtual-machines/linux/tutorial-secure-web-server?wt.mc_id=studentamb_491762" rel="noopener noreferrer"&gt;
      Secure a Web Server on an Azure Virtual Machine
    &lt;/a&gt;
  &lt;/li&gt;

  &lt;li&gt;
    &lt;a href="https://learn.microsoft.com/en-us/azure/app-service/quickstart-python?wt.mc_id=studentamb_491762" rel="noopener noreferrer"&gt;
      Deploy a Python / FastAPI Web App to Azure App Service
    &lt;/a&gt;
  &lt;/li&gt;

  &lt;li&gt;
    &lt;a href="https://learn.microsoft.com/en-us/azure/app-service/tutorial-python-postgresql-app-fastapi?wt.mc_id=studentamb_491762" rel="noopener noreferrer"&gt;
      Deploy a FastAPI Web App with PostgreSQL on Azure
    &lt;/a&gt;
  &lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;
&lt;strong&gt;Tags:&lt;/strong&gt;
#azure #fastapi #python #cloud #devops
&lt;/p&gt;

</description>
      <category>azure</category>
      <category>devops</category>
      <category>fastapi</category>
      <category>python</category>
    </item>
  </channel>
</rss>
