Working on a Python app (deploymill)
Reference for agents modifying a deploymill-managed Python app scaffolded from the python template. Covers the layout deploymill assumes, the contract with the platform, and the moves that work cleanly with the rest of the system.
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. Build stage installs deps into the system site-packages; runtime stage copies them over.EXPOSE 8000,CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"].
The platform contract
- 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. Pinned to 8000 in the scaffold.
Health checks
/healthz returns {"ok": true}. Same convention as the Node stack — the platform doesn't require it, but it's the liveness probe target. If you remove it, leave a replacement at a stable path.
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, 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 scaffoldssrc/templates/worker-python/— 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
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: { provider: "neon" } 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.
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 }). Seedeploymill://guides/logsfor the full loop. Runtime container logs (stdout/stderr of a running app) aren't yet exposed over REST — use the edge-probe signal and a readiness route to narrow down runtime issues. - "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.