Migrating production infrastructure? Get up to $10K in migration credits.

Apply now

Deploy a FastAPI app with Postgres and Render Workflows using Codex CLI

This walkthrough follows one Codex CLI session that deploys a FastAPI repo to Render with Render Postgres and a Render Workflows task. The repo has three parts:

  • An async SQLAlchemy data layer on asyncpg
  • Alembic migrations
  • One slow endpoint that builds a report export inside the request

Along the way, Codex hits five FastAPI-specific problems that generic deploy steps don't warn you about. For each one, you get the prompt you send and the fix it produces.

You need an existing FastAPI repo that uses asyncpg, plus a working Codex CLI install. If you don't have those yet, set them up first. This guide doesn't cover them.

The session runs in this order:

  1. Install the plugin.
  2. Prompt the deploy.
  3. Review the generated render.yaml.
  4. Validate it.
  5. Work through the fix loop.
  6. Move the slow work into Workflows.
  7. Verify through logs.

Install the Render plugin and invoke skills

Render agent skills add deployment, debugging, and monitoring capabilities to AI coding tools such as Codex. For setup in other tools, see render.com/agents. Install the official Render plugin from Codex's plugin marketplace:

  1. In a Codex session, open the plugin marketplace with /plugins.
  2. Search for render.
  3. Install the Render plugin.

The marketplace plugin gives Codex Render's agent skills and the Render MCP server, which lets Codex manage your Render infrastructure from natural language prompts.

As an alternative, you can install Render agent skills with the Render CLI. The render skills install command installs the skills only, without the MCP server, so if you take the CLI route, set up the MCP server separately. For MCP server setup, see render.com/agents. The render skills install command requires version 2.10 or later of the Render CLI. From your repo root, run:

The install command adds the skills, and list shows the installed skills and detected tools. For a markdown version of Render's coding agent docs, append .md to the URL.

Inside a Codex session, invoke a skill by prefixing its name with $. Later, you use the $render-workflows skill to move the report export out of the request path. A first invocation looks like this:

Ask for a plan before any edits. It shows you what the skill expects from your repo before it starts writing code.

Prompt the deploy and let Codex write render.yaml

Give Codex the whole stack up front. The more it knows about the driver and migration tool, the fewer guesses it makes.

Before Codex generates any config, it reads your repo:

  • requirements.txt to confirm fastapi, uvicorn, sqlalchemy, asyncpg, and alembic
  • app/main.py to find the app object
  • alembic.ini and alembic/env.py to see how migrations get their database URL

It then writes a first draft. The draft wires the database's connection string into DATABASE_URL. That's the internal URL, which Render recommends for services in the same region because it keeps traffic on the private network.

This draft has several problems:

  • The start command doesn't set a host or port.
  • The app consumes the raw connection string as-is.
  • There's no health check path.
  • There's no migration step.
  • The worker count is hard-coded.

The next sections find and fix each one.

Validate the Blueprint before deploying

Ask Codex to validate the file before anything ships:

Codex runs the CLI's Blueprint validator, which requires Render CLI v2.7.0 or later:

The validator checks YAML syntax, the schema (required fields and types), semantics such as valid plans and regions, and conflicts with existing resources in your workspace. It exits with a non-zero status when validation fails, so Codex can treat it as a gate. An illustrative, abbreviated result looks like this:

The draft passes, and that's the lesson. Validation catches config mistakes. Runtime problems, such as a server bound to the wrong interface or a driver that can't parse your URL, show up only in the deploy and its logs.

The fix loop: five FastAPI problems Codex hits mid-deploy

Now tell Codex to ship the validated Blueprint:

The instruction to stop and report matters. It keeps Codex from stacking several speculative fixes into one change, so you can see which fix resolved which failure. Each problem below surfaced in the deploy events or logs, in the order Codex hit them.

Uvicorn binds to localhost

Symptom: the deploy never goes live, and Codex reports that no open port was detected. Uvicorn binds to 127.0.0.1 on port 8000 by default. Render expects your app to bind to 0.0.0.0 on the port in the PORT environment variable, which defaults to 10000 for web services.

