Python com Docker Compose e PostgreSQL Local

Aprenda a montar um ambiente local com Python, Docker Compose, PostgreSQL, FastAPI, variáveis de ambiente, migrações simples e testes reproduzíveis.

8 min de leitura Equipe Python Brasil

Um dos saltos mais importantes para quem estuda Python em 2026 é sair do projeto que só roda na própria máquina e chegar em um ambiente reproduzível. Vagas de backend, dados, automação e IA quase sempre citam Docker, PostgreSQL, variáveis de ambiente, testes e algum framework web. Mesmo quando a empresa não espera que uma pessoa júnior saiba Kubernetes, ela espera que o projeto abra sem drama: clonar, subir serviços, rodar testes e entender onde fica cada configuração.

Docker Compose resolve exatamente esse problema para projetos Python pequenos e médios. Em vez de instalar PostgreSQL manualmente, decorar porta, criar banco no clique e descobrir que cada colega usa uma versão diferente, você descreve os serviços em um arquivo compose.yaml. O resultado é um ambiente local mais próximo de produção, ótimo para portfólio, teste técnico, freelas e estudos.

Neste guia, vamos montar uma API Python simples com FastAPI, PostgreSQL, psycopg, variáveis de ambiente, script de inicialização e teste de conexão. A ideia não é criar uma arquitetura gigante. É mostrar o mínimo profissional: aplicação, banco, configuração, comandos claros e cuidados para não transformar Docker em caixa-preta.

Quando Docker Compose vale a pena

Use Docker Compose quando seu projeto depende de mais do que apenas Python puro. Alguns casos comuns:

ProjetoServiços típicos
API backendaplicação Python, PostgreSQL, Redis
dashboard internoapp Python, banco, worker de atualização
automação de CRMapp, fila, banco de logs
RAG com documentosAPI, PostgreSQL com pgvector, storage local
teste técnicoAPI, banco e dados de exemplo

Para scripts curtos, venv ou uv pode bastar. Para uma API com banco, Docker Compose deixa o projeto mais fácil de revisar. A pessoa avaliadora não precisa instalar PostgreSQL na mão; ela só roda um comando e testa.

Se você ainda está no começo, leia também ambientes virtuais em Python, uv como gerenciador de pacotes e Python e Docker. O Compose não substitui essas bases; ele organiza dependências externas.

Estrutura do projeto

Vamos usar esta estrutura:

python-compose-postgres/
  app/
    __init__.py
    main.py
    settings.py
    db.py
  tests/
    test_health.py
  compose.yaml
  Dockerfile
  pyproject.toml
  .env.example
  README.md

A separação é simples de propósito. settings.py carrega configuração, db.py concentra conexão com o banco, main.py expõe a API e tests/ prova que o serviço responde. Em projetos reais, você pode adicionar models.py, repositories.py, services.py e migrações com Alembic, mas não comece com camadas vazias apenas para parecer enterprise.

Criando o projeto com uv

O fluxo abaixo usa uv, mas você pode adaptar para pip e venv:

uv init python-compose-postgres
cd python-compose-postgres
uv add fastapi uvicorn psycopg[binary] pydantic-settings
uv add --dev pytest httpx ruff

Um pyproject.toml mínimo pode ficar assim:

[project]
name = "python-compose-postgres"
version = "0.1.0"
description = "API Python com Docker Compose e PostgreSQL"
requires-python = ">=3.12"
dependencies = [
    "fastapi",
    "uvicorn[standard]",
    "psycopg[binary]",
    "pydantic-settings",
]

[dependency-groups]
dev = ["pytest", "httpx", "ruff"]

A escolha por pyproject.toml ajuda em vagas e portfólio porque mostra um projeto moderno. Se a empresa ainda usa requirements.txt, tudo bem; o importante é ter um comando claro e reprodutível.

Configuração com variáveis de ambiente

Nunca coloque senha real de banco no código. Mesmo em projeto de estudo, use .env.example para documentar o que precisa existir:

DATABASE_URL=postgresql://app:app@db:5432/app
APP_ENV=development

Agora crie app/settings.py:

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    database_url: str
    app_env: str = "development"

    model_config = SettingsConfigDict(env_file=".env", extra="ignore")


settings = Settings()

Esse padrão combina bem com o guia de pydantic-settings. O ganho prático é falhar cedo: se DATABASE_URL não existir, a aplicação avisa na inicialização em vez de quebrar no meio de uma requisição.

Dockerfile para a aplicação Python

O Dockerfile abaixo usa imagem oficial, instala dependências e roda o servidor:

FROM python:3.12-slim

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

COPY pyproject.toml ./
RUN pip install --no-cache-dir fastapi "uvicorn[standard]" "psycopg[binary]" pydantic-settings

COPY app ./app

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Para um projeto de produção, você pode trocar o pip install direto por lockfile, usuário não root, multi-stage build e checagens de segurança. Para estudo e teste técnico, esse Dockerfile é compreensível e suficiente para mostrar o fluxo.

compose.yaml com PostgreSQL

Agora vem o coração do ambiente local:

services:
  app:
    build: .
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgresql://app:app@db:5432/app
      APP_ENV: development
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - ./app:/app/app

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
      POSTGRES_DB: app
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 5s
      timeout: 3s
      retries: 10
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

O detalhe importante é o healthcheck. depends_on sozinho só garante que o container do banco foi iniciado, não que o PostgreSQL já aceita conexão. Com condition: service_healthy, a aplicação espera o banco ficar pronto antes de subir.

Também repare no host da URL: dentro da rede do Compose, a aplicação acessa o banco pelo nome do serviço, db, não por localhost. localhost dentro do container da aplicação aponta para o próprio container, não para o banco.

Conexão e rota de saúde

Crie app/db.py:

import psycopg

from app.settings import settings


def buscar_versao_postgres() -> str:
    with psycopg.connect(settings.database_url) as conn:
        with conn.cursor() as cur:
            cur.execute("select version()")
            row = cur.fetchone()
            return row[0]

Agora app/main.py:

from fastapi import FastAPI

from app.db import buscar_versao_postgres

app = FastAPI(title="API Python com PostgreSQL")


@app.get("/health")
def health_check():
    return {"status": "ok"}


@app.get("/db-version")
def db_version():
    return {"postgres": buscar_versao_postgres()}

Suba o ambiente:

docker compose up --build

Em outro terminal, teste:

curl http://localhost:8000/health
curl http://localhost:8000/db-version

Se a segunda rota retornar a versão do PostgreSQL, a aplicação, a rede interna e a variável DATABASE_URL estão funcionando.

Inicializando tabelas sem complicar

Para um primeiro projeto, você pode criar uma função simples de inicialização:

import psycopg

from app.settings import settings


def criar_tabelas():
    with psycopg.connect(settings.database_url) as conn:
        with conn.cursor() as cur:
            cur.execute(
                """
                create table if not exists tarefas (
                    id serial primary key,
                    titulo text not null,
                    concluida boolean not null default false,
                    criada_em timestamptz not null default now()
                )
                """
            )

Em uma aplicação maior, use Alembic. O ponto para portfólio é mostrar que você sabe que schema do banco precisa ser versionado e reproduzido. Criar tabela manualmente no DBeaver e esquecer de documentar é um erro clássico em teste técnico.

Teste automatizado básico

Crie tests/test_health.py:

from fastapi.testclient import TestClient

from app.main import app


def test_health_check():
    client = TestClient(app)

    response = client.get("/health")

    assert response.status_code == 200
    assert response.json() == {"status": "ok"}

Rode localmente:

uv run pytest

Se quiser rodar testes dentro do container:

docker compose run --rm app pytest

Para que isso funcione, instale também as dependências de desenvolvimento na imagem ou crie um alvo específico para testes. Em projetos profissionais, é comum ter um Dockerfile mais completo para CI e uma imagem mais enxuta para produção.

Erros comuns

Usar localhost na DATABASE_URL dentro do container

Dentro do container, use db, não localhost:

# Errado dentro do Compose
postgresql://app:app@localhost:5432/app

# Certo
postgresql://app:app@db:5432/app

Subir sem healthcheck

Sem healthcheck, a API pode tentar conectar antes do PostgreSQL terminar de iniciar. Isso gera erro intermitente: às vezes funciona, às vezes quebra. Esse tipo de falha é ruim em teste técnico porque passa impressão de projeto frágil.

Commitar .env real

Use .env.example no repositório e coloque .env no .gitignore. Senhas, tokens e URLs reais nunca devem aparecer no Git. Mesmo que seja um banco local, o hábito importa.

Misturar migração, seed e aplicação

Criar tabela, inserir dados de exemplo e iniciar servidor são responsabilidades diferentes. Para estudo, uma função simples ajuda. Para produção, separe comandos: migrate, seed e start.

Não documentar comandos

Um bom README.md precisa dizer:

cp .env.example .env
docker compose up --build
curl http://localhost:8000/health
uv run pytest

Se a pessoa revisora precisa adivinhar o comando, seu projeto perde pontos mesmo que o código seja bom.

Como transformar isso em portfólio

Um projeto pequeno com Docker Compose pode ser mais forte do que um clone genérico de tutorial se resolver um problema reconhecível. Algumas ideias:

  • API de candidaturas com status, empresa, tecnologia e link da vaga;
  • controle de despesas com categorias e relatório mensal;
  • catálogo de livros, cursos ou ferramentas Python;
  • webhook de leads com deduplicação por e-mail;
  • mini pipeline que importa CSV, valida dados e salva no PostgreSQL.

Conecte isso ao seu currículo Python para vaga júnior, ao guia de projetos de portfólio Python e à página de vagas Python. O diferencial não é apenas ter Docker no repositório; é conseguir explicar por que cada serviço existe, como rodar, como testar e o que faria diferente em produção.

Relação com outras stacks

Python é excelente para APIs, automações, dados e protótipos de produto. Em ambientes com alta concorrência, algumas equipes usam Python junto com Go ou Kotlin. Uma API administrativa pode ficar em Python enquanto serviços de baixa latência rodam em Go para backend. O importante para quem está aprendendo é entender os fundamentos compartilhados: container, banco, configuração, logs e testes.

Se você domina esse fluxo em Python, fica mais fácil conversar com equipes de DevOps, dados e produto. Docker Compose vira uma ponte entre o notebook que roda sozinho e o sistema que outras pessoas conseguem operar.

Checklist final

Antes de publicar seu projeto, confira:

  • compose.yaml sobe aplicação e banco;
  • DATABASE_URL usa o host correto dentro da rede do Compose;
  • .env.example existe e .env não está no Git;
  • há uma rota /health simples;
  • o README tem comandos de instalação, execução e teste;
  • pelo menos um teste automatizado passa;
  • o projeto tem uma explicação de negócio, não apenas tecnologia;
  • código, comentários e README usam português brasileiro claro.

Docker Compose não é obrigatório para todo script Python, mas é uma habilidade muito vendável. Ele mostra que você entende ambiente, dependência externa, banco, configuração e reprodutibilidade. Para quem quer trabalhar com backend, dados, automação ou IA aplicada, esse conjunto aparece em vagas reais e diferencia o portfólio de quem só empilhou arquivos soltos.

E

Equipe Python Brasil

Contribuidor do Python Brasil — Aprenda Python em Português