I remember a couple of months ago, during a late-night debugging session for a particularly stubborn memory leak in a new Ktor service, a few developer friends hit me up. They knew I hosted most of my side projects and many of my Discord bots on DigitalOcean, and lately, they'd been curious about my setup. Not just 'how do you host a website,' but the whole lifecycle: from local dev to a production-ready cloud deployment. I’ve seen countless tutorials that skip vital parts or assume too much, leaving developers with a half-baked understanding.
My friends wanted to see the smallest real app I could deploy. Something that touched on an API, a database, and proper containerization, all running on a cloud VM. This wasn't about a static site or a serverless function; it was about demonstrating a classic, robust backend deployment pattern. So, I built them exactly that: a tiny FastAPI application connected to MongoDB, all orchestrated with Docker and served via Nginx on a DigitalOcean Droplet.
This isn't just theory. This is the exact pattern I use for many of my production microservices, scaled up or down. Whether I'm building a new backend for a Jetpack Compose Android app or a complex Discord bot, the principles remain consistent. If you're looking to understand the mechanics of taking a Python backend to the cloud, this guide is for you.
The "Smallest Real App": A Simple Counter Service
To keep things minimal yet demonstrative, I chose a simple counter service. It exposes a single endpoint that increments a counter and returns its current value. The state (the counter's value) is persisted in a MongoDB database. This allows us to demonstrate:
- A functional API endpoint.
- Database integration and persistence.
- Containerization of both the application and the database.
- Deployment to a cloud VM.
- Basic reverse proxy setup with Nginx.
1. The FastAPI Application
First, let's set up our FastAPI application. If you haven't worked with FastAPI before, I highly recommend checking out its official documentation. It's a fantastic framework for building high-performance APIs with Python, leveraging Starlette and Pydantic. I've even written a detailed comparison between Ktor and FastAPI that you can find at /blog/ktor-fastapi-backend-comparison, highlighting why FastAPI often wins for specific use cases.
Create a directory for your project, say do-counter-app. Inside, create a file named main.py:
# main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from pymongo import MongoClient
from pymongo.errors import ConnectionFailure
import os
import uvicorn
app = FastAPI()
# MongoDB Configuration
MONGO_URI = os.getenv("MONGO_URI", "mongodb://localhost:27017/")
DATABASE_NAME = os.getenv("DATABASE_NAME", "counter_db")
COLLECTION_NAME = os.getenv("COLLECTION_NAME", "counters")
client = None
db = None
try:
client = MongoClient(MONGO_URI)
db = client[DATABASE_NAME]
# The ping command is cheap and does not require auth.
client.admin.command('ping')
print("Successfully connected to MongoDB!")
except ConnectionFailure as e:
print(f"Could not connect to MongoDB: {e}")
# In a real app, you might want to retry or exit
client = None
db = None
class Counter(BaseModel):
value: int = 0
@app.get("/")
async def read_root():
return {"message": "Welcome to the DigitalOcean Counter App!"}
@app.post("/increment", response_model=Counter)
async def increment_counter():
if not db:
raise HTTPException(status_code=500, detail="Database connection not established.")
counters_collection = db[COLLECTION_NAME]
# Atomically increment the counter or insert if it doesn't exist
result = counters_collection.find_one_and_update(
{"_id": "global_counter"},
{"$inc": {"value": 1}},
upsert=True,
return_document=True
)
if result:
return Counter(value=result["value"])
else:
raise HTTPException(status_code=500, detail="Failed to increment counter.")
@app.get("/current", response_model=Counter)
async def get_current_counter():
if not db:
raise HTTPException(status_code=500, detail="Database connection not established.")
counters_collection = db[COLLECTION_NAME]
counter_doc = counters_collection.find_one({"_id": "global_counter"})
if counter_doc:
return Counter(value=counter_doc["value"])
else:
return Counter(value=0) # Default to 0 if not found
if __name__ == "__main__":
# This block is for local testing with `python main.py`
# For production, Uvicorn is run via Gunicorn or direct `uvicorn main:app` command.
uvicorn.run(app, host="0.0.0.0", port=8000)And our dependencies in requirements.txt:
# requirements.txt
fastapi==0.111.0
uvicorn[standard]==0.29.0
pymongo==4.7.2
pydantic==2.7.1
2. Dockerizing the Application and Database
Containerization is non-negotiable for modern deployments. Docker provides consistency between development and production environments, eliminating
Need Help with Custom APIs or Backend Systems?
I build robust, secure, and scalable backend services, databases, and microservices using FastAPI, Ktor, Node.js, and MongoDB. Let's build your server infrastructure!
Written by
Hazrat Ummar Shaikh
Android Developer with 4+ years of experience. Built production Android apps, Ktor backends, Discord bots, and SaaS products using Kotlin, Python, and MongoDB. Passionate about building robust systems and writing clean code.