Why it's a FastAPI problem: local uvicorn defaults work fine on a laptop, so nothing in development warns you. Render forwards inbound traffic to only one HTTP port per web service, so binding to the right one matters.

The URL scheme doesn't match asyncpg

Symptom: with the port fixed, startup now crashes when the app creates its engine:

Render's connection strings use the plain postgresql:// scheme. SQLAlchemy maps a plain postgresql:// URL to psycopg2, because the PostgreSQL dialect uses psycopg2 as the default DBAPI. This repo installs only asyncpg, so engine creation fails at startup. SQLAlchemy's async engine needs the postgresql+asyncpg:// scheme, which names the asyncpg driver after the dialect.

Why it's a FastAPI problem: async SQLAlchemy picks its driver from the URL scheme, so a plain Postgres URL selects a sync driver your async app can't use. Rewriting the scheme in code leaves the injected env var untouched. Sharing one function also keeps Alembic's async env.py from drifting onto a different driver. This excerpt assumes an env.py generated from Alembic's async template (alembic init -t async), which runs migrations through an async engine. The default sync env.py can't use a postgresql+asyncpg:// URL. For details, see Using asyncio with Alembic.

No health check path

Symptom: Codex points it at /. In this repo, / sits behind an auth dependency and returns 401, so the new instance never passes its health check. Render cancels a deploy whose new instances don't pass within 15 minutes and keeps routing traffic to the existing instances. Render treats any 4xx or 5xx response as unhealthy and any 2xx or 3xx response as healthy.

Set the path with the healthCheckPath field in render.yaml. Render recommends operation-critical checks, such as executing a simple database query to confirm connectivity. That's why the route runs SELECT 1.

Why it's a FastAPI problem: global dependencies and middleware such as auth often wrap every route by default. Keep the health route outside them. If auth is an app-level dependency (FastAPI(dependencies=[...])), it applies to every route, including /healthz. Move it onto your routers (APIRouter(dependencies=[...])) and register /healthz directly on app, so only the router groups get the dependency. If auth is middleware, skip it when request.url.path == "/healthz".

Migrations never run

Symptom: the service goes live, but requests fail with relation "reports" does not exist. The tables are missing because no step in the Blueprint runs Alembic.

Why it's a FastAPI problem: the pre-deploy migration works only because the async env.py shares the asyncpg URL function from the URL scheme fix. Without that shared function, Alembic fails on the same driver error the app hit. Separately, putting alembic upgrade head && uvicorn... in the start command reruns migrations every time an instance starts. It can also race when several instances boot together. A dedicated pre-deploy step runs the migration once per deploy.

Too many workers for the instance

Symptom: logs show instances restarting under memory pressure. The hard-coded --workers 4 starts four processes, and each one loads its own copy of the app, SQLAlchemy engine, and connection pool.

Render sets WEB_CONCURRENCY based on the CPU count for the service's compute plan: 1 on the 0.5c-512mb plan and 2 on the 2c-4g plan. Render sets this default only for web and private services created after December 8, 2025. On older services WEB_CONCURRENCY is empty, and uvicorn's --workers flag defaults to $WEB_CONCURRENCY if available, or 1, so uvicorn starts one process unless you set it. This example uses the 2c-4g plan, so expect two server processes. On 0.5c-512mb, expect one. To override it, set WEB_CONCURRENCY yourself after you measure per-worker memory.

Why it's a FastAPI problem: each async worker already handles many concurrent requests, so extra processes mostly add memory. They also add database connections. Render Postgres instances with less than 8 GB of RAM allow 100 connections, and plans with more memory allow more. Each concurrent export_report run from the Workflows step below executes on its own instance with its own SQLAlchemy engine and pool, so its connections add to the web service's. Each worker process has its own SQLAlchemy engine, whose pool defaults to pool_size=5 plus max_overflow=10, so peak connections are roughly 15 x workers x instances for the web service, plus the connections each concurrent export run opens, and the total must stay under the limit.

