Docs / Working on a Python app
Guidedeploymill://guides/stack/python

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 on fastapi + uvicorn[standard].
  • Dockerfile — multi-stage build using python:3.12-slim + uv for 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

  1. Bind to 0.0.0.0, not 127.0.0.1. Uvicorn defaults to localhost; the scaffold passes --host 0.0.0.0 explicitly. Don't drop that flag.
  2. 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.
  3. 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.toml dependencies, then push_files. The build stage uses uv pip install --system so the deps land on PATH in the runtime image.
  • Switch to Django/Flask: keep the host/port contract and /healthz, update CMD to start the framework's server. Gunicorn or uvicorn workers both work; pick one and tune workers in CMD.
  • Multiple workers: for uvicorn add --workers N to CMD (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 src/templates/worker-python/ — no EXPOSE, 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. For asyncpg or async SQLAlchemy, change the pool import in app/db.py; the rest of the playbook still applies.
  • Add static files: mount them via FastAPI's StaticFiles so 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:app without --host 0.0.0.0. Localhost binding makes the container unreachable from the platform's router.
  • Don't run pip install at container start. It's a build-stage thing.
  • Don't python -m venv inside the Dockerfile. The slim base + uv pip install --system keeps the image lean; a venv adds layers without buying isolation.
  • Don't hard-code secrets in app/. Use set_env_vars and read from os.environ.

Debugging a failed deploy

  • list_deployments shows the most recent deploy and its status.
  • Build + container stdout is available via get_logs({ applicationId }). See deploymill://guides/logs for 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.
  • ModuleNotFoundError at startup → a dep is missing from pyproject.toml. The runtime stage installs ONLY what's declared.