Pular para conteúdo

Deploy do EnergIA — VPS com Docker

Guia de deploy e atualização do sistema em produção.

Ambiente de produção atual: - Domínio: energiasm.online - OS: Ubuntu 24.04 LTS - Disco: 80 GB NVMe SSD · RAM: 2 GB · CPU: 2 vCore


Índice

  1. Arquitetura de produção
  2. Pré-requisitos do servidor
  3. Setup inicial (primeira vez)
  4. Configurar o arquivo .env
  5. Build e subida dos containers
  6. Configurar SSL com Let's Encrypt
  7. Configurar Evolution API (WhatsApp)
  8. Atualizar o sistema em produção
  9. Comandos úteis
  10. Notas de operação
  11. Tarefas agendadas (cron)

1. Arquitetura de produção

Internet
    │
    ▼ :80 (HTTP) / :443 (HTTPS)
┌──────────────────────────────────────────────┐
│           VPS — Ubuntu 24.04                 │
│  2 vCore · 2 GB RAM · 80 GB NVMe SSD         │
│                                              │
│  ┌──────────────────────────────────────┐   │
│  │           nginx:alpine               │   │
│  │  · Reverse proxy                     │   │
│  │  · SSL termination (Let's Encrypt)   │   │
│  │  · Serve /media/ diretamente         │   │
│  └──────────────────┬─────────────────┘    │
│                      │ :8000 (interno)       │
│  ┌───────────────────▼─────────────────┐   │
│  │  web (Django + Gunicorn)            │   │
│  │  4 workers · timeout 120s           │   │
│  │  Imagem: multi-stage build          │   │
│  └───────────────────┬─────────────────┘   │
│                       │                      │
│  ┌────────────────────▼────────────────┐   │
│  │  db (postgres:16-alpine)            │   │
│  │  Volumes: postgres_data             │   │
│  │  Bancos: energia_db, evolution_api  │   │
│  └─────────────────────────────────────┘   │
│                                              │
│  ┌─────────────────────────────────────┐   │
│  │  mqtt_subscriber (management cmd)   │   │
│  │  PYTHONUNBUFFERED=1                 │   │
│  └─────────────────────────────────────┘   │
│                                              │
│  ┌─────────────────────────────────────┐   │
│  │  evolution-api :8080                │   │
│  │  evoapicloud/evolution-api:latest   │   │
│  │  Instância: energiabot              │   │
│  └─────────────────────────────────────┘   │
└──────────────────────────────────────────────┘

Imagens Docker:

Serviço Imagem Tamanho
web energia-web (python:3.11-slim, multi-stage) ~2.5 GB
mqtt_subscriber mesma imagem (camadas compartilhadas) ~10 MB adicional
db postgres:16-alpine ~79 MB
nginx nginx:alpine ~25 MB
evolution-api evoapicloud/evolution-api:latest ~300 MB

Com 80 GB de disco, é possível trocar para timescale/timescaledb:latest-pg16 (~487 MB) se quiser ativar hypertables e agregação contínua em produção. Basta alterar a imagem do serviço db no docker-compose.prod.yml e remover as migrations condicionais.


2. Pré-requisitos do servidor

# Docker e Docker Compose
docker --version          # Docker 29.x+
docker compose version    # Compose v2.x

# Git
git --version

Instalar Docker (Ubuntu 24.04)

curl -fsSL https://get.docker.com | sh
usermod -aG docker $USER

Swap (opcional com 2 GB RAM)

O servidor tem RAM suficiente para builds normais. Swap só é necessário se builds de ML travarem:

fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab

3. Setup inicial (primeira vez)

# Clonar o repositório
git clone https://github.com/renatoteodoro/EnergIA.git /root/EnergIA
cd /root/EnergIA

# Criar e configurar o .env
cp .env.example .env
nano .env

4. Configurar o arquivo .env

# Django
SECRET_KEY=gere-com-python3-c-import-secrets-print-secrets.token_hex-50
DEBUG=False
ALLOWED_HOSTS=energiasm.online,www.energiasm.online

# Antes do SSL (HTTP): use http://
# Após o SSL (HTTPS): troque para https://
CSRF_TRUSTED_ORIGINS=https://energiasm.online,https://www.energiasm.online

# Hardening de transporte — só habilite após o SSL estar funcionando (seção 6);
# antes disso, HTTPS ainda não existe e SECURE_SSL_REDIRECT causaria loop de redirecionamento.
SECURE_SSL_REDIRECT=True
SESSION_COOKIE_SECURE=True
CSRF_COOKIE_SECURE=True

# Banco de dados
DB_NAME=energia_db
DB_USER=postgres
DB_PASSWORD=sua-senha-forte
DB_HOST=db
DB_PORT=5432

# MQTT (HiveMQ Cloud)
MQTT_HOST=seu-host.hivemq.cloud
MQTT_PORT=8883
MQTT_USERNAME=seu-usuario
MQTT_PASSWORD=sua-senha

# OpenAI (assistente IA)
OPENAI_API_KEY=sk-...

# Evolution API (WhatsApp)
# Em produção Docker: use o nome do serviço interno
EVOLUTION_API_URL=http://evolution-api:8080
EVOLUTION_INSTANCE=energiabot
EVOLUTION_API_KEY=sua-chave-aqui

# Clima
CLIMATE_LAT=-27.64
CLIMATE_LON=-48.67
CLIMATE_CITY=Palhoça, SC

Gerar SECRET_KEY:

python3 -c "import secrets; print(secrets.token_hex(50))"


5. Build e subida dos containers

cd /root/EnergIA

# Build (construa um serviço por vez para não pressionar memória durante pip install)
docker compose -f docker-compose.prod.yml build web
docker compose -f docker-compose.prod.yml build mqtt_subscriber

# Subir todos os containers
docker compose -f docker-compose.prod.yml up -d

# Verificar
docker compose -f docker-compose.prod.yml ps
docker compose -f docker-compose.prod.yml logs web --tail=30

# Criar superusuário
docker compose -f docker-compose.prod.yml exec web python manage.py createsuperuser

Neste ponto o sistema está acessível em http://energiasm.online (HTTP).


6. Configurar SSL com Let's Encrypt

6.1 Verificar DNS

Antes de emitir o certificado, confirme que o domínio aponta para o IP do servidor:

# No servidor
curl -s https://api.ipify.org    # seu IP público

# Em qualquer máquina
dig +short energiasm.online

Os dois devem retornar o mesmo IP.

6.2 Instalar Certbot no host

apt install -y certbot

6.3 Emitir o certificado (standalone)

A conf do nginx não serve o desafio ACME via webroot, então o método usado é o --standalone: o certbot sobe um servidor próprio na porta 80, e o nginx fica parado por ~30 segundos.

docker compose -f docker-compose.prod.yml stop nginx
certbot certonly --standalone \
  -d energiasm.online \
  -d www.energiasm.online \
  --email renatoteodoro2@gmail.com \
  --agree-tos \
  --non-interactive
docker compose -f docker-compose.prod.yml start nginx

6.4 HTTPS no nginx

O nginx/conf.d/energia.conf do repositório já vem com o bloco 443 ativo apontando para /etc/letsencrypt/live/energiasm.online/ (o host monta /etc/letsencrypt como volume somente leitura no container). Com o certificado emitido, basta subir o nginx — nada a editar.

Se o nginx não subir, verifique com docker compose -f docker-compose.prod.yml logs nginx — a causa mais comum é o certificado ainda não existir no caminho esperado.

6.5 Atualizar o .env para HTTPS

# Já deve estar assim se você seguiu a seção 4:
CSRF_TRUSTED_ORIGINS=https://energiasm.online,https://www.energiasm.online

Reinicie o container web para aplicar:

docker compose -f docker-compose.prod.yml restart web

6.6 Renovação automática do certificado

O certbot instalado via apt cria um timer systemd que roda certbot renew duas vezes ao dia — não é preciso cron. Porém, como a emissão foi --standalone, a renovação precisa da porta 80 livre. Configure os hooks que param e sobem o nginx (uma vez só):

cat >> /etc/letsencrypt/renewal/energiasm.online.conf <<'EOF'
pre_hook = docker compose -f /root/EnergIA/docker-compose.prod.yml stop nginx
post_hook = docker compose -f /root/EnergIA/docker-compose.prod.yml start nginx
EOF

# Testar a renovação de ponta a ponta (para o nginx por ~20s)
certbot renew --dry-run

# Conferir que o timer está ativo
systemctl list-timers | grep certbot

Sem os hooks a renovação falha silenciosamente (porta 80 ocupada pelo nginx) e o certificado expira após 90 dias.


7. Configurar Evolution API (WhatsApp)

A Evolution API roda como serviço Docker (evolution-api) e precisa de setup manual na primeira vez.

7.1 Criar o banco de dados auxiliar

docker compose -f docker-compose.prod.yml exec db psql -U postgres -c "CREATE DATABASE evolution_api;"

7.2 (Re)iniciar a Evolution API

docker compose -f docker-compose.prod.yml restart evolution-api
docker compose -f docker-compose.prod.yml logs --tail=20 evolution-api

A Evolution API roda as próprias migrations via Prisma ao iniciar.

7.3 Acessar o Manager

O manager está disponível em http://<IP-DA-VPS>:8080/manager:

  • Server URL: http://<IP-DA-VPS>:8080
  • API Key: valor de EVOLUTION_API_KEY do .env

Se a porta 8080 estiver bloqueada no firewall, libere temporariamente:

ufw allow 8080/tcp
# após configurar, bloqueie novamente:
ufw delete allow 8080/tcp

7.4 Criar a instância e conectar via QR Code

No Manager, crie a instância com o nome definido em EVOLUTION_INSTANCE (ex.: energiabot) e escaneie o QR Code com o WhatsApp do número do bot.

Importante: use evoapicloud/evolution-api:latest (não atendai/evolution-api). A imagem oficial trata corretamente o messageSecret do protocolo MLS — necessário para mensagens legíveis em iPhones.

7.5 Configurar o Webhook

Após conectar a instância, configure o webhook para apontar ao Django:

curl -X POST http://localhost:8080/webhook/set/energiabot \
  -H "apikey: <EVOLUTION_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook": {
      "enabled": true,
      "url": "https://energiasm.online/assistant/webhook/",
      "byEvents": false,
      "base64": false,
      "events": ["MESSAGES_UPSERT"]
    }
  }'