The corrected render.yaml

Here's the Blueprint for the web service and database after the fix loop. It includes the RENDER_API_KEY variable the web service uses to trigger tasks. The reports-workflow service entry you add under services: follows in the Workflows step below:

For broader production structure, see FastAPI production best practices on Render.

Move the slow work into a Render Workflows task

The report export still runs inside the request. Hand it to the skill:

A workflow service pulls its task definitions from a Git repo, where you define tasks as Python functions with the Render SDK. Add render>=1.0.1 as a dependency in requirements.txt. Import TaskContext and Workflows from the render package, and give every task function a TaskContext as its first positional argument. Render supplies that argument to each run, so callers don't include it in the task input. For full signatures, see the Python SDK reference.

Each run executes in its own instance, which Render spins up on demand. That's why the task saves its output to Postgres instead of returning a local file path.

“
Task runs can't receive incoming connections. Workflows can send private network requests but can't receive them. You trigger individual task runs using the Render SDK or API.

Create and deploy the workflow service

The task code needs a workflow service to run in. A workflow service is a collection of tasks that you define as code and register with Render. Declare it in render.yaml alongside the web service and database, and give it the database connection string so save_export_to_db can reach Postgres:

Render documents Blueprint support for workflow services, which suits a workflow that ships beside other Render services. Codex adds this entry under services: in render.yaml:

Per the Blueprint YAML reference, a workflow service uses type: workflow and requires a region. Its slug sets the prefix of its task identifiers. The validator requires repo on a workflow even when you validate from inside that repo. Plans and pre-deploy commands aren't supported on workflow services, so leave them out (Blueprint YAML reference). The slug reports-workflow is the first part of the reports-workflow/export_report identifier the endpoint below uses, because task slugs have the format {workflow-slug}/{task-name}. The web service, workflow, and database share a region so the internal connection string works. Confirm that export_report shows up in the task list before you wire up the trigger. An illustrative, abbreviated result looks like this:

To assemble the complete render.yaml, add the reports-workflow entry above under services: in the corrected Blueprint, after reports-api. Check the Blueprint YAML reference for the current workflow fields.

Trigger the task from FastAPI

The FastAPI endpoint triggers the task and returns right away. For other trigger options, see Triggering Task Runs.

The SDK client detects the RENDER_API_KEY environment variable automatically. Codex adds that key to the web service as a secret you fill in, as shown in the corrected Blueprint above.

“
Blueprints can't yet manage workflows in preview environments, and Render skips creating preview copies of any workflows your Blueprint defines. Preview environments copy reports-api and reports-db but not reports-workflow.

For a comparison of these service types, see Workflows, workers, and cron jobs.

Verify the deploy through logs

Render's skills include debugging and monitoring capabilities, so you can ask Codex to read the logs for you. Give it a checklist that maps to each fix, so a missing line stands out:

This illustrative, abbreviated excerpt shows a line for each fix:

In this example, WEB_CONCURRENCY is 2. That's why the excerpt shows two server processes on the 2c-4g plan. Next, trigger the task:

Then ask Codex to follow the run:

A healthy result shows the run completing without errors and a matching row in Postgres. If the run never starts, check that you've set RENDER_API_KEY on the web service.

What you shipped

You installed the Render plugin into Codex, had the agent write, validate, and deploy a Blueprint, and then worked through five runtime fixes that validation couldn't catch. Last, you moved the slow export into a Workflows task triggered from FastAPI.

Problem
Fix
Deploy found no open port
--host 0.0.0.0 --port $PORT
Async engine failed on the URL scheme
Rewrite to postgresql+asyncpg:// in config, shared with Alembic
Health check hit an auth-protected route
Unauthenticated /healthz plus healthCheckPath
Missing tables
preDeployCommand: alembic upgrade head
Workers exceeded memory
Drop --workers and size from WEB_CONCURRENCY

For related reading, see FastAPI production best practices on Render and Workflows, workers, and cron jobs.

Frequently asked questions