# Deploy

Requisitos, Docker, vários contêineres e hospedagem própria.

O Nimbly suporta deploy tanto em contêineres quanto manual numa VPS. Os contêineres empacotam a aplicação e o runtime juntos para atualizações de imagem repetíveis. O deploy manual numa VPS é igualmente válido para servidores Apache/PHP autogerenciados, quando você prefere controle direto sobre o servidor. Escolha o modelo que combina com a forma como o site será operado.

Todo site responde `/health` para a verificação de um balanceador de carga ou contêiner: `200` com `ok` quando o PHP roda e `ext/data` pode ser gravado, `503` caso contrário. A resposta nunca vai para cache. Uma rota `ext/uri/health` a substitui.

Atrás de um balanceador de carga que termina o TLS, informe o endereço dele no `.env`:

```env
TRUSTED_PROXIES=10.0.0.0/8
```

Endereços e faixas, separados por vírgulas. Uma requisição que vem diretamente de um deles recebe esquema, porta e endereço do visitante de `X-Forwarded-Proto`, `X-Forwarded-Port` e `X-Forwarded-For`, para que o cookie de sessão seja `secure`, os links absolutos comecem com `https://` e a pausa de login e as estatísticas contem o visitante, e não o balanceador. De qualquer outro endereço, e sem essa configuração, esses cabeçalhos são ignorados.

O log de acesso do próprio servidor web, e o que o lê (as regras para bots abaixo), também precisam do endereço do visitante. As imagens de contêiner cuidam disso ao iniciar, a partir da mesma configuração. Num servidor próprio, ative o `mod_remoteip` e coloque as linhas que este comando mostra no `<VirtualHost>` do site (não no `.htaccess`), de novo a cada mudança da configuração:

```bash
sudo a2enmod remoteip
./nimbly host:proxy-conf
```

Um bloqueio por endereço não impede nada atrás de um balanceador de carga: a conexão vem do balanceador. Faça o balanceador ou o firewall dele recusar os endereços que as regras encontrarem.

## Bots

Bots são bem-vindos quando seguem o `robots.txt`, pedem só endereços que o site publicou e leem no máximo uma página por segundo. O Nimbly responde corretamente e deixa o bloqueio para o servidor:

- **Armadilha.** Todo site tem um endereço, `/nb-` mais doze caracteres derivados do `PEPPER`, que o `robots.txt` proíbe e para o qual toda página tem um link invisível (`[#bot-tripwire-link#]` na shell da página). Um pedido a ele recebe o status `418`. O endereço é fixo por site e muda de um site para outro; uma shell de página própria também precisa do shortcode.
- **Regras do servidor.** `core/cli/setup/fail2ban/` contém três filtros do fail2ban e um arquivo de jail para um servidor com logs de acesso do Apache: bloqueio a qualquer resposta `418`, a mais de 60 requisições de página por minuto e a 20 respostas de não encontrado em dez minutos. Imagens, estilos, scripts, chamadas de API e telas do admin não são requisições de página, e uma imagem, estilo, script ou documento ausente não conta como não encontrado: é um link quebrado do próprio site. Copie os filtros para `/etc/fail2ban/filter.d/` e o arquivo de jail para `/etc/fail2ban/jail.d/`, acrescente seus próprios servidores a `ignoreip` e rode cada filtro com `fail2ban-regex` nos seus logs antes de ativá-lo.

## Deploy com Docker

A partir do checkout do projeto, gere os arquivos da imagem da aplicação em `ext/`:

```bash
./nimbly docker:init
```

Faça commit dos `ext/Dockerfile`, `ext/.dockerignore` e `ext/.github/workflows/docker-publish.yml` gerados no repositório `ext/`. A cada push, o GitHub Actions faz o build da imagem da aplicação a partir da imagem `master` atual do Nimbly e a publica no GHCR. O core publica continuamente `ghcr.io/nimbly-web-systems/nimbly-core:master`; os builds de aplicação gerados usam essa imagem e clonam o código `master` correspondente para o build dos assets.

Para testar o deploy localmente sem esperar que o GitHub Actions publique uma nova imagem do core, faça o build de uma imagem local do core e passe-a como base do build da aplicação:

```bash
docker build -t nimbly-core-local:master -f docker/prod/Dockerfile .
docker build -t nimbly-app-local:test \
  --build-arg NIMBLY_BASE_IMAGE=nimbly-core-local:master \
  -f ext/Dockerfile ext
```

Escolha a fonte de verdade dos dados de produção antes do deploy:

- **Imutável, de desenvolvimento para produção:** edite o conteúdo em desenvolvimento, faça commit em `ext/` e publique uma nova imagem. A produção não é usada como editor. Nenhum token Git com escrita é necessário.
- **Edição em produção:** os editores mudam conteúdo, envios ou registros no site em produção. Nesse modo, a produção grava em `ext/data/`, e essas mudanças precisam voltar para o repositório `ext/` antes que o próximo deploy substitua o contêiner.

