Moving a FastAPI app's web service, background worker, cron job, Render Postgres and environment variables from render.yaml to your own AWS account, with Encore or with ECS and Terraform, and copying the data with pg_dump.
FastAPI apps on Render usually live in a render.yaml Blueprint with a web service, a background worker fed by Render Key Value, a cron job and a Render Postgres database. Moving to your own AWS account turns each of those entries into a separate AWS resource with its own networking, IAM and deploy step.
This guide moves an invoicing API with all of those pieces, first with Encore and then by hand with ECS on Fargate, RDS and Terraform. It then covers the database copy and the DNS switch.
This is the Blueprint being replaced:
services:
- type: web
name: invoices-api
runtime: python
plan: starter
buildCommand: pip install -r requirements.txt
preDeployCommand: alembic upgrade head
startCommand: uvicorn app.main:app --host 0.0.0.0 --port $PORT
healthCheckPath: /healthz
envVars:
- key: DATABASE_URL
fromDatabase: { name: invoices-db, property: connectionString }
- key: REDIS_URL
fromService: { type: keyvalue, name: invoices-queue, property: connectionString }
- key: STRIPE_API_KEY
sync: false
- type: worker
name: invoices-worker
runtime: python
buildCommand: pip install -r requirements.txt
startCommand: rq worker --url $REDIS_URL invoices
envVars:
- key: DATABASE_URL
fromDatabase: { name: invoices-db, property: connectionString }
- key: REDIS_URL
fromService: { type: keyvalue, name: invoices-queue, property: connectionString }
- type: cron
name: mark-overdue
runtime: python
schedule: "0 3 * * *"
buildCommand: pip install -r requirements.txt
startCommand: python -m app.jobs.mark_overdue
envVars:
- key: DATABASE_URL
fromDatabase: { name: invoices-db, property: connectionString }
- key: REDIS_URL
fromService: { type: keyvalue, name: invoices-queue, property: connectionString }
- type: keyvalue
name: invoices-queue
ipAllowList: []
databases:
- name: invoices-db
postgresMajorVersion: "17"
Encore combines an open source infrastructure SDK, added to the existing FastAPI app, with a platform that provisions what the code declares in your own AWS account. The database, queue, schedule and secret that render.yaml described become declarations in Python, and encore run starts Postgres locally with the API and delivers queue messages to the worker before anything is deployed.