Para conferir a configuração atual:

curl -s http://localhost:8080/webhook/find/energiabot -H "apikey: <EVOLUTION_API_KEY>"

7.6 Cadastrar o número do usuário

No Django Admin (/admin/), edite o usuário e preencha o campo Número WhatsApp com o número no formato internacional sem +: 554899XXXXXXXX.


8. Atualizar o sistema em produção

Após git push de novas alterações:

cd /root/EnergIA
git pull origin main

# Rebuild (sem --no-cache reutiliza pip install do cache)
docker compose -f docker-compose.prod.yml build web
docker compose -f docker-compose.prod.yml build mqtt_subscriber

# Aplica migrations e reinicia sem derrubar o banco
docker compose -f docker-compose.prod.yml up -d --no-deps web mqtt_subscriber nginx

# Migrations (o web já aplica no startup, mas pode forçar)
docker compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput

# Limpar imagens antigas
docker image prune -f

9. Comandos úteis

# Status dos containers
docker compose -f docker-compose.prod.yml ps

# Logs em tempo real
docker compose -f docker-compose.prod.yml logs -f web
docker compose -f docker-compose.prod.yml logs -f mqtt_subscriber
docker compose -f docker-compose.prod.yml logs -f nginx

# Django shell
docker compose -f docker-compose.prod.yml exec web python manage.py shell

