← Blog

Fazendo deploy de FastAPI na Hetzner

Railyard

6 de julho de 2026 · 7 min de leitura

Siga isso no seu próprio servidor. Comece grátis, sem cartão

uvicorn main:app --reload é como uma aplicação FastAPI roda num laptop; o que ela precisa pra produção é um gerenciador de processos de verdade, um banco de dados que sobrevive a um restart, migrations que rodam antes do código novo entrar no ar, e HTTPS na frente. Nada disso é específico do FastAPI exatamente, mas os detalhes diferem o suficiente de um deploy de Flask ou Django, um servidor ASGI em vez de WSGI, Alembic em vez das migrations embutidas do Django, que copiar um Dockerfile genérico de Python geralmente deixa alguma coisa pela metade. Este guia constrói a coisa de verdade em Docker num servidor Hetzner Cloud, com o Caddy cuidando dos certificados, e termina com uma aplicação FastAPI rodando no seu próprio domínio que você sabe como atualizar e avançar.

O que você vai precisar

  • Um servidor Hetzner Cloud. Um CX23 (2 vCPU, 4 GB de memória) a €5,99/mês sem IVA roda uma única aplicação FastAPI com seu banco Postgres ao lado com conforto; suba pra um CX33 (4 vCPU, 8 GB, €8,99/mês) se a aplicação também faz trabalho pesado em segundo plano.
  • Um domínio, com o DNS já apontado pro servidor antes de solicitar o HTTPS.
  • Uma conta Hetzner Cloud e um token de API (Security → API Tokens, Read & Write) se você quiser provisionar o servidor pela API em vez do console.
  • Uma aplicação FastAPI num repositório Git. Este guia usa o FastAPI 0.161.1, que exige Python 3.10 ou mais novo; o Dockerfile abaixo fixa o Python 3.13.
  • Docker e o plugin do Compose, localmente pra testar o build primeiro ou direto no servidor.

Passo 1: criar o servidor

No Hetzner Cloud Console, crie um projeto se precisar, depois Add Server: escolha Ubuntu 24.04, o tipo de vCPU compartilhada CX23, uma localização (Nuremberg, Falkenstein e Helsinki todas ficam na UE), adicione sua chave SSH, e crie ele.

ssh root@your-server-ip

Você deve cair num shell root sem nenhum pedido de senha.

Passo 2: instalar o Docker

curl -fsSL https://get.docker.com | sh
docker --version
docker compose version

Passo 3: escrever o Dockerfile

FROM python:3.13-slim

WORKDIR /app

RUN apt-get update \
    && apt-get install -y --no-install-recommends build-essential libpq-dev \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000
CMD ["fastapi", "run", "app/main.py", "--port", "8000"]

fastapi run é o comando de produção que os próprios docs do FastAPI apontam; ele roda o Uvicorn por debaixo sem um comando separado pra lembrar. libpq-dev e build-essential estão ali pro psycopg, que precisa compilar contra a biblioteca cliente do Postgres; remova-os se o seu requirements.txt usar psycopg[binary] ou asyncpg no lugar, já que os dois trazem wheels pré-compiladas.

Um requirements.txt mínimo pra este guia:

fastapi[standard]>=0.161
psycopg[binary]>=3.2
sqlalchemy>=2.0
alembic>=1.13

Construa uma vez pra confirmar que compila limpo:

docker build -t my-fastapi-app .

Passo 4: escrever o arquivo do Compose

services:
  app:
    build: .
    restart: unless-stopped
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy
    expose:
      - "8000"

  db:
    image: postgres:18-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 5s
      timeout: 5s
      retries: 5

  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy-data:/data
      - caddy-config:/config
    depends_on:
      - app

volumes:
  db-data:
  caddy-data:
  caddy-config:

Seu arquivo .env no servidor:

DB_NAME=app
DB_USER=app
DB_PASSWORD=change-this
DATABASE_URL=postgresql+psycopg://app:change-this@db:5432/app

Valide antes de iniciar:

docker compose config

Passo 5: escrever o Caddyfile

api.example.com {
    reverse_proxy app:8000
}

Passo 6: primeiro boot e migrations

docker compose up -d
docker compose exec app alembic upgrade head
docker compose ps

Você deve ver app, db e caddy todos running, e https://api.example.com deve responder com um certificado válido. Os docs interativos do FastAPI vivem em /docs por padrão; acessar isso deve mostrar os endpoints reais da sua API.

Passo 7: fazer deploy de novas versões

git pull
docker compose build app
docker compose up -d --no-deps app
docker compose exec app alembic upgrade head

Reconstruir só o app deixa db e caddy intocados, então o certificado e o banco de dados ficam no ar durante o deploy. Rodar a migration depois que o novo container já está de pé, em vez de como parte do build, significa que uma falha de migration aparece como um erro de comando claro em vez de uma imagem com deploy pela metade.

Backups

O Postgres no container db é a única peça de estado neste stack que não é reproduzível a partir do Git. Agende um dump diário pra fora do servidor:

docker compose exec -T db pg_dump -U app app | gzip > backup-$(date +%F).sql.gz

Copie esse arquivo pra armazenamento de objetos ou outra máquina num cron job, e restaure ele periodicamente num banco descartável pra confirmar que ele é de fato utilizável antes de você precisar dele de verdade.

O caminho mais curto: fazer isso com o Railyard

Deploy FastAPI to Hetzner roda a mesma receita a partir de uma URL de repositório, sem você escrever o Dockerfile, o arquivo do Compose ou o Caddyfile. Conecte um servidor com um token do Hetzner Cloud Console, ou traga um que você já provisionou. O Railyard detecta o FastAPI a partir do requirements.txt ou pyproject.toml, serve main:app na $PORT com o uvicorn (apontado pro seu próprio módulo automaticamente se a aplicação viver em outro lugar), e reconhece psycopg, asyncpg ou SQLAlchemy como uma dependência pra provisionar o Postgres com DATABASE_URL injetado. Defina alembic upgrade head como o comando de release em Settings → Deploy pra migrar em cada release, o mesmo que o Passo 6 acima mas rodado automaticamente antes de cada versão nova receber tráfego. Qualquer processo num Procfile se torna um worker que você pode escalar independentemente do processo web. Veja os docs de deploy from a repository pras regras exatas de detecção.

Resolução de problemas

  • connection refused no primeiro boot: o app iniciou antes do Postgres terminar de inicializar. depends_on com uma condição de healthcheck (como no Passo 4) deveria prevenir isso, mas se você remover o healthcheck, adicione um loop curto de retry em volta do primeiro comando de migration em vez de assumir que o banco está pronto no instante em que o container inicia.
  • Erros 500 sem nenhum detalhe na resposta: o FastAPI não expõe tracebacks nas respostas por padrão fora de ferramentas de debug; cheque docker compose logs app pra ver a exceção real.
  • ModuleNotFoundError pro psycopg em tempo de execução mesmo com um build limpo: geralmente uma incompatibilidade entre psycopg (a variante pure Python) e psycopg[binary] ou psycopg2-binary entre edições do requirements.txt; fixe um explicitamente em vez de deixar um lockfile desatualizado resolver um diferente do pretendido.
  • O HTTPS nunca é emitido, o Caddy continua tentando de novo: o DNS ainda não está apontando pro servidor. dig +short api.example.com deve bater com o IP do servidor.
  • O Alembic diz “Target database is not up to date” no deploy: a migration de um deploy anterior não rodou, geralmente porque o comando de migration do Passo 7 foi pulado manualmente uma vez. Rode docker compose exec app alembic current pra ver onde o banco realmente está versus alembic heads pra onde o código espera que ele esteja.

Perguntas frequentes

Por que fastapi run em vez de chamar o uvicorn direto? Os dois fazem a mesma coisa por debaixo; fastapi run é o comando que os próprios docs do FastAPI recomendam pra produção nas versões que este guia cobre, já que ele escolhe padrões sensatos sem flags extras.

Eu deveria usar múltiplos workers do Uvicorn num único container? Os próprios docs de deploy do FastAPI recomendam contra isso pra qualquer coisa rodando sob Docker Compose, Kubernetes, ou similar: deixe o orquestrador rodar múltiplas réplicas de container em vez de um container fazendo malabarismo com múltiplos processos worker, já que réplicas podem ser reiniciadas, escaladas e verificadas independentemente.

Postgres ou outra coisa? O SQLAlchemy e a maior parte do ecossistema FastAPI são agnósticos a banco de dados; este guia usa Postgres já que é a combinação mais comum, mas trocar a imagem do db e o esquema do DATABASE_URL funciona do mesmo jeito pro MySQL.

Um CX23 é realmente suficiente? Pra uma única API servindo volumes de requisição típicos, sim; 4 GB se dividem com conforto entre o container da aplicação e o Postgres. Trabalho pesado em CPU em segundo plano (processamento de dados grande, inferência de ML) é um motivo pra subir de tamanho.

E se minha aplicação usa uma app factory em vez de uma variável app no nível do módulo? Aponte o CMD do Dockerfile ou o comando detectado pelo Railyard explicitamente pra ela, por exemplo fastapi run app/main.py --port 8000 se torna a chamada equivalente contra um nome de função factory se for assim que sua aplicação está estruturada; o exemplo deste guia assume uma instância app simples.

Isso funciona com endpoints só-async e endpoints síncronos misturados numa aplicação? Sim; o FastAPI roda operações de rota síncronas num thread pool automaticamente pra que elas não bloqueiem o event loop, o que é parte do motivo pelo qual um único processo Uvicorn trata uma mistura dos dois sem configuração extra do seu lado.

Uma aplicação FastAPI num servidor Hetzner que você mesmo provisionou custa €5,99 por mês, com cada dependência, versão do Python e etapa de migration explícita em três arquivos que você controla. Lançar uma versão nova depois que eles existem é um rebuild de um container e um comando de migration, nada escondido no meio do caminho.

Por que escolhem o Railyard para isso

  • Sua própria conta de cloud. Faça deploy no DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, ou em qualquer servidor Linux via SSH, cobrado direto pelo seu provedor, sem markup.
  • Um preço fixo por equipe. O Free é grátis para sempre: um servidor mais um servidor de banco de dados, três apps, sem cartão. O Pro custa US$19 por mês para até cinco servidores e apps ilimitados. O Team custa US$49 por mês com servidores ilimitados. Sem cobrança por app, por deploy, por assento ou por gigabyte.
  • Templates de um clique. Faça deploy de apps open source, como bancos de dados, analytics e ferramentas de dev, no seu próprio servidor em um clique.
  • Bancos de dados gerenciados. Postgres, MySQL e Redis com backups programados, verificados por restauração, e recuperação point-in-time.
  • Push-to-deploy. Faça deploy a partir do GitHub, GitLab ou da CLI, com HTTPS automático e releases sem downtime.

Pule a configuração do servidor.

Suba um repositório FastAPI pro Railyard e ele provisiona o Postgres, roda as migrations, e faz deploy em cada push.

Plano grátis, sem cartão, e 14 dias de Pro para cada equipe nova. Um preço fixo por equipe.