Deploying a Python FastAPI service from GitHub without writing a Dockerfile
Published 2026-05-08 by tavin
FastAPI is one of the most common Python web frameworks for new services in 2026 — async by default, Pydantic-validated requests and responses, and an OpenAPI schema that comes for free. It is also one of the easier Python services to deploy from a Git repo without any hand-written container recipe.
This post is a practical walkthrough: take a FastAPI repo on GitHub, give it a public HTTPS URL, and wire it so an AI coding agent can deploy and observe it.
A minimal FastAPI repo
Assume a small service:
# app/main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def root() -> dict:
return {"hello": "tavin.cloud"}
@app.get("/healthz")
def healthz() -> dict:
return {"ok": True}
# pyproject.toml
[project]
name = "hello"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115",
"uvicorn[standard]>=0.30",
]
[tool.uv]
dev-dependencies = []
The repo has uv for dependency management. (pip + requirements.txt works too; so does Poetry or pipenv. The auto-detector reads the lockfile and adapts.)
Why no Dockerfile is the right default
The Python deployment story has historically been “write a Dockerfile, juggle base images, hope pip install --no-cache-dir produces a small enough image.” Modern auto-builders flip that default.
Railpack is the auto-builder tavin.cloud uses. For a FastAPI repo it:
- Detects Python from
pyproject.toml(orrequirements.txt,Pipfile,setup.py). - Picks the Python version from
requires-pythonor.python-version. - Picks the right package manager (
uv,pip,poetry,pipenv) from the lockfile or config. - Installs dependencies inside a clean container.
- Picks a sensible start command — for FastAPI,
uvicorn app.main:app --host 0.0.0.0 --port $PORT. - Produces a normal OCI container image.
You do not write any of this. You can override any of it via env vars or a small config file when you need to. And the moment you genuinely need a Dockerfile — system packages, a custom binary, a non-standard runtime — you drop one at the repo root and the build switches to it. We covered that tradeoff in Railpack vs Dockerfile.
This is the escape-hatch principle applied to Python: auto-detect the common case, stay out of the way for the unusual case.
Uvicorn, Gunicorn, and the worker question
A FastAPI service almost always runs under Uvicorn. The interesting question is whether to put Gunicorn in front of it.
The short version:
- Single worker, async-bound workload, no CPU-heavy code — plain Uvicorn is fine.
uvicorn app.main:appand let the platform restart it on crash. - Multiple workers needed for CPU parallelism —
gunicorn -k uvicorn.workers.UvicornWorker -w N app.main:app. PickNbased on actual CPU available, not the legendary(2 * CPU) + 1. - You want the supervision behavior of Gunicorn (graceful reload, pre-fork worker pool) — same as above.
For most early services, plain Uvicorn under the platform’s process supervisor is the simpler answer. The PaaS already restarts the process if it dies; you do not need a second supervisor inside the container.
If your workload is truly CPU-bound — image processing, model inference, heavy data transformation — consider whether serverless or a queue worker is the better runtime shape before stacking workers inside a single container.
Environment variables and secrets
A real FastAPI service reads config from environment:
import os
DATABASE_URL = os.environ["DATABASE_URL"]
JWT_SECRET = os.environ["JWT_SECRET"]
Or, more idiomatic for FastAPI, Pydantic Settings:
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
database_url: str
jwt_secret: str
model_config = SettingsConfigDict(env_file=".env")
settings = Settings()
The platform should inject these at runtime, not bake them into the image. Set them through the dashboard, the CLI, or — if an AI agent is doing the work — through a scoped MCP tool. Never commit secrets to the repo, and never ship .env files to production.
A health endpoint and what to do with it
Add /healthz (or /health, /_health, whatever your team’s convention is). Make it cheap — no DB query, no external HTTP call. The platform pings it to decide whether the deployment is healthy.
If you want fancier checks (DB connectivity, Redis ping, downstream API), put them on /readyz and let your platform — or Kubernetes’ readiness probe model under the hood — distinguish “the process is alive” from “the process is ready to serve traffic.” For most small services, just /healthz is enough.
A walkthrough on tavin.cloud
The actual deploy:
- Push the repo to GitHub.
- Sign in at tavin.cloud, connect the repo, pick the branch, and pick the listening port (e.g.
8000). - tavin.cloud detects Python, installs dependencies, starts Uvicorn, and exposes the service on a
tavin.appURL. - Open the URL.
GET /returns the JSON.GET /docsopens the auto-generated OpenAPI docs. - Add a custom domain when you are ready: a
CNAMEin your DNS provider plus a domain in the project, and TLS is handled automatically via Let’s Encrypt.
The full step-by-step is in the quickstart. If you would rather drive this from a script, the tavin CLI docs cover the CLI surface.
Letting an AI agent deploy your FastAPI service
This is where the design choices around AI coding agents start to matter. A FastAPI service is a great candidate for agent-operated deployment: small surface, structured logs, an obvious health endpoint, and no special runtime requirements.
The pattern that holds up:
- The agent uses a scoped credential — OAuth, a Personal Access Token, or a one-time deploy intent. Not your dashboard cookie. Not a kubeconfig.
- The agent calls structured tools over MCP —
init_from_repo,deploy_project_source,set_env,wait_for_deployment,get_deployment_runtime_logs. - The agent’s calls are audited, the credential is revocable, and the agent never has authority it does not need for the current task.
A reasonable agent flow for “deploy this branch to staging and tell me when it is healthy”:
- Agent calls
init_from_repo({ repoUrl, branch: "staging" }). - Agent calls
set_env({ projectId, key: "DATABASE_URL", value: "..." })for any new variables. - Agent calls
deploy_project_source({ projectId, branch }). - Agent calls
wait_for_deployment({ deploymentId })— gets backstatus: "succeeded"and the live URL, orstatus: "failed"and the build log handle. - Agent reports to the human: “Deployed to https://hello-staging.tavin.app. Health check returns
{ ok: true }.”
The Python code did not change. The agent’s interface to the platform did. We argue this is the durable wedge in agent-safe deployment is the wedge.
Where this differs from serverless Python
Serverless Python — AWS Lambda, Vercel Functions, Cloudflare Workers (with Python), Google Cloud Run functions — is a different shape. Cold starts, request-scoped execution, tighter packaging constraints. Some FastAPI patterns survive that environment, but background tasks, long-lived WebSocket sessions, and stateful caching usually do not.
The rule of thumb from docker-paas vs serverless:
- Function-shaped workload: short, stateless, request-scoped → serverless is fine.
- Service-shaped workload: long-lived, background tasks, persistent connections → repo-to-container PaaS is usually simpler.
FastAPI services that are mostly request handlers with a thin DB layer can run on either. Services that maintain warm caches, run scheduled jobs in the same process, or hold open connections to message brokers are squarely in container territory.
A reasonable default for new Python services in 2026
For a new FastAPI service that needs to be on the public internet by the end of the day:
- Put it on GitHub.
- Skip the
Dockerfileuntil something forces you to write one. - Pick a host that auto-detects Python and gives you build logs, runtime logs, env vars, custom domains, and TLS for free.
- Issue a scoped credential to the AI agent in your editor so it can deploy without inheriting your account.
That is the deploy story tavin.cloud is built around. It is the same shape that already works for Node — the auto-detector handles the differences between Python and Node, and the rest of the platform stays identical.
If you want to try it on a real FastAPI repo, the quickstart is the shortest path to a live URL. If your service has unusual requirements — system packages, custom binaries, exotic Python builds — the Dockerfile escape hatch is always available.
The Python part of the story is the easy part. The interesting design work is everything around it — credentials, MCP tools, audit rows, deploy intents — and that is platform behavior, not framework behavior.