compartido-server-docker-deploy

Implanta uma aplicação Node (com dependências nativas ou Chromium) em um servidor Docker compartilhado, sem derrubar outras aplicações. Abrange: liberação de disco antes da construção, limites de memória por serviço, healthcheck apenas em serviços HTTP, compatibilidade de versão de módulos nativos com o Node, publicação atrás de um proxy Caddy compartilhado com autenticação básica, e validação/recarregamento do proxy sem downtime.

O que é compartido-server-docker-deploy?

Este skill garante a implantação segura de aplicações Node.js em servidores Docker que já hospedam outros projetos, evitando interrupções. Ele aborda cenários críticos como falhas de build por falta de espaço em disco, crashes de módulos nativos (como better-sqlite3 ou sharp) por incompatibilidade com a versão do Node, e a exposição de novos serviços atrás de um proxy existente (Caddy) com autenticação básica. O skill orienta sobre a separação de processos pesados (como Chromium) do servidor HTTP, a definição de limites de memória por serviço, e a validação e recarga do proxy Caddy sem downtime. Inclui também um checklist pré-implantação e um procedimento de deploy idempotente via SSH, com backup e validação do Caddyfile. É ideal para ambientes onde não se pode derrubar outras aplicações e onde a estabilidade e o uso eficiente de recursos são prioridades.

Funciona com~Claude Code~Codex CLI~Cursor
Part ofMobile
npx skills add https://github.com/leoarayas/agent-skills/tree/main/shared-server-docker-deploy

Installed? Explore more Programação e Desenvolvimento skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Deploy em servidor Docker compartilhado

Quando usar

  • O servidor não é dedicado: já executa deploys de outras pessoas ou outros projetos.
  • A aplicação tem dependência nativa (better-sqlite3, sharp, canvas) ou navegador (Chromium).
  • Já existe um proxy reverso (Caddy ou nginx) que publica vários domínios.
  • Qualquer EADDRINUSE, OOM ou disco cheio afetaria os outros stacks.

Regra de ouro: a nova aplicação não pode ser a causa da queda de outra. Por isso, tudo é delimitado e medido antes.


0. Antes de construir: medir e liberar disco

Um build com Chromium baixa ~2 GB de imagem. Se o disco está em 78%, o build falha ou deixa o servidor no limite, e o próximo prune apaga imagens de produção.

ssh SERVIDOR 'docker system df; df -h /'

O que geralmente está solto e é seguro liberar:

docker builder prune -af      # cache de build (geralmente o mais pesado)
docker image prune -af        # imagens não usadas
docker container prune -f     # contêineres parados

Antes e depois, conte os contêineres para confirmar que nada caiu:

docker ps -q | wc -l   # guardar o número, comparar depois

Medido em um caso real: 78% → 32% recuperados, com os 41 contêineres de produção intactos.


1. Módulos nativos e versão do Node

Um arquivo .node compilado para outra major do Node não falha com erro claro: ele aborta o processo com um assert interno e um stack nativo inútil.

Sintoma típico:

# node.exe[61084]: void __cdecl node::RemoveEnvironmentCleanupHook(Isolate *, CleanupHook, void *)
# Assertion failed: (env) != nullptr
----- Native stack trace -----

Causa: better-sqlite3@11 com Node 24. O suporte vem no better-sqlite3@12.

Regras:

  • Verificar a versão do módulo nativo contra a major do Node antes de integrar.
  • No Dockerfile de build, instalar o toolchain (python3 make g++) para que um binário pré-compilado que não exista possa ser compilado na hora.
  • Em runtime, não assumir que o toolchain é necessário: melhor uma imagem final menor.
  • Se o módulo é importado de vários pacotes do workspace, testá-lo em um processo que não faça mais nada: um crash por GC postergado pode aparecer horas depois.

2. Separar o processo pesado do que responde

Chromium e um módulo nativo no mesmo processo que o servidor HTTP causam crash e, além disso, bloqueiam o event loop durante os jobs longos.

api     : HTTP + SSE, memória baixa, healthcheck HTTP
worker  : fila + navegador, memória alta, sem portas, sem healthcheck HTTP

Eles se comunicam por um armazenamento compartilhado (SQLite, Redis, Postgres). A API não precisa saber se o worker está vivo.

O healthcheck é definido apenas no que serve HTTP. Herdá-lo do Dockerfile no worker o deixa unhealthy para sempre, porque ele nunca abre uma porta:

worker:
  healthcheck:
    disable: true

3. Delimitar memória por serviço

api:
  mem_limit: 400m
worker:
  mem_limit: 1600m
  mem_reservation: 400m
  volumes:
    - /dev/shm:/dev/shm:rw,size=256m   # Chromium usa /dev/shm; 64 MB o quebra
  tmpfs:
    - /tmp:size=256m
  security_opt:
    - no-new-privileges:true

Além disso, no código, reduzir o trabalho: o scraper deve ter um DETAIL_MAX / MAX_PAGES mais baixo no servidor do que localmente. Se algum dia faltar RAM no servidor, o culpado é quase sempre esse número, não o Docker.