# Acessar o banco
docker compose -f docker-compose.prod.yml exec db psql -U postgres -d energia_db

# Reiniciar serviço específico (apenas para mudanças de config/env — NÃO recarrega código Python)
docker compose -f docker-compose.prod.yml restart web
docker compose -f docker-compose.prod.yml restart nginx

# Atualizar código Python (requer rebuild — restart sozinho NÃO recarrega .py)
docker compose -f docker-compose.prod.yml build web && docker compose -f docker-compose.prod.yml up -d web

# Parar tudo (mantém volumes/dados)
docker compose -f docker-compose.prod.yml down

# Parar e APAGAR todos os dados (CUIDADO)
docker compose -f docker-compose.prod.yml down -v

# Uso de disco
df -h /
docker system df

# Liberar espaço (imagens e cache não usados — NÃO use --volumes)
docker system prune -af

9.1 Reprocessar agregados de telemetria

Quando leituras atrasadas chegam via backfill do buffer offline do firmware (ver docs/hardware.md §7) fora da janela automática de 3 dias do TimescaleDB, os agregados contínuos (telemetry_hourly/telemetry_daily) daquele período ficam desatualizados até serem reprocessados manualmente:

# Sempre teste com --dry-run primeiro
docker compose -f docker-compose.prod.yml exec web python manage.py reprocess_readings --start 2026-01-01 --end 2026-01-15 --dry-run

