Working on a Python app
Reference for an agent modifying a DeployMill-managed Python app scaffolded from the python template. Read it before editing app code, the Dockerfile, or pyproject.toml: it covers the layout DeployMill assumes, the contract that makes the deploy come up, and the modifications that work cleanly with the rest of the system. For library choices (not requirements), read deploymill://guides/conventions/python alongside this.
What DeployMill scaffolds
app/main.py: FastAPI app; exposes/(HTML) and/healthz(JSON).pyproject.toml: PEP 621 metadata, deps onfastapi+uvicorn[standard].Dockerfile: multi-stage build usingpython:3.12-slim+uvfor fast install. The build stage installs deps into the system site-packages; the runtime stage copies them over.EXPOSE 8000,CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"].
The platform contract
Three things must hold for the deploy to come up:
- Bind to
0.0.0.0, not127.0.0.1. Uvicorn defaults to localhost; the scaffold passes--host 0.0.0.0explicitly. Don't drop that flag. - Listen on port 8000. Unlike the Node template, the Python scaffold hard-codes 8000 in both the Dockerfile and
CMD. If you change it, change both. EXPOSE <port>in the Dockerfile must match--port. ThatEXPOSEline is what DeployMill reads to publish the container port (overridable byportin.deploymill/project.json). Pinned to 8000 in the scaffold.
Health checks
/healthz returns {"ok": true}, and the platform probes it — it's the default health path (DEFAULT_HEALTH_PATH), used unless health.path in .deploymill/project.json or a probePath argument to deploy overrides it. It's graded strictly: only a 200 counts as healthy; any other status, a connection error, or a timeout is unhealthy, and on an app armed with rollback: "auto" that triggers an automatic revert of the deploy. A 404 at the health path is the one soft case — the probe falls back to a lenient / check — but don't lean on that. If you move the route, point health.path at the new one; keep whatever you leave behind returning a real 200 only when the app is actually healthy.
Common modifications
- Add a dependency: edit
pyproject.tomldependencies, thenpush_files. The build stage usesuv pip install --systemso the deps land on PATH in the runtime image. - Switch to Django/Flask: keep the host/port contract and
/healthz, and updateCMDto start the framework's server. Gunicorn or uvicorn workers both work; pick one and tune workers inCMD. - Multiple workers: for uvicorn add
--workers NtoCMD(good rule of thumb: 2N+1 vs CPU cores). For gunicorn use-w N. - Run a headless background worker (queue consumer, scheduler, etc.): use
start_project({ stack: "python", workload: "worker" }). That scaffolds the Python worker starter: noEXPOSE, no domain, a stdout heartbeat loop as the starter. Replace the loop with real recurring work. The worker holds an active-app quota slot while running. - Async DB drivers: the database guide uses sync
psycopg. Forasyncpgor async SQLAlchemy, change the pool import inapp/db.py; the rest of the playbook still applies. - Add static files: mount them via FastAPI's
StaticFilesso they share the uvicorn process.
Persistent state
The container filesystem is ephemeral. To keep files across deploys (uploads, caches, an on-disk index), add a mount in .deploymill/project.json and read/write under that path. For relational app data, prefer the managed database instead of a volume. See deploymill://guides/storage.
Database
For Postgres, declare database: { engine: "postgres" } in .deploymill/project.json, run reconcile_project, then fetch deploymill://guides/database/python and follow it. Don't roll your own pool. The guide gives you the one that respects the database's connection limit.
There is no database provider to choose: DeployMill runs the Postgres itself, so there is no vendor account to connect and nothing to sign up for. engine is the only axis you set, and postgres is its only selectable value.
What NOT to do
- Don't
uvicorn app.main:appwithout--host 0.0.0.0. Localhost binding makes the container unreachable from the platform's router. - Don't run
pip installat container start. It's a build-stage thing. - Don't
python -m venvinside the Dockerfile. The slim base +uv pip install --systemkeeps the image lean; a venv adds layers without buying isolation. - Don't hard-code secrets in
app/. Useset_env_varsand read fromos.environ.
Debugging a failed deploy
list_deploymentsshows the most recent deploy and its status.- Build + container stdout is available via
get_logs({ applicationId }). For a running app's stdout/stderr, callget_logs({ applicationId, source: "runtime" }). Seedeploymill://guides/logsfor the full loop. - "App is not responding" usually means: localhost binding, port mismatch, or the import failed at startup. Run
get_logs({ applicationId })to read the build output. ModuleNotFoundErrorat startup → a dep is missing frompyproject.toml. The runtime stage installs ONLY what's declared.