Para edição em produção, a imagem de produção precisa incluir `ext/.git` e o contêiner precisa receber `GIT_TOKEN`. As agendas de prod e stage incluem o `ext:sync`, que faz commit das mudanças nos dados e as envia de volta ao repositório `ext/`. No GitHub, use um token fine-grained restrito ao repositório `ext/`, com **Contents: Read and write**. O contêiner configura as credenciais do Git a partir desse token ao iniciar; não faça commit do token nem o coloque dentro da imagem.

Para testes locais com Docker ou uma VPS autogerenciada, um arquivo env fora do Git serve:

```bash
docker run --rm --env-file .env.prod ghcr.io/your-org/your-nimbly-app:main
```

Mantenha `.env`, `.env.prod`, `.env.stage` e arquivos parecidos fora do Git. Faça commit só de arquivos de exemplo, como `.env.example` ou `.env.prod.example`, com valores fictícios. Em plataformas de contêineres gerenciadas, configure esses valores como variáveis de ambiente ou segredos de runtime:

- **AWS ECS/Fargate:** guarde os segredos no AWS Secrets Manager ou no SSM Parameter Store e referencie-os no bloco `secrets` da definição da tarefa.
- **AWS App Runner:** configure as variáveis de ambiente e os segredos de runtime no serviço.
- **Outros hosts de contêineres:** use o mecanismo de segredos e variáveis de ambiente da plataforma, não o build da imagem Docker.

Exemplo mínimo de Compose para produção:

```yaml
services:
  web:
    image: ghcr.io/your-org/your-nimbly-app:main
    restart: unless-stopped
    env_file:
      - .env.prod
    ports:
      - "80:80"
    environment:
      APP_ENV: prod
      SITE_URL: https://example.com
      SITE_NAME: Example
      MAIL_SERVICE: resend
      MAIL_FROM: no-reply@example.com
      MAIL_FROM_NAME: Example
    volumes:
      - nimbly-data:/var/www/nimbly/ext/data

volumes:
  nimbly-data:
```

Se o conteúdo de produção é editado no próprio lugar, não acrescente `.git` ao `ext/.dockerignore`. Sem `ext/.git` ou `GIT_TOKEN`, o contêiner ainda inicia, mas o `ext:sync` não consegue enviar as mudanças feitas em produção, e elas podem se perder no próximo deploy de imagem.

Atualizar contêineres é atualizar imagens: envie o `ext/`, deixe o workflow publicar uma nova imagem, faça pull dela no host e reinicie o contêiner. O contêiner roda o `system:setup` ao iniciar e o agendador a cada minuto. No modo de edição em produção, garanta que o `ext:sync` tenha enviado os dados atuais de produção antes de substituir o contêiner.

## Vários contêineres

Um site pode rodar em vários contêineres idênticos atrás de um balanceador de carga. Não há configuração para isso: tudo o que um site guarda é um arquivo em `ext/` (registros, envios, sessões, jobs, estado do agendador, estatísticas de requisições, caches), então os contêineres só precisam compartilhar essa pasta.

- **Um volume.** Monte o mesmo volume em `/var/www/nimbly/ext/data` em todos os contêineres. Com edição em produção, monte o `ext/` inteiro, para que haja um único checkout Git para o `ext:sync`, e não um por contêiner. O volume precisa suportar travas de arquivo (`flock`): NFSv4 e Amazon EFS suportam.
- **O mesmo `.env`.** Todos os contêineres recebem o mesmo `PEPPER` e as mesmas configurações.
- **Balanceador de carga.** Aponte a verificação dele para `/health` e defina `TRUSTED_PROXIES` (acima) com o endereço dele. Não são necessárias sessões fixas: um login num contêiner vale nos outros.
- **Agendador.** Todos os contêineres o rodam a cada minuto; uma trava no volume compartilhado faz com que só um deles execute o trabalho, então cada tarefa roda uma vez.
- **Gravações.** As mudanças nos registros de um recurso acontecem uma de cada vez em todos os contêineres, então dois editores salvando o mesmo registro não perdem nada.

```yaml
services:
  web:
    image: ghcr.io/your-org/your-nimbly-app:main
    restart: unless-stopped
    env_file:
      - .env.prod
    deploy:
      replicas: 2
    volumes:
      - nimbly-data:/var/www/nimbly/ext/data
      - nimbly-thumbs:/var/www/nimbly/ext/static/_thumb_

volumes:
  nimbly-data:
    driver_opts:
      type: nfs
      o: addr=10.0.0.10,nfsvers=4.1,rw
      device: ":/nimbly/example/data"
  nimbly-thumbs:
    driver_opts:
      type: nfs
      o: addr=10.0.0.10,nfsvers=4.1,rw
      device: ":/nimbly/example/thumbs"
```