# Aplicar de fato
docker compose -f docker-compose.prod.yml exec web python manage.py reprocess_readings --start 2026-01-01 --end 2026-01-15

Sem efeito (reporta e sai sem erro) em ambientes sem a extensão TimescaleDB. Operação idempotente — rodar para o mesmo intervalo mais de uma vez sempre recalcula a partir dos dados brutos atuais.


10. Notas de operação

restart vs build — quando usar cada um

Situação Comando
Mudança de variável de ambiente (.env) restart web
Mudança em docker-compose.prod.yml (env, volumes) up -d web
Mudança de código Python (.py, templates .html) build web && up -d web && docker restart energia-nginx-1
Mudança em requirements.prod.txt build web --no-cache && up -d web && docker restart energia-nginx-1

docker compose restart não recarrega código Python — o código é copiado para dentro da imagem no build (COPY . . no Dockerfile). Qualquer alteração em .py ou templates requer rebuild da imagem.

Após rebuild do web, reinicie sempre o nginx — o nginx resolve o DNS do upstream (web:8000) apenas na inicialização. Quando o container web recebe um novo IP interno após o rebuild, o nginx fica com o IP antigo em cache até ser reiniciado, causando 502.

Permissões do diretório de mídia

O Dockerfile cria /app/media com ownership do appuser (via mkdir -p /app/media && chown -R appuser:appuser /app). Em deployments novos isso é automático. Se o volume já existia com permissões erradas:

docker compose -f docker-compose.prod.yml exec --user root web chown -R appuser:appuser /app/media

Dockerfile multi-stage

  • Stage 1 (builder): instala gcc e libpq-dev, compila os pacotes Python.
  • Stage 2 (runtime): apenas libpq5. Resultado ~200 MB menor que uma imagem simples.

requirements.prod.txt vs requirements.txt

  • requirements.txt — lockfile completo (dev + prod), usado localmente.
  • requirements.prod.txt — sem flet, flake8, black, isort, cookiecutter. Usado no Dockerfile.

TimescaleDB (opcional em produção)

Em produção usa postgres:16-alpine. As migrations 0002_hypertable e 0003_continuous_aggregates detectam se create_hypertable está disponível e se ignoram quando não está. Para ativar TimescaleDB em produção (recomendado com 80 GB de disco), troque no docker-compose.prod.yml:

db:
  image: timescale/timescaledb:latest-pg16

SSH — chave do servidor

Se a VPS for reinstalada e aparecer WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED:

ssh-keygen -R <IP-do-servidor>


11. Tarefas agendadas (cron)

Nenhum agendador está embutido na aplicação (sem Celery/APScheduler) — os management commands abaixo são pensados para rodar via cron do sistema operacional, chamando o container web já em execução.

Sincronizar preços da tarifa branca (CELESC)

tariffs.sync_white_tariff busca os valores vigentes dos 3 postos horários em celesc.com.br/tarifas-de-energia e (1) atualiza toda Tariff cadastrada com o nome exato "CELESC Residencial Branca" (is_white=True) e (2) cria automaticamente essa mesma tarifa (sempre is_active=False) para todo owner que já tenha uma "CELESC Residencial" confirmada (cliente CELESC conhecido via fatura real enviada) mas ainda não tenha a versão branca — assim ela já fica disponível para simulação/comparação sem o usuário precisar cadastrá-la manualmente. Em nenhum dos dois casos o comando ativa uma tarifa ou contata a CELESC — migrar de modalidade tarifária de verdade continua sendo decisão do usuário, feita diretamente com a distribuidora, fora do EnergIA. Envia um alerta via WhatsApp ao dono da tarifa tanto na criação quanto na atualização de preço.