Medir antes e depois de uma busca real:

docker stats --no-stream --format '{{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}' <api> <worker>
free -m | head -2

4. Build: estágios e caches que realmente funcionam

FROM mcr.microsoft.com/playwright:v1.63.0-noble AS build   # versão fixada ao pacote npm
...
FROM mcr.microsoft.com/playwright:v1.63.0-noble AS runtime
  • Fixar a versão da imagem à do pacote npm instalado. Se não coincidirem, o navegador não está onde o Playwright o procura.
  • Copiar primeiro os package.json e depois o código, para que COPY . . não invalide a camada de install.
  • Em runtime, pnpm/npm são necessários se os scripts de inicialização os usam → corepack enable.
  • Se node_modules é copiado do build e as permissões não coincidem, o pnpm tenta "repará-lo" a cada inicialização e falha. Isso é desativado com verify-deps-before-run=false em um .npmrc da imagem.
  • Usuário não root em runtime. As imagens do Playwright já trazem pwuser.

pnpm-lock.yaml deve ser commitado e o install do build deve ser com --frozen-lockfile.


5. Publicar por um proxy que já existe

Se já existe um Caddy publicando vários domínios, não levantar outro proxy nem abrir outra porta pública: adiciona-se um bloco ao Caddyfile existente e conecta-se à rede Docker.

services:
  api:
    networks: [edge_proxy]        # rede externa, já criada
networks:
  edge_proxy:
    external: true
    name: edge_proxy

Descobrir a rede e onde vive o Caddyfile:

docker inspect <proxy> --format '{{range .Mounts}}{{.Source}} -> {{.Destination}} rw={{.RW}}{{println}}{{end}}'

Editar o arquivo no host: se o bind mount é rw=false, escrever dentro do contêiner falha com Resource busy ou se perde ao reiniciar.

Procedimento seguro (nunca editar às cegas):

cp "$CADDYFILE" "$CADDYFILE.bak-$(date +%Y%m%d-%H%M%S)"   # 1. backup
# 2. adicionar o bloco
docker exec <proxy> caddy validate --config /etc/caddy/Caddyfile  # 3. validar
docker exec <proxy> caddy reload  --config /etc/caddy/Caddyfile   # 4. recarregar (sem downtime)

Se validate falhar, restaurar o backup. O reload não corta as conexões existentes.

Autenticação básica

prospector.exemplo.dev {
	basic_auth {
		leo $2a$14$...        # bcrypt, NUNCA em texto claro
	}
	reverse_proxy api:3000 {
		flush_interval -1     # imprescindível para SSE
	}
	encode zstd gzip
	header {
		X-Frame-Options DENY
		-Server
	}
}
  • Gerar o hash no servidor: docker exec <proxy> caddy hash-password --plaintext '...'.
  • Guardar a senha em texto claro apenas em um arquivo root (/root/<app>-password.txt, 600) para poder recuperá-la ou rotacioná-la, e nunca no repositório.
  • Não usar o proxy do Cloudflare na frente se a aplicação usa SSE: o timeout de 100 s e o buffering cortam o progresso. DNS-only (nuvem cinza) permite que a origem emita o certificado real.
  • basic_auth protege apenas o que passa pelo proxy. Se a aplicação também é exposta por outra porta, fica sem proteção: documente isso.

6. Implantar a partir do Windows

rsync não existe no Windows. O que funciona e é multiplataforma é tar por pipe SSH:

const tar = spawn('tar', ['-cf', '-', '--exclude', './node_modules', ...excludes, '.'], { cwd: ROOT });
const ssh = spawn('ssh', [HOST, `mkdir -p ${DIR} && tar -xf - -C ${DIR}`], { stdio: ['pipe', 'inherit', 'inherit'] });
tar.stdout.pipe(ssh.stdin);

spawn com shell: true é obrigatório para resolver pnpm no Windows (é um .cmd), mas então não é possível passar handles de arquivo para redirecionar a saída: use a variável de ambiente LOG_FILE e deixe que a própria aplicação escreva o log.


7. Antes de tocar no servidor compartilhado

  • docker ps -q | wc -l guardado, para comparar no final
  • Espaço em disco suficiente após liberar
  • mem_limit em todos os serviços novos
  • O processo pesado em seu próprio contêiner, sem portas
  • Healthcheck apenas em quem serve HTTP
  • Rede externa do proxy declarada, e não uma rede nova
  • Backup do Caddyfile e caddy validate antes do reload
  • Log em arquivo (o processo fica sem console)
  • Script de deploy idempotente: check / push / build / up / logs / stats / backup
  • Volume com backup testado e restaurado, não apenas criado

Diagnóstico rápido

docker stats --no-stream --format '{{.Name}} {{.MemUsage}} {{.CPUPerc}}' <api> <worker>
docker logs --tail 80 <worker>
docker exec <api> node -e "fetch('http://127.0.0.1:3000/api/health').then(r=>r.text()).then(console.log)"
free -m && df -h /

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

Habilidades Relacionadas