The whole invoicing service fits in one module. The worker becomes a subscriber in the same process, and the cron service becomes a route the schedule calls:
import os
from contextlib import asynccontextmanager
import encore
import uvicorn
from encore import cron, pubsub, secrets
from encore.storage import postgres
from fastapi import FastAPI
from pydantic import BaseModel
# Replaces the Render Postgres instance and DATABASE_URL.
db = postgres.Database("invoices", migrations="./migrations")
# Replaces the STRIPE_API_KEY env var marked sync: false in render.yaml.
stripe_key = secrets.SharedSecret[str]("StripeApiKey")
class InvoiceCreated(BaseModel):
invoice_id: int
# Replaces the Render background worker and its Key Value queue.
invoice_created = pubsub.Topic[InvoiceCreated]("invoice-created")
async def send_invoice(msg: pubsub.Message[InvoiceCreated]) -> None:
# Only the first delivery moves the row from pending to sent,
# so a redelivered message changes nothing.
result = await db.exec(
"UPDATE invoices SET status = 'sent' WHERE id = $1 AND status = 'pending'",
msg.data.invoice_id,
)
if result.rows_affected == 1:
print(f"invoice {msg.data.invoice_id} sent (key set: {bool(stripe_key.value)})")
_ = pubsub.Subscription[InvoiceCreated](
invoice_created, "send-invoice", handler=send_invoice, max_retries=5
)
# Replaces the Render cron job service.
_ = cron.Job(
"mark-overdue",
path="/internal/mark-overdue",
title="Mark unpaid invoices overdue",
schedule="0 3 * * *",
)
@asynccontextmanager
async def lifespan(app: FastAPI):
await encore.start()
yield
app = FastAPI(lifespan=lifespan)
class NewInvoice(BaseModel):
customer_email: str
amount_cents: int
@app.post("/invoices", status_code=201)
async def create_invoice(body: NewInvoice) -> dict:
row = await db.query(
"INSERT INTO invoices (customer_email, amount_cents) VALUES ($1, $2) RETURNING id",
body.customer_email,
body.amount_cents,
).one()
await invoice_created.publish(InvoiceCreated(invoice_id=row["id"]))
return {"id": row["id"]}
@app.get("/invoices/{invoice_id}")
async def get_invoice(invoice_id: int) -> dict:
row = await db.query(
"SELECT id, status FROM invoices WHERE id = $1", invoice_id
).one()
return {"id": row["id"], "status": row["status"]}
@app.post("/internal/mark-overdue")
async def mark_overdue() -> dict:
result = await db.exec(
"UPDATE invoices SET status = 'overdue' "
"WHERE status = 'sent' AND created_at < now() - interval '30 days'"
)
return {"marked": result.rows_affected}
@app.get("/healthz")
async def healthz() -> dict:
return {"ok": True}
if __name__ == "__main__":
host, _, port = os.environ.get("ENCORE_LISTEN_ADDR", "0.0.0.0:8000").rpartition(":")
uvicorn.run(app, host=host or "0.0.0.0", port=int(port))
An encore.service.json next to the module names the service, and pyproject.toml adds encore-dev to the existing dependencies. The secret is set once per machine for local runs, and the endpoints behave as they did on Render:
Cron jobs do not run under encore run, so the overdue job is tested with curl -X POST localhost:4000/internal/mark-overdue. The route is reachable by anything that can reach the service, which is why it only changes invoices that are already past due. For production, set the secret with encore secret set --type production StripeApiKey, connect your AWS account and deploy. Encore creates RDS, the SNS topic and SQS queue with a dead-letter queue, the cron schedule and a Fargate service in your AWS account, and provisions an IAM role for the service. Each of your environments gets its own copies.
An AI agent porting the next RQ job writes one more subscriber, runs it locally and leaves the AWS side to the deploy. Cloud credentials stay with Encore's role in your own account rather than with the agent.
Encore applies the SQL files in migrations/ locally and on deploy, which replaces alembic upgrade head in preDeployCommand. For an existing database, the first migration is a baseline generated from Render's schema, shown in the data section below.
By hand, the web service and the worker each become an ECS service built from the same image, running as a non-root user:
FROM python:3.13-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN useradd --create-home --uid 10001 app
USER app
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
STRIPE_API_KEY moves to Secrets Manager, and the task execution role needs permission to read it before the task definition can reference it with valueFrom:
resource "aws_secretsmanager_secret" "stripe" {
name = "invoices/stripe-api-key"
}
resource "aws_iam_role_policy" "read_stripe_key" {
role = aws_iam_role.task_execution.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "secretsmanager:GetSecretValue"
Resource = aws_secretsmanager_secret.stripe.arn
}]
})
}
The value is stored once with aws secretsmanager put-secret-value --secret-id invoices/stripe-api-key --secret-string "$STRIPE_API_KEY", which keeps it out of Terraform state.
Around this sit the VPC and private subnets, the load balancer and target group, security groups between the load balancer, the tasks and RDS, an ECR repository, an SQS queue with a dead-letter queue and a task role for the worker to consume it, and an EventBridge Scheduler rule that runs the overdue job as a task. RQ needs Redis, so either the worker is rewritten to poll SQS or ElastiCache replaces Render Key Value. preDeployCommand becomes a one-off migration task in the deploy pipeline.
The copy is the same for both paths. First, generate the baseline migration from Render's schema and commit it as migrations/1_baseline.up.sql, so the deploy creates the tables. The external URL is in the Connect menu on the database page in the Render Dashboard. The grep strips session settings that are not valid in a migration file:
Then, at cutover, suspend the old Render mark-overdue cron service before the final dump and keep it suspended through the DNS observation and any rollback. Enable maintenance mode on the Render web service so no new invoices arrive (maintenance mode blocks public traffic, per the Render docs on October 28, 2026; if it is not available on your instance, suspend the web service instead), and wait for the worker to drain the queue. Pick a window away from the 03:00 run of the cron job.
The RDS instance sits in private subnets, so run the copy from an AWS CloudShell VPC environment in the Encore environment's VPC, and add an inbound rule for port 5432 on the RDS security group from the CloudShell security group. The database credentials are on the Infrastructure page of the Encore dashboard under Database Cluster, Users, as described in managing database users:
A data-only dump also carries sequence values, so new rows continue from the last Render ID. The -j 4 flag restores tables in parallel, as the pg_restore documentation describes; when tables reference each other through foreign keys, drop it so tables load in dependency order. AWS covers the same approach in importing PostgreSQL data into RDS. Compare row counts on the largest tables, then remove the temporary security group rule. A rehearsal of these steps against a 500-row copy restored every row and the next insert received ID 501.
In the DNS provider, lower the TTL on the existing record a day ahead. With Encore, add the domain under Settings, Custom Domains, choose the environment, and create a CNAME to custom-domain.encr.app, per the custom domains docs. Encore issues the certificate and the domain moves to Active once the CNAME resolves. Keep the Render services in place, with the web service in maintenance mode, until traffic on AWS looks normal for a few days.
You can sign up for early access to Encore for Python to try it with your FastAPI project.
Map each service in render.yaml to an AWS resource, deploy the app against a new RDS database, copy the data with pg_dump and pg_restore while the Render web service is in maintenance mode, and point your domain at the new deployment. With Encore, the Render database, worker, cron job and secrets become postgres.Database, pubsub.Topic with a Subscription, cron.Job and secrets.SharedSecret declarations in the FastAPI code, and Encore creates RDS, SNS and SQS, the schedule and Secrets Manager entries in your AWS account.
A queue and a consumer. On AWS that is usually SQS with an ECS service polling it, plus a dead-letter queue for messages that keep failing.
Use the external database URL from the Render Dashboard, dump with pg_dump in the custom format, and restore into RDS with pg_restore using --no-owner and --no-privileges. RDS runs in private subnets, so run the restore from a host inside the VPC, such as a CloudShell VPC environment.
Deploy to your cloud on AWS and Google Cloud
without managing infrastructure
Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.