CLI em JavaScript puro para deploy incremental e seguro em VPS Linux. Funciona diretamente no PowerShell: a máquina local precisa apenas de Node.js e de uma chave SSH; não usa rsync, WSL, MSYS2, scp nem o executável ssh local.
O transporte é feito com ssh2 (SSH + SFTP), e cada release tem um manifest SHA-256. Depois do primeiro deploy, só arquivos adicionados ou modificados são enviados; a nova release começa como cópia da release ativa e recebe as alterações.
npm install
npm link
autodeploy init # detecta docker/pm2, cria deploy.config.json e AUTODEPLOY.md
autodeploy doctor # confere SSH, docker/pm2, permissões na VPS
autodeploy deploy --dry-run
autodeploy deployinit pergunta só o essencial (host, usuário, health check opcional), adiciona deploy.config.json ao .gitignore e gera o AUTODEPLOY.md: um arquivo de contexto na raiz do projeto, com formato de skill, que explica a uma IA (Claude Code, Cursor...) como publicar, quais comandos usar, o que nunca fazer e como resolver erros comuns. Ele não é enviado à VPS.
Na VPS continuam necessários curl para health checks e PM2 ou Docker Compose conforme o runtime.
| Comando | O que faz |
|---|---|
init |
Cria config + AUTODEPLOY.md (--force recria; --host --user --port --health --yes para não-interativo) |
doctor |
Verifica a VPS antes do deploy |
deploy [--dry-run] [--yes] [--include-env] |
Deploy incremental com rollback automático |
status / logs [-f] [--tail N] |
Estado e logs da aplicação |
restart |
Reinicia a release ativa |
releases / rollback [id] |
Lista releases / volta para a anterior |
unlock |
Libera o lock de um deploy interrompido |
server add <nome> / server list |
Perfis de servidor reutilizáveis |
--yes nunca pergunta nada e nunca envia .env por conta própria (use --include-env ou envPolicy).
Só host, user e runtime são obrigatórios. Padrões: name vem do package.json/pasta, remotePath = /opt/apps/<name>, port 22, releases.keep 5.
{ "host": "1.2.3.4", "user": "deploy", "runtime": "docker",
"persistentPaths": "auto",
"docker": { "envFile": true, "requiredEnv": "auto" } }server: em vez dehost/user/port/privateKey, use"server": "minha-vps", cadastrado uma vez comautodeploy server add minha-vps(fica em~/.autodeploy/servers.json). Valores do projeto vencem os do perfil.docker.envFile: true: equivale a<remotePath>/.env.docker.requiredEnv: "auto": usa as${VAR}sem valor padrão do Compose.persistentPaths: "auto": usa os bind mounts relativos do Compose (./data:/app/data), exceto pastas que existem localmente e não estão no.gitignore(provável código-fonte). O deploy imprime o que deduziu.envPolicy:"ask"(padrão),"never"ou"overwrite": decide o que fazer com o.envsem perguntar toda vez.respectGitignore: true: soma os padrões do.gitignoreaoignore(sem suporte a!negação).- Chaves desconhecidas geram aviso com sugestão (
persistantPaths→persistentPaths).
Deploys e rollbacks ainda usam um lock remoto (.deploy.lock); se um deploy for interrompido, autodeploy unlock. Se o deploy falha antes de ativar, a release incompleta é removida. No PM2, o processo é recriado a partir de current a cada deploy (um pm2 restart reaproveitaria o diretório de uma release antiga).
autodeploy restart não envia arquivos nem cria release: ele reinicia a release apontada por current e executa o health check configurado.
Crie deploy.config.json na raiz do projeto:
{
"name": "project-botchat",
"host": "122.122.122.122",
"user": "user",
"port": 22,
"privateKey": "~/.ssh/id",
"localPath": ".",
"remotePath": "/var/www/project-botchat",
"runtime": "docker",
"ignore": ["node_modules", ".git", ".next", "dist", ".env", ".env.*"],
"commands": { "build": [] },
"docker": { "projectName": "project-botchat", "composeFile": "docker-compose.yml", "build": true, "force": false },
"health": { "url": "http://127.0.0.1:3002/status", "timeout": 30000, "interval": 2000 },
"releases": { "keep": 5 }
}ignore diz o que não é enviado. persistentPaths diz o que não é copiado da release anterior para a nova — são coisas diferentes, e é a segunda que interessa para dados de execução.
"persistentPaths": ["data"]Cada nome é um diretório na raiz da release, entra automaticamente no ignore e é criado em shared/ pelo deploy, com o dono da conta que deploya — antes que o container o crie como root e ninguém mais consiga olhar dentro. Serve para o que a aplicação escreve em tempo de execução: banco em arquivo, sessões, uploads, perfis de navegador. Sem isso a cópia entre releases arrasta esse conteúdo a cada deploy — e falha com Permission denied quando um container gravou ali como root, porque o usuário do deploy não consegue entrar no diretório. A CLI reconhece essa falha e diz qual nome acrescentar aqui.
Somente nomes de raiz são aceitos. data/perfil é recusado na validação: exclua data inteiro e aponte a aplicação para um caminho fora das releases, como <remotePath>/shared/data.
Se privateKey for omitido, a CLI procura ~/.ssh/id_ed25519 e depois ~/.ssh/id_rsa. O conteúdo da chave nunca é registrado em logs. Há exemplos completos para PM2 e Docker nos arquivos deploy.config.*.example.json.
Configs V2 permanecem aceitas: afterUpload, healthCheck e keepReleases continuam funcionando. As novas equivalências são commands.build, health e releases.keep.
- Lê a release apontada por
currente seu.autodeploy-manifest.json. - Escaneia os arquivos locais com SHA-256 por stream e mostra A/M/D e o total estimado de upload.
- Cria
releases/YYYYMMDD-HHmmss; se houver release anterior, fazcp -adela dentro da VPS, pulando ospersistentPaths. - Cria os diretórios necessários e envia apenas A/M via SFTP, com até quatro uploads simultâneos.
- Remove D apenas da nova release, grava o manifest e liga
shared/.envcomo.envda release quando existir. - Executa o build, atualiza
current, reinicia o runtime e aguarda o health check na VPS. - Se reinício ou health check falhar, restaura
currentpara a release anterior e reinicia novamente. Releases antigas são limpas somente após sucesso, e uma limpeza que falhe sai como aviso: a release nova já está ativa e saudável, então apresentá-la como erro levaria a desfazer um deploy que deu certo.
No primeiro deploy, todos os arquivos são tratados como adicionados. Sem mudanças, nenhuma release é criada. Em --dry-run, nada é criado, enviado, removido, construído, reiniciado ou ativado.
remotePathdeve ser um caminho absoluto específico; caminhos como/,/var,/var/www,/roote/tmpsão bloqueados.- Todos os caminhos de arquivos remotos passam por validação de pertencimento à release, bloqueando traversal (
..). .enve.env.*são protegidos por padrão. Antes do deploy interativo, a CLI pergunta se devem ser enviados; respondaspara incluí-los na release ou use--include-env. Quando enviados, eles não sobrescrevem os arquivos emshared/.- A release ativa nunca é alterada durante upload ou build.
Para PM2, informe pm2.script (ou pm2.restartCommand). Para Docker, informe docker.projectName e opcionalmente docker.composeFile. Defina docker.force: true para reconstruir todas as imagens sem usar cache: a CLI executará docker compose build --no-cache antes de docker compose up -d. O comando Docker e o PM2 executam a partir de current, portanto o rollback usa a release anterior corretamente.
Quando um deploy Docker termina com sucesso, a CLI salva a identidade do stack (nome do projeto e arquivo Compose) em .autodeploy-runtime.json, fora das releases. Se um deploy futuro mudar docker.projectName ou docker.composeFile, ela:
- remove o arquivo Compose antigo da cópia da nova release;
- executa
docker compose down --remove-orphansno stack anterior; - sobe somente o stack definido pela configuração nova.
Assim, containers órfãos e o Compose antigo não ficam ativos na VPS. A primeira execução após atualizar esta CLI grava esse estado sem alterar o comportamento normal.
Projetos já existentes que precisam trocar o Compose imediatamente, antes de haver esse estado remoto, podem informar a origem uma única vez:
{
"docker": {
"projectName": "novo-nome",
"composeFile": "compose.prod.yml",
"previousProjectName": "nome-antigo",
"previousComposeFile": "docker-compose.yml"
}
}Depois de um deploy bem-sucedido, remova previousProjectName e previousComposeFile: eles deixam de ser necessários.
Arquivos .env continuam protegidos por padrão. Use autodeploy deploy --include-env para enviar os .env locais: a CLI remove antes o symlink copiado da release anterior e grava o arquivo da nova release, sem sobrescrever shared/.env.
Use deploy.config.99-scrapper.example.json como configuração do projeto. O exemplo mantém data/ fora do manifest, do upload e da cópia entre releases. Portanto SQLite legado, sessão do WhatsApp e perfis do Chromium só podem existir em /opt/apps/99-scrapper/shared/data.
O arquivo de segredos existe somente na VPS, em /opt/apps/99-scrapper/.env, e a própria CLI o cria no primeiro deploy, a partir do .env local, com modo 600. Não há passo manual no servidor. Nos deploys seguintes ele é mantido como está — a VPS é a fonte da verdade dele — e responder que sim à pergunta sobre .env é o que autoriza sobrescrevê-lo com o local. A release recebe apenas um symlink para ele. Ele precisa conter valores reais para:
POSTGRES_DB=scrapper
POSTGRES_USER=scrapper
POSTGRES_PASSWORD=<defina-uma-senha-forte-na-vps>
POSTGRES_HOST_PORT=5433
APP_DATA_DIR=/opt/apps/99-scrapper/shared/dataCom docker.envFile configurado, antes de apontar current para a nova release a CLI exporta esse arquivo, confirma que todas essas variáveis não estão vazias e executa docker compose --env-file /opt/apps/99-scrapper/.env -f <nova-release>/docker-compose.yml config --quiet. Só depois executa o up -d --build. A release recebe apenas um symlink de .env para esse arquivo persistente, para manter compatibilidade com env_file: .env no Compose.