Deploy a full-stack Python app with MongoDB using CLI or MCP
Deploy a full-stack Python app with MongoDB using CLI or MCP
Author: NEXUS AI Team
Category: Python · MongoDB · App Deployment
Reading time: 12 minutes
![]()
You have a Python app working on your laptop. Now you need somewhere to run it, a database that keeps its data, and a way to find out what went wrong when a request fails.
This guide walks through that next step. You will build a small notes app with a browser interface, a Flask backend, and MongoDB for storage, then deploy it on NEXUS AI.
You can deploy from your terminal with the CLI or ask an AI assistant to deploy through MCP. Both paths use the same application and database setup. Choose one deployment path to avoid creating two copies.
Step 1: Understand what you are deploying
The app has three parts:
| Part | What it does |
|---|---|
| Frontend | An HTML page with a form and a list of saved notes |
| Python backend | Flask routes that read and write notes through PyMongo |
| MongoDB | Stores notes outside the app container |
The request flow is:
Browser
|
| HTTPS
v
Flask app on NEXUS AI
|
| PyMongo connection over the deployment network
v
MongoDB service
Flask serves the HTML and the API from the same origin, so this example needs one web deployment and no separate frontend build.
We will use NEXUS AI managed infrastructure with provider: docker. Its MongoDB sidecar service supplies the app's connection settings, including MONGO_URI and MONGO_DATABASE. The MongoDB sidecar option is not supported on the Cloud Run, ECS Fargate, or Azure Container Apps deployment paths.
Before starting, have these ready:
- Python 3.12 and Git.
- Docker for the local MongoDB example.
- A GitHub account and an empty repository named
python-mongo-notes. - A NEXUS AI account with access to managed container deployments and enough quota for the app and database.
- Node.js 18 or later for the NEXUS AI CLI, or an MCP client such as Cursor or Claude Code.
This is a shared public demo: everyone who opens the URL can read and add notes. Use sample content. Add authentication and per-user access controls before storing private information.
Step 2: Create the project
Run these commands in Bash or Zsh on macOS or Linux:
mkdir python-mongo-notes
cd python-mongo-notes
python3 -m venv .venv
source .venv/bin/activate
mkdir templates
Create requirements.txt:
Flask>=3.1,<4
pymongo>=4.10,<5
gunicorn>=23,<24
python-dotenv>=1,<2
Install the dependencies:
python -m pip install -r requirements.txt
These ranges keep the example within the intended major versions. Before maintaining a release, lock the exact versions you have tested.
Create .gitignore:
.venv/
__pycache__/
.env
.env.*
Your final project will contain:
python-mongo-notes/
app.py
templates/
index.html
requirements.txt
Dockerfile
.dockerignore
.gitignore
Step 3: Add the Python backend
Create app.py:
import os
from dotenv import load_dotenv
from flask import Flask, jsonify, render_template, request
from pymongo import MongoClient
from pymongo.errors import PyMongoError
load_dotenv()
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 16 * 1024
client = MongoClient(
os.environ["MONGO_URI"],
serverSelectionTimeoutMS=5000,
)
db = client[os.getenv("MONGO_DATABASE", "appdb")]
@app.get("/")
def index():
return render_template("index.html")
@app.get("/api/notes")
def list_notes():
documents = db.notes.find().sort("_id", -1).limit(50)
return jsonify([
{"id": str(note["_id"]), "text": note["text"]}
for note in documents
])
@app.post("/api/notes")
def create_note():
data = request.get_json(silent=True)
if not isinstance(data, dict):
return jsonify(error="Send a JSON object."), 400
text = data.get("text")
if not isinstance(text, str) or not 1 <= len(text.strip()) <= 500:
return jsonify(error="Enter a note between 1 and 500 characters."), 400
text = text.strip()
result = db.notes.insert_one({"text": text})
return jsonify(id=str(result.inserted_id), text=text), 201
@app.get("/health")
def health():
try:
client.admin.command("ping")
except PyMongoError:
return jsonify(status="unhealthy", database="unavailable"), 503
return jsonify(status="ok", database="connected")
MONGO_URI comes from the environment. No database credentials belong in the source code or browser JavaScript. PyMongo manages connection pooling through MongoClient. See the official MongoClient documentation.
The /health route actually pings MongoDB. A working homepage alone does not tell you whether the app can reach its database.
Step 4: Add the browser interface
Create templates/index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Python + MongoDB Notes</title>
<style>
body { font: 18px system-ui; max-width: 640px; margin: 60px auto;
padding: 0 20px; color: #142033; }
input, button { font: inherit; padding: 12px; }
input { width: 60%; }
button { background: #0a60ff; color: white; border: 0;
border-radius: 6px; cursor: pointer; }
li { padding: 12px 0; overflow-wrap: anywhere; }
</style>
</head>
<body>
<h1>Your notes, saved with Python + MongoDB</h1>
<form id="note-form">
<label for="note">New note</label><br>
<input id="note" maxlength="500" required>
<button>Save note</button>
</form>
<p id="message" role="status"></p>
<ul id="notes"></ul>
<script>
const form = document.querySelector('#note-form');
const input = document.querySelector('#note');
const message = document.querySelector('#message');
async function loadNotes() {
const response = await fetch('/api/notes');
if (!response.ok) throw new Error('Could not load notes.');
const notes = await response.json();
const list = document.querySelector('#notes');
list.replaceChildren();
for (const note of notes) {
const item = document.createElement('li');
item.textContent = note.text;
list.appendChild(item);
}
}
form.addEventListener('submit', async (event) => {
event.preventDefault();
try {
const response = await fetch('/api/notes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: input.value })
});
if (!response.ok) throw new Error('Could not save your note.');
input.value = '';
message.textContent = 'Note saved.';
await loadNotes();
} catch (error) {
message.textContent = error.message;
}
});
loadNotes().catch(error => { message.textContent = error.message; });
</script>
</body>
</html>
The browser sends requests to /api/notes. Only the Python process connects to MongoDB. Using textContent also makes note content display as text instead of executable HTML.
Step 5: Run the complete app locally
Start a local MongoDB container:
docker run -d \
--name python-mongo-notes-db \
-p 127.0.0.1:27017:27017 \
-v python-mongo-notes-data:/data/db \
mongo:7
This local development database has no authentication and binds only to your machine's loopback interface. The named volume stores data separately from the container. On later runs, start the existing container with docker start python-mongo-notes-db.
Create a local .env file:
MONGO_URI=mongodb://127.0.0.1:27017/appdb
MONGO_DATABASE=appdb
Start Flask:
flask --app app run --port 8000
Open http://127.0.0.1:8000, save a note, and refresh the page. The note should still appear.
In another terminal, check the database connection:
curl --fail-with-body http://127.0.0.1:8000/health
Expected response, with key order allowed to vary:
{"database":"connected","status":"ok"}
Checkpoint: the page loads, a note survives a refresh, and /health reports a database connection. Fix local errors before deploying.
Step 6: Package the app and push it to GitHub
Create Dockerfile:
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
COPY templates ./templates
EXPOSE 8000
CMD ["sh", "-c", "exec gunicorn --bind 0.0.0.0:${PORT:-8000} --workers 2 --access-logfile - --error-logfile - app:app"]
Gunicorn runs Flask in the deployment container and listens on all container interfaces. Its logs go to stdout and stderr so you can inspect them through NEXUS AI. Flask's development server is for local use; see Flask's deployment guide.
Create .dockerignore:
.git
.venv
__pycache__
.env
.env.*
Push the project to the empty GitHub repository you created. Replace YOUR_USERNAME first:
git init
git add app.py templates/index.html requirements.txt Dockerfile .dockerignore .gitignore
git commit -m "Add Python and MongoDB notes app"
git branch -M main
git remote add origin https://github.com/YOUR_USERNAME/python-mongo-notes.git
git push -u origin main
The explicit file list keeps your local .env out of the commit. For this tutorial, use a public repository containing only demo code. A private repository requires repository access configured in NEXUS AI.
Step 7: Deploy using the CLI
If you prefer an AI assistant, skip to Step 8. You only need one deployment path.
Install the CLI and sign in:
npm install -g nexusapp-cli@latest
nexus auth login
nexus auth whoami
Deploy the GitHub repository with its MongoDB service:
nexus deploy source \
--repo https://github.com/YOUR_USERNAME/python-mongo-notes.git \
--branch main \
--name python-mongo-notes \
--provider docker \
--environment DEVELOPMENT \
--framework python \
--dockerfile Dockerfile \
--services mongodb \
--wait
Here is what matters in that command:
| Flag | Purpose |
|---|---|
--dockerfile Dockerfile |
Builds the app using your repository's Dockerfile |
--provider docker |
Selects NEXUS AI managed container infrastructure |
--services mongodb |
Adds the MongoDB sidecar and injects its connection environment |
--environment DEVELOPMENT |
Labels this tutorial deployment as development |
--wait |
Waits for the deployment to reach a terminal build/run state |
Do not pass your local .env file to this deployment. Its 127.0.0.1 address points to the app container in the cloud, which is a different machine from your laptop. The platform supplies the database connection instead.
Save the deployment ID from the output. Inspect its status and public URL:
nexus deploy status python-mongo-notes
For command details and version-specific options, see the NEXUS AI CLI reference.
Step 8: Deploy using MCP instead
MCP, the Model Context Protocol, lets an AI assistant call NEXUS AI deployment tools. You describe the task, and the assistant sends structured tool calls.
For Claude Code, connect the server:
claude mcp add --transport http nexus-ai https://mcp.nexusai.run/mcp
Complete browser sign-in when prompted. For Cursor, add the same URL in its MCP settings. The NEXUS AI MCP setup guide explains the connection flow.
Allow the deployment read, create, and log scopes needed for this workflow: deployments:read, deployments:create, and deployments:logs.
Then send your assistant this prompt, replacing the repository URL:
Deploy https://github.com/YOUR_USERNAME/python-mongo-notes.git from branch main to NEXUS AI. Name it python-mongo-notes, use the docker provider and DEVELOPMENT environment, build with the repository Dockerfile, and attach the mongodb service. Keep health checks enabled. Confirm my account and available providers first. Follow the returned deployment ID until it is running, inspect health, and return its public URL. If deployment fails, inspect the build and runtime logs. Do not print database credentials.
The assistant should first call nexusai_whoami and nexusai_providers_list with empty argument objects. The source deployment tool call then uses these arguments:
MCP tool: nexusai_deploy_source
{
"repoUrl": "https://github.com/YOUR_USERNAME/python-mongo-notes.git",
"repoBranch": "main",
"name": "python-mongo-notes",
"provider": "docker",
"environment": "DEVELOPMENT",
"framework": "python",
"dockerfile": "Dockerfile",
"services": ["mongodb"],
"healthCheckEnabled": true
}
These JSON blocks are MCP tool arguments for your assistant, not terminal commands. Tool names may have a client-specific prefix. If your connection does not expose the services field, update the connection before proceeding with this recipe.
With the returned ID, the assistant checks progress:
MCP tool: nexusai_deploy_status
{
"deploymentId": "REPLACE_WITH_RETURNED_DEPLOYMENT_ID"
}
Once running, it checks platform health:
MCP tool: nexusai_deploy_health
{
"deploymentId": "REPLACE_WITH_RETURNED_DEPLOYMENT_ID"
}
A queued deployment is not yet a working application. Keep the actual returned ID and follow status through completion.
Step 9: Verify the deployed app and MongoDB
Whichever deployment path you used, copy the actual public HTTPS URL from the deployment output. Set it in your terminal, replacing the example:
APP_URL='https://REPLACE_WITH_YOUR_APP_HOSTNAME'
curl --fail-with-body "$APP_URL/health"
The expected result is status: ok and database: connected. This explicit request tests the application's database check even if platform health checks use another route.
Create a note through the deployed API:
curl --fail-with-body -X POST "$APP_URL/api/notes" \
-H 'Content-Type: application/json' \
-d '{"text":"My Python app is live with MongoDB."}'
curl --fail-with-body "$APP_URL/api/notes"
Open the public URL in your browser. The same note should appear. Add another note through the form and refresh.
Checkpoint: the HTTPS page works, the API can write and read notes, and the database health check succeeds. You have verified the full request path from browser to Python to MongoDB.
Step 10: Read logs and deploy updates
If something fails, start with the logs.
CLI:
nexus deploy logs python-mongo-notes --type build --lines 100
nexus deploy logs python-mongo-notes --type runtime --lines 100
MCP tool: nexusai_deploy_logs
{
"deploymentId": "REPLACE_WITH_RETURNED_DEPLOYMENT_ID",
"type": "runtime",
"limit": 100
}
Use "type": "build" to inspect build failures.
| Symptom | First thing to check |
|---|---|
ModuleNotFoundError |
The missing dependency is in the committed requirements.txt |
KeyError: 'MONGO_URI' |
MongoDB was attached and its environment reached the app |
| MongoDB connection timeout | The database is running, and the deployed URI was not overwritten with localhost |
| Authentication failure | The injected database credentials match the existing database; inspect settings without printing secrets |
| App starts but URL fails | Gunicorn binds to 0.0.0.0 and the configured container port |
| Latest edits are missing | You committed and pushed them to the deployment's branch |
To release an update, commit and push the changed files, then run:
nexus deploy redeploy python-mongo-notes --wait
Or ask your assistant to use nexusai_deploy_redeploy:
{
"deploymentId": "REPLACE_WITH_RETURNED_DEPLOYMENT_ID"
}
For this repository-based deployment, omit name and provider overrides to use an in-place rebuild. Afterward, repeat the /health and notes checks. Existing notes should still be present; verifying this also catches accidental attachment to a new or empty database.
Before using the app for real users
The tutorial gives you a working deployment foundation. Before collecting real data, add authentication, per-user note ownership, and rate limits. Confirm persistent database storage and a backup policy, then test restoring a backup. App container storage and database backups serve different purposes.
If you choose MongoDB Atlas later, supply its connection URI through secure deployment settings and configure Atlas network access for your deployment's outbound traffic. Atlas permits connections from entries in its IP access list. For MCP workflows, keep credential-bearing URIs out of chat and tool arguments; configure them through the authenticated dashboard.
Start by saving one note, refreshing the page, and seeing it again at your public URL. From there, you can build on an application whose frontend, Python backend, and MongoDB connection have all been checked.