As miniaturas ficam em cache em `ext/static/_thumb_`. Monte ali também um segundo volume compartilhado: senão cada contêiner gera as suas, e as miniaturas de uma imagem excluída ficam para trás nos contêineres que não trataram a exclusão.

Todo contêiner roda o `system:setup` ao iniciar e depois torna `www-data` dono dos arquivos do site, incluindo o volume compartilhado. Por isso o volume precisa permitir que o root troque donos (no NFS: `no_root_squash`, ou um export que já pertença a `www-data`), e um volume com muitíssimos arquivos deixa a inicialização lenta. Dois contêineres podem iniciar ao mesmo tempo num volume vazio.

`core/tests/multi-node.sh` testa isso numa estação de trabalho: ele inicia um site descartável em dois contêineres numa pasta (`docker/dev/docker-compose.nodes.yml`) e verifica o login, gravações simultâneas num registro, o agendador e as estatísticas de requisições. Ele só rodou num disco local, não num volume de rede nem atrás de um balanceador de carga real.

## Deploy manual numa VPS

O deploy manual numa VPS é suportado para instalações autogerenciadas. A base de um release é o mesmo caminho usado no CI:

```bash
./nimbly deps
APP_ENV=prod SITE_NAME="My Site" ADMIN_EMAIL=admin@example.com ADMIN_PASSWORD="change-me" PEPPER="$PEPPER" ./nimbly system:setup
./nimbly build
./nimbly help
find core -name '*.php' -print0 | xargs -0 -n1 php -l
```

Quando não há PHP no host, use a CLI e o lint de PHP via Docker:

```bash
APP_ENV=prod SITE_NAME="My Site" ADMIN_EMAIL=admin@example.com ADMIN_PASSWORD="change-me" PEPPER="$PEPPER" ./nimbly --docker system:setup
docker compose -f docker/dev/docker-compose.yml run --rm --build nimbly sh -lc "find core -name '*.php' -print0 | xargs -0 -n1 php -l"
```

Mantenha o `.env` no servidor de destino e garanta que ele tenha o `APP_ENV` de produção, um `PEPPER` estável, o `SITE_URL` correto e as configurações de e-mail. Rodar o `system:setup` de novo é seguro, inclusive quando um deploy precisa criar pastas, rotas, funções, `.jobs` ou `.htaccess` que faltam.

## Agendador e jobs

Em deploys manuais numa VPS, instale o orquestrador do agendador uma vez por servidor. Registre todo projeto publicado; o agendamento é necessário mesmo sem tarefas próprias da aplicação:

```bash
sudo php /var/www/site/core/cli/nimbly.php scheduler:install
sudo php /var/www/site/core/cli/nimbly.php scheduler:project:add site /var/www/site
sudo php /var/www/site/core/cli/nimbly.php scheduler:cron:install --user=www-data
```

Isso cria `/etc/cron.d/nimbly-scheduler`, que roda `/usr/local/bin/nimbly-scheduler-orchestrator` a cada minuto como `www-data`, e `/etc/logrotate.d/nimbly-scheduler`, que rotaciona o log do agendador semanalmente. O orquestrador então dispara os agendadores dos projetos registrados de forma independente. Em contêineres Docker o agendador roda sozinho, então não é preciso configurar o cron do host.

Rode o `schedule:init` uma vez para criar os arquivos de agenda do projeto em `ext/cli/`:

```bash
./nimbly schedule:init
```

Isso gera três arquivos:

| Arquivo | Usado quando |
|---|---|
| `ext/cli/schedule.inc` | `APP_ENV=dev` ou nenhum arquivo do ambiente encontrado |
| `ext/cli/schedule.prod.inc` | `APP_ENV=prod` |
| `ext/cli/schedule.stage.inc` | `APP_ENV=stage` |

As agendas de prod e stage incluem o `ext:sync`, que a cada hora faz commit das mudanças de dados em `ext/` e as envia de volta ao repositório. A agenda de dev não. Ajuste cada arquivo separadamente para tarefas específicas de cada ambiente.

Ao migrar uma VPS existente, primeiro instale o orquestrador, registre os projetos, rode `scheduler:run --dry-run`, rode o wrapper uma vez à mão como `www-data` e confira `/var/log/nimbly-scheduler.log`. Faça backup do crontab antigo de `www-data` em `/etc/nimbly/backups/` e depois remova só as linhas `schedule:run` por projeto que foram migradas. Não remova cron jobs que não têm relação com isso.

O `git pull` pelo admin continua disponível em Settings como uma opção simples de atualização para sites autogerenciados. Para sites com processo de release, prefira um caminho de deploy repetível: faça pull da revisão Git desejada, rode setup, build e lint, e garanta que o agendador esteja ativo.

---
