Back to blog Tutorials

How to Deploy FastAPI with Docker in Brazil

A practical guide to deploying a FastAPI API in a container in Brazil, with uvicorn, environment variables, health checks, and HTTPS using Guara Cloud.

8 min read

By Guara Cloud Editorial

Tested with Python 3.12 / FastAPI 0.115 / Docker / Guara Cloud

FastAPI has become the default choice for building Python APIs. Pydantic validation, automatic OpenAPI docs, native async. Everything works great in development. The trouble starts when you need to ship to production. The uvicorn app:app command you run in your terminal is not meant for real traffic, containerizing the app has a few gotchas, and configuring workers and health checks is the kind of thing that gets forgotten until the first outage.

This tutorial covers the full path: a lean Dockerfile, production setup with uvicorn and gunicorn, health checks, environment variables, and deployment on Guara Cloud with HTTPS in São Paulo.

Quick answer

To deploy a FastAPI API in Brazil, create a Dockerfile with Python slim, set the run command with gunicorn and uvicorn workers, read the port from the PORT environment variable, add a /health endpoint, and publish the container on Guara Cloud. The platform handles HTTPS, public domain, and automatic restarts.

Key points

  • Use gunicorn with uvicorn workers in production. Uvicorn alone works, but gunicorn manages multiple processes and handles graceful reloads.
  • Read PORT from the environment. The platform sets the port, not you.
  • Add a /health endpoint that returns 200 when the service is ready to accept traffic.
  • Configure workers based on available CPUs. The general formula is 2 * CPUs + 1.
  • Use pydantic-settings to manage environment variables with type validation. It catches missing variables earlier than raw os.environ lookups.
  • Install dependencies with pip install --no-cache-dir and copy requirements.txt before the rest of the code to take advantage of Docker layer caching.

When this tutorial applies

Use this flow for REST APIs built with FastAPI that run as long-lived HTTP services. If your API connects to PostgreSQL, Redis, RabbitMQ, or any other external service, the container works the same way. The difference is in the environment variables you inject and the ports you need to open.

This also works for FastAPI applications that use WebSockets via Starlette. Uvicorn supports WebSockets natively, and gunicorn with uvicorn workers keeps that compatibility intact.

When not to use this flow

If your project uses FastAPI as part of a larger monorepo with multiple Python services, adjust the Dockerfile to install dependencies for the entire project and point to the correct module. If the application is a queue worker (Celery, Dramatiq) with no HTTP port, the deployment is similar, but you don’t need the HTTP health check endpoint or gunicorn. For those cases, the worker runs as a direct command in the Dockerfile.

If the application depends on CPython extensions that compile native code (for example, numpy with optimized BLAS or grpcio-tools), the base image might need to change from slim to bookworm to include system libraries. This increases image size from around 150MB to roughly 400MB, but the deployment flow stays the same.

Before you start

  • A FastAPI project with at least one working endpoint
  • Python 3.12+ installed locally
  • Docker installed to validate the image
  • A Guara Cloud account

1. Add a health check endpoint

The platform needs to know when the container is ready to receive requests. Add a simple endpoint that checks whether the service is responsive:

from fastapi import FastAPI, HTTPException
import asyncio

app = FastAPI()

@app.get("/health")
async def health_check():
    try:
        await asyncio.sleep(0)
        return {"status": "healthy"}
    except Exception as e:
        raise HTTPException(status_code=503, detail=str(e))

If your API depends on a database, it can be worth checking the connection in the health check. But be careful: if the database is down, the platform will keep restarting the container endlessly. I prefer having two separate endpoints: /health for liveness (the process is alive) and /ready for readiness (dependencies are reachable).

2. Configure environment variables with pydantic-settings

Instead of accessing os.environ directly, use pydantic-settings. This validates types and fails early if a required variable is missing.

Install the package:

pip install pydantic-settings

Create the config file:

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    app_name: str = "my-api"
    database_url: str = ""
    log_level: str = "info"
    workers: int = 2

    class Config:
        env_prefix = ""

settings = Settings()

The Config with env_prefix = "" reads variables directly (DATABASE_URL, LOG_LEVEL). If you prefer a prefix, change it to env_prefix = "APP_" and the variables become APP_DATABASE_URL, etc.

3. Production Dockerfile

The Dockerfile has two concerns: image size and build speed. Copying requirements.txt before the application code lets Docker reuse the dependency layer when only the code changes.

Dockerfile
FROM python:3.12-slim

WORKDIR /app

# Copy requirements first for layer caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application code
COPY . .

# Don't run as root
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser

ENV PYTHONUNBUFFERED=1

COPY entrypoint.sh .
RUN chmod +x entrypoint.sh

CMD ["./entrypoint.sh"]

A few things worth noting here:

Binding to 0.0.0.0 in gunicorn is mandatory. Without it, gunicorn only listens on loopback and the platform’s load balancer cannot reach the container.

