By Chirag Patil · · 7 min read

A Deployment Story: Taking a FastAPI AI Chatbot Live on DigitalOcean

How to deploy your fastapi application to Digital Ocean Droplet

Infrastructureaiagentic-ailarge-language-modelsllmfastapideploymentartificial-intelligence

I'm building a chat agent, and after getting it working on my local machine, it was time for the moment of truth: deploying it to a live server. I chose a budget-friendly $12/month DigitalOcean Droplet with 1 vCPU, 2GB of RAM, and a 50GB SSD, running Ubuntu 25.04.

The path from localhost to a fully functional, publicly accessible application wasn't a straight line. It was filled with errors that felt like roadblocks but were actually crucial learning experiences. Here is the story of how I deployed my app, the walls I ran into, and the solutions that finally brought it to life.

The Deployment Cheatsheet: My Most-Used Commands

Before we dive in, here are the essential Linux commands that were my constant companions during this process.

  • ssh-keygen: Creates a secure SSH key pair. We used it to give the server access to a private GitHub repo.
  • cat: Displays the content of a file. Perfect for viewing the public key (cat ~/.ssh/id_ed25519.pub).
  • git pull: Downloads the latest code changes from a Git repository.
  • uv sync: A command from the uv tool that installs all Python dependencies from a requirements.txt or pyproject.toml file.
  • systemctl [status|start|stop|restart] [service]: The master command for controlling background services (like our app or Nginx).
  • journalctl -u [service] --no-pager: Shows the full, detailed logs for a specific service. This is your number one debugging tool.
  • chmod: Changes the permissions of a file or directory, controlling who can read, write, or execute it.
  • htop: An interactive, real-time dashboard for monitoring your server's CPU and memory usage.
  • free -h: Gives a quick snapshot of your server's RAM and Swap usage in a human-readable format.
  • certbot: An amazing tool that automatically obtains and installs free SSL certificates.

Getting the Code on the Server

My project code lived in a private GitHub repository. To get it onto the droplet securely, I used a Deploy Key. This is an SSH key generated on the server that grants read-only access to a single repo. After creating the key with ssh-keygen, I added the public part to my GitHub repo's settings. This allowed me to git clone the project securely.

The Initial Stumbles

With the code on the server, I used uv sync to install my Python dependencies. My first attempt to run the app (uv run uvicorn main:app) crashed with an ImportError. The database driver (psycopg) needed its core engine. A quick uv pip install psycopg-binary fixed that.

The app was running in my terminal! But it would stop the second I logged off. I needed a permanent setup.

The Production Setup: Gunicorn, Systemd, and Nginx

To run an application reliably, the standard approach is to use a combination of these three powerful tools.

  • Gunicorn is a process manager for Python apps. It manages multiple "worker" processes, allowing your app to handle several requests at once.
  • Systemd is the Linux service manager. I created a service file at /etc/systemd/system/fastapi_app.service to tell systemd how to run Gunicorn in the background and restart it if it ever fails.
  • Nginx is a high-performance web server. I configured it as a reverse proxy—a public-facing front door at /etc/nginx/sites-available/fastapi_app that listens for web traffic and forwards it internally to Gunicorn.

I configured everything, started the services, and then hit my first major wall.

The Out-of-Memory Killer Strikes ☠️

I went to my server's IP address and was greeted by a 502 Bad Gateway. This error means Nginx is working, but it can't connect to the application. A quick check with sudo systemctl status fastapi_app revealed the brutal truth: Active: failed (Result: oom-kill). The Linux "Out of Memory Killer" had terminated my app.

The logs showed my application's memory usage peaked at 1.6GB on a server with only 2GB of RAM. The cause? I had configured Gunicorn to run with 4 workers, which was too much for my heavy AI app.

The solution was two-fold:

  1. Reduce Workers: I edited the fastapi_app.service file and changed --workers 4 to --workers 2.
  2. Add Swap Space: As a safety net, I created a 2GB swap file. Swap is an area on the hard drive that the OS uses as "virtual RAM" during memory spikes. bash sudo fallocate -l 2G /swapfile sudo swapon /swapfile After this, the service stayed active (running). But the saga wasn't over.

Connecting the Frontend: The Final Bosses

My backend was now live, but the whole point was to connect it to my frontend, which is hosted on Vercel. I updated my frontend's environment variables to point to http://159.65.157.136 and... nothing worked. The browser console lit up with errors.