Os postos/valores só mudam no ciclo anual de revisão tarifária da distribuidora — uma checagem semanal é mais que suficiente:

# crontab -e (como root, ou usuário com acesso ao Docker)
0 6 * * 1 cd /root/EnergIA && docker compose -f docker-compose.prod.yml exec -T web python manage.py sync_white_tariff >> /var/log/energia-sync-tarifa.log 2>&1

Testar manualmente antes de agendar:

docker compose -f docker-compose.prod.yml exec web python manage.py sync_white_tariff

IP bloqueado pela CELESC (caso da VPS atual): o scraping falha em no-op. O caminho alternativo é obter os valores rodando o scraper de outra máquina (ex.: na sua estação, com o venv do projeto):

python -c "import django,os; os.environ.setdefault('DJANGO_SETTINGS_MODULE','core.settings'); django.setup(); from tariffs.services import fetch_celesc_white_tariff_rates; print(fetch_celesc_white_tariff_rates())"

e informá-los manualmente na VPS — mesmos efeitos do sync automático (atualização + criação para clientes CELESC), com a origem manual registrada em rate_source:

docker compose -f docker-compose.prod.yml exec web python manage.py sync_white_tariff \
  --peak 1.17082 --intermediate 0.79279 --off-peak 0.59715

Retenção de dados (LGPD)

core.purge_old_data remove leituras de telemetria além de 24 meses e eventos de degrau nunca identificados (status=pending) além de 90 dias — nunca remove eventos confirmados/automáticos. Ver docs/lgpd.md seção 5. Sugestão: mensal, com --dry-run na primeira execução para conferir o volume antes de agendar de fato:

docker compose -f docker-compose.prod.yml exec web python manage.py purge_old_data --dry-run

# depois de conferir, agendar:
0 5 1 * * cd /root/EnergIA && docker compose -f docker-compose.prod.yml exec -T web python manage.py purge_old_data >> /var/log/energia-purge.log 2>&1

exec -T (sem alocar TTY) é necessário para comandos disparados pelo cron, que não roda num terminal interativo.


12. Modo demonstração dos alertas

Os alertas de anomalia usam amostragem (1 a cada ANOMALY_EVAL_EVERY_N=5 leituras) e cooldown (ANOMALY_COOLDOWN_MINUTES=30 por medidor) — bom para produção, ruim para demonstração ao vivo. Para a demo (ver docs/alertas.md):

# Opção 1 — .env: ANOMALY_EVAL_EVERY_N=1 e ANOMALY_COOLDOWN_MINUTES=0,
# depois recriar os containers que leem o .env:
docker compose -f docker-compose.prod.yml up -d web mqtt_subscriber

# Opção 2 — só zerar o cooldown entre demonstrações (sem tocar no .env):
docker compose -f docker-compose.prod.yml exec web python manage.py reset_anomaly_cooldown

Lembrar de reverter o .env após a demonstração.


13. Site de documentação (/docs/)

A documentação deste diretório é publicada como site estático (MkDocs + tema Material) em https://energiasm.online/docs/, com botão de acesso na landing page:

  • Configuração: mkdocs.yml na raiz (navegação, tema escuro padrão, exclusão de docs/superpowers/).
  • Build no deploy: o passo 2 do deploy/update.sh roda docker run --rm -v "$APP_DIR":/docs squidfunk/mkdocs-material build — gera ./site/ sem instalar nada na VPS.
  • Serviço: o nginx monta ./site em /usr/share/nginx/docs (volume em docker-compose.prod.yml) e serve via location /docs/ em nginx/conf.d/energia.conf.
  • Primeira ativação: como o volume do nginx mudou, rodar uma vez docker compose -f docker-compose.prod.yml up -d nginx (o --no-deps nginx do update.sh já cobre os deploys seguintes).

Atenção: o site é público, sem autenticação — o conteúdo de docs/ deve continuar livre de credenciais e dados pessoais (as convenções do projeto já exigem secrets só no .env).