PYTHONUNBUFFERED=1 makes sure logs appear in real time, without buffering. This matters a lot when you’re debugging something in production.

The useradd creates a non-root user. Running containers as root is a silly security risk that’s easy to avoid.

4. Entrypoint script with gunicorn and dynamic port

Create a script that reads the port from the environment and configures gunicorn:

entrypoint.sh
#!/bin/sh
PORT=${PORT:-8000}
WORKERS=${WORKERS:-2}

exec gunicorn app.main:app --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:$PORT --workers $WORKERS --timeout 120 --access-logfile - --error-logfile -

The --timeout 120 stops gunicorn from killing workers that take more than 30 seconds (the default). This matters when your API makes synchronous calls to external services or runs heavy database queries.

The --access-logfile - sends access logs to stdout, which is where Guara Cloud collects logs.

5. How many workers to use

The number of workers defines how many Python processes run in parallel. Each worker handles multiple simultaneous connections when uvicorn runs in async mode, but CPU-heavy processing still blocks the entire worker.

The formula is 2 * CPUs + 1. On Guara Cloud, if the container has 1 CPU, use 3 workers. For 2 CPUs, use 5.

# In the Guara Cloud panel, add the variable:
WORKERS=3

For APIs that do heavy I/O (database queries, HTTP calls to other services), FastAPI’s async handles concurrency within each worker. For APIs with CPU-bound work (machine learning, PDF generation), additional workers help, but the better approach is to move that work to a separate queue worker.

6. Deploy to Guara Cloud

Once the image is ready, deploy the service:

Steps to publish

  1. Go to app.guaracloud.com and create a new project
  2. Click "New Service" and select "Container"
  3. Connect your Git repository or provide the Docker image URL
  4. Set the HTTP port in the service configuration
  5. Configure environment variables in the panel (DATABASE_URL, LOG_LEVEL, WORKERS)
  6. Choose your resource plan (CPU and memory)
  7. Deploy

7. Set up environment variables

The Guara Cloud panel has an environment variable editor for each service.

Recommended variables

Variable Value
DATABASE_URL postgresql://user:***@host:5432/db
LOG_LEVEL info
WORKERS 3
APP_NAME my-api

Guara Cloud automatically injects the PORT variable when the service is created. You don’t need to configure it manually.

Troubleshooting

Common issues

Problem Container starts but returns 502 Bad Gateway
Solution Gunicorn is not binding to 0.0.0.0 or the wrong port. Check that entrypoint.sh uses --bind 0.0.0.0:$PORT and that the port configured in the panel matches the PORT variable. Check container logs to confirm gunicorn started successfully.
Problem ImportError: No module named app.main
Solution The module path in the gunicorn command is wrong. If the file is at src/main.py, the path should be src.main:app. Fix the entrypoint.sh script.
Problem Pip install is very slow during build
Solution Make sure requirements.txt is copied before COPY . . in the Dockerfile. This lets Docker cache the dependency layer and only reinstall when requirements.txt changes.
Problem Workers die with Worker timeout
Solution Increase the gunicorn --timeout. The default is 30 seconds. If the API makes external synchronous calls or runs heavy queries, 120 seconds is safer.
Problem Logs do not appear in the Guara Cloud panel
Solution Make sure PYTHONUNBUFFERED=1 is set and gunicorn logs go to stdout/stderr (not to files). Guara Cloud only captures what is written to standard file descriptors of the container.

FAQ

Do I need gunicorn or can I run just uvicorn in production?

Uvicorn alone works in production for low traffic (under 100 req/s). Above that, gunicorn with uvicorn workers is more stable because it adds process management, graceful restarts, and automatic worker respawning when a worker dies.

How many workers should I configure?

The general formula is 2 * CPUs + 1. For a container with 1 CPU, use 3 workers. For 2 CPUs, use 5. Monitor CPU usage after deployment and adjust as needed.

Can I use Poetry instead of pip?

Yes. Export dependencies with poetry export -f requirements.txt --output requirements.txt --without-hashes and use the same Dockerfile. If you want to keep pyproject.toml in the container, install Poetry in the build stage and run poetry install --only main.

How do I connect my FastAPI API to PostgreSQL on Guara Cloud?

Create the PostgreSQL service through the Guara Cloud catalog in the same project. The platform automatically injects connection variables (DATABASE_URL, DATABASE_HOST) as environment variables in your FastAPI service. Just read these variables in pydantic-settings.

Does the deployment support WebSockets with FastAPI?

Yes. Uvicorn supports WebSockets natively, and gunicorn with uvicorn workers preserves that compatibility. If your API uses WebSocket endpoints via Starlette, traffic goes through the same Guara Cloud load balancer.

Deploy your FastAPI API in Brazil

HTTPS, domain, logs, and billing in BRL. Container deployment with infrastructure in São Paulo.

Create free account