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¶
- Arquitetura de produção
- Pré-requisitos do servidor
- Setup inicial (primeira vez)
- Configurar o arquivo .env
- Build e subida dos containers
- Configurar SSL com Let's Encrypt
- Configurar Evolution API (WhatsApp)
- Atualizar o sistema em produção
- Comandos úteis
- Notas de operação
- 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çodbnodocker-compose.prod.ymle 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_KEYdo.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 restartnão recarrega código Python — o código é copiado para dentro da imagem nobuild(COPY . .no Dockerfile). Qualquer alteração em.pyou 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 containerwebrecebe 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
gccelibpq-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.ymlna raiz (navegação, tema escuro padrão, exclusão dedocs/superpowers/). - Build no deploy: o passo 2 do
deploy/update.shrodadocker run --rm -v "$APP_DIR":/docs squidfunk/mkdocs-material build— gera./site/sem instalar nada na VPS. - Serviço: o nginx monta
./siteem/usr/share/nginx/docs(volume emdocker-compose.prod.yml) e serve vialocation /docs/emnginx/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 nginxdo 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).