Deploy de app Python na VPS com Docker

Deploy de aplicação Python com Docker elimina o problema clássico de "funciona na minha máquina", o container carrega exatamente a versão do Python, as dependências e as variáveis de ambiente necessárias. O processo é o mesmo para Flask, FastAPI, Django, scripts de automação ou bots.

Dockerfile Python: boas práticas

A ordem das instruções no Dockerfile importa para o cache de build, copie requirements.txt antes do código para evitar reinstalar dependências a cada mudança de código:

dockerfile
# Dockerfile
FROM python:3.12-slim

# Variáveis que melhoram comportamento do Python em containers
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

WORKDIR /app

# Dependências do sistema (somente o necessário)
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    && rm -rf /var/lib/apt/lists/*

# Requirements antes do código (cache de camada)
COPY requirements.txt .
RUN pip install -r requirements.txt

# Código da aplicação
COPY . .

# Usuário não-root
RUN useradd --create-home appuser
USER appuser

EXPOSE 8000
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

requirements.txt: fixar versões

Fixar versões exatas no requirements.txt garante builds reproduzíveis, sem surpresas com breaking changes de atualizações automáticas:

bash
# Gerar requirements.txt a partir do ambiente atual
pip freeze > requirements.txt

# Ou usar pip-tools para gestão mais precisa
pip install pip-tools

# Criar requirements.in com dependências diretas
# requirements.in:
# fastapi
# sqlalchemy
# python-dotenv

# Gerar requirements.txt com versões pinadas (incluindo transitive deps)
pip-compile requirements.in

# Atualizar dependências (quando necessário)
pip-compile --upgrade requirements.in

# Instalar exatamente as versões especificadas
pip install -r requirements.txt

docker-compose.yml para aplicação Python

Configuração padrão para web app Python com banco de dados:

yaml
services:
  web:
    build: .
    restart: always
    env_file: .env
    ports: []                    # não expor diretamente — usar Nginx
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    networks:
      - app_net

  db:
    image: postgres:16-alpine
    restart: always
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASS}
      POSTGRES_DB: ${DB_NAME}
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 10s
      retries: 5
    networks:
      - app_net

volumes:
  db_data:

networks:
  app_net:

Virtual environments vs Docker: quando usar cada um

Em VPS com Docker, o container já é o isolamento, não é necessário venv dentro do container. Mas em VPS sem Docker, venv é essencial:

bash
# SEM Docker (venv no host):
# Criar venv
python3 -m venv /opt/minha-app/venv

# Ativar e instalar dependências
source /opt/minha-app/venv/bin/activate
pip install -r requirements.txt

# Rodar com systemd (sem ativar o venv manualmente)
# /etc/systemd/system/minha-app.service:
# [Service]
# ExecStart=/opt/minha-app/venv/bin/python -m uvicorn main:app
# WorkingDirectory=/opt/minha-app

# COM Docker (sem venv — o container já isola):
# No Dockerfile, instale diretamente no sistema Python do container
# pip install -r requirements.txt (sem venv)

Debug: entrar no container e inspecionar

Comandos para diagnosticar problemas em containers Python:

bash
# Entrar no container em execução
docker compose exec web bash   # ou sh para Alpine
docker compose exec web python  # console Python interativo

# Inspecionar variáveis de ambiente dentro do container
docker compose exec web env | grep -E "DB_|SECRET|ENV"

# Rodar script Python avulso no container
docker compose exec web python scripts/seed_database.py

# Ver logs de erros do Python
docker compose logs web --tail=50 | grep -i "error|exception|traceback"

# Verificar dependências instaladas
docker compose exec web pip list
docker compose exec web pip show fastapi

Não quer configurar manualmente?

Implante o VPS em menos de 5 minutos com a Runstack. Infraestrutura da OPEN Datacenter, com servidores em São Paulo.

Perguntas frequentes

python:3.12: imagem completa, ~900 MB. python:3.12-slim: Debian sem ferramentas de build, ~150 MB, ideal para a maioria dos apps. python:3.12-alpine: Alpine Linux, ~60 MB, menor mas usa musl libc que pode causar incompatibilidades com pacotes compilados (scipy, numpy, pillow). Recomendação: comece com slim; mude para alpine só se o tamanho for crítico.

Opção 1: cron no host chamando docker compose run --rm web python scripts/meu_script.py. Opção 2: container separado só para o cron (imagem ofelia ou supercronic). Opção 3: Celery Beat para schedules dentro da aplicação. Para scripts simples, cron no host é mais prático. Para scripts que precisam do contexto da aplicação (banco, config), Celery Beat é mais adequado.

docker compose exec web python manage.py migrate. Para deploy automatizado: adicione um container "migrate" no docker-compose que roda as migrations e sai antes do container web iniciar (depends_on com condition: service_completed_successfully). Isso garante que o banco está migrado antes do código novo processar requisições.

Use python-json-logger: from pythonjsonlogger import jsonlogger. Configure o handler para formatar logs em JSON. Docker coleta stdout/stderr, com logs JSON, ferramentas como Loki ou CloudWatch conseguem filtrar por campo (level, request_id, user_id). Evite logging para arquivo dentro do container, use stdout.

Monte o código como volume e use --reload: volumes: - .:/app e command: uvicorn main:app --reload --host 0.0.0.0. O Uvicorn detecta mudanças e recarrega automaticamente. Para Flask: flask run --debug com FLASK_ENV=development. Nunca use --reload em produção, causa instabilidade e expõe informações de debug.