Challenge 1: The "Mixed Content" Error

The console was very clear:

Mixed Content: The page at 'https://fos-schat.vercel.app/login' was loaded over HTTPS, but requested an insecure resource 'http://159.65.157.136/login'. This request has been blocked...

This happens because my Vercel frontend is automatically secure (HTTPS), but my backend was on an insecure HTTP connection. For security, browsers block this type of request. The only real solution was to make my backend secure as well. This required a domain name.

I decided a subdomain was the cleanest approach. I went into my DNS settings for lordpatil.com and created an "A" record for the host chatapi to point to my droplet's IP, 159.65.157.136.

Then, I updated my Nginx config to use this new subdomain:

# Edit /etc/nginx/sites-available/fastapi_app
# Change server_name from the IP to the subdomain
server_name chatapi.lordpatil.com;

Next, I used the fantastic Certbot tool to get a free SSL certificate. I first had to install it (sudo apt install certbot python3-certbot-nginx -y), then I ran it:

sudo certbot --nginx -d chatapi.lordpatil.com

Certbot automatically configured Nginx for HTTPS. I was ready.

Challenge 2: The ERR_CONNECTION_TIMED_OUT Wall 🧱

I went to my new secure URL, https://chatapi.lordpatil.com, full of confidence, only to be met with a new error: ERR_CONNECTION_TIMED_OUT.

This error means the server isn't responding at all. This almost always points to a firewall issue.

I realized that Certbot had configured Nginx to listen on port 443 (the standard for HTTPS), but my DigitalOcean firewall was only allowing traffic on ports 22 (SSH) and 80 (HTTP). It was silently dropping all requests to port 443.

The fix was to go into my DigitalOcean dashboard, edit my firewall's Inbound Rules, and add a new rule to allow HTTPS traffic on port 443.

With that final firewall change, the site loaded! My frontend was talking to my backend, and everything seemed perfect. I started a chat, sent a message, and waited... and waited. The response appeared all at once, not streamed token by token like it did on my local machine. The core functionality was there, but the "live" feeling was gone. This led me to the final, and most subtle, challenge of the entire deployment.

Challenge 3: The Case of the Missing Stream

My application is built for streaming responses. A StreamingResponse in FastAPI sends data back to the client in small chunks, creating a real-time, token-by-token effect in the chat window. But on the live server, this wasn't happening.

The issue wasn't in my Python code or my frontend. The culprit was Nginx.

By default, Nginx is designed for performance with traditional websites. To achieve this, it uses response buffering. When my FastAPI app started sending the stream, Nginx would dutifully collect every single chunk. Only after the entire stream was finished would it send the complete, fully-formed response to the browser. This completely defeated the purpose of streaming.

The Fix (A Few Lines in the Nginx Config):

I needed to explicitly tell Nginx to disable this buffering behavior for my application.

I went back into my Nginx configuration file one last time:

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

Inside the location / { ... } block, I added a few crucial directives:

location / {
    proxy_pass http://unix:/home/fastapi/FOSSchat/fastapi_app.sock;
    # ... other proxy_set_header lines ...

    # --- ADDED THESE LINES TO ENABLE STREAMING ---
    proxy_buffering off;
    proxy_cache off;
    chunked_transfer_encoding off;
}
  • proxy_buffering off;: This is the magic command. It tells Nginx to act as a pass-through and send data chunks to the browser the moment they arrive from my app.
  • proxy_cache off;: Disables caching, which is essential for dynamic, live streams.

I saved the file and restarted Nginx:

sudo systemctl restart nginx

I went back to my frontend, sent a message, and watched in triumph as the response streamed in, token by token. The application was now fully deployed and working exactly as intended.

The Adventure Continues... (Updated Conclusion)

Deploying this app was a journey of solving one problem at a time. Each error, from oom-kill to 502 Bad Gateway to timeouts and even a mysterious lack of streaming, was a clue that led to a deeper understanding of how a full web stack works. It was a challenge, but the result is a robust, permanent home for my application.

If you're on a similar path, just remember to read the error messages carefully and tackle one problem at a time. The browser console, systemctl status, journalctl, and the Nginx error logs are your best friends. You'll get there! 🎉

This essay keeps its original canonical home at blog.lordpatil.com.