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:

  1. Detects Python from pyproject.toml (or requirements.txt, Pipfile, setup.py).
  2. Picks the Python version from requires-python or .python-version.
  3. Picks the right package manager (uv, pip, poetry, pipenv) from the lockfile or config.
  4. Installs dependencies inside a clean container.
  5. Picks a sensible start command — for FastAPI, uvicorn app.main:app --host 0.0.0.0 --port $PORT.
  6. 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:

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:

  1. Push the repo to GitHub.
  2. Sign in at tavin.cloud, connect the repo, pick the branch, and pick the listening port (e.g. 8000).
  3. tavin.cloud detects Python, installs dependencies, starts Uvicorn, and exposes the service on a tavin.app URL.
  4. Open the URL. GET / returns the JSON. GET /docs opens the auto-generated OpenAPI docs.
  5. Add a custom domain when you are ready: a CNAME in 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:

A reasonable agent flow for “deploy this branch to staging and tell me when it is healthy”:

  1. Agent calls init_from_repo({ repoUrl, branch: "staging" }).
  2. Agent calls set_env({ projectId, key: "DATABASE_URL", value: "..." }) for any new variables.
  3. Agent calls deploy_project_source({ projectId, branch }).
  4. Agent calls wait_for_deployment({ deploymentId }) — gets back status: "succeeded" and the live URL, or status: "failed" and the build log handle.
  5. 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:

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:

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.