# Deploy em containers (front + back no mesmo servidor)

Front (Next) e back (PHP/Apache) rodam em containers no servidor `45.163.72.178`,
um par por ambiente. As portas são publicadas **só em `127.0.0.1`**; o nginx do
host expõe pra internet (`proxy_pass`, **sem rewrite de path**).

| Ambiente | Branch        | Front (host)     | Back (host)      | Path do back      | Dir no servidor      |
| -------- | ------------- | ---------------- | ---------------- | ----------------- | -------------------- |
| prod     | `main`        | `127.0.0.1:3000` | `127.0.0.1:8081` | `/back-end`       | `/opt/linksun/prod`  |
| homolog  | `homolog`     | `127.0.0.1:3001` | `127.0.0.1:8082` | `/back-end-hom`   | `/opt/linksun/hom`   |
| pre-of.  | `pre-oficial` | `127.0.0.1:3002` | `127.0.0.1:8083` | `/back-end-others`| `/opt/linksun/others`|

> ⚠️ **Hostname importa.** `front-end/services/api.tsx` (e `api3.tsx`) escolhe o
> backend por substring da URL do navegador: o host de homolog precisa conter
> `homologacao-linksun` e o de pre-oficial precisa conter `pre-oficial`. Qualquer
> outro hostname cai no default = **backend de produção**.

## 1. Pré-checks no servidor (antes do primeiro build)

```bash
php -v                       # fixar a MESMA versão na imagem (vars.PHP_VERSION no GitHub)
php -m                       # comparar extensões com as do docker/back/Dockerfile
docker --version && docker compose version
crontab -l                   # scripts CLI que precisam virar `docker compose exec`
ls /var/www/html/back-end/vendor/mpdf/mpdf/ttfonts | grep -i popp
ls -la /var/www/html/back-end | grep -iE 'credentials|\.env|key'
du -sh /var/www/html/back-end/arquivos_temp /var/www/html/back-end/backups
```

Dois resultados podem mudar a imagem:

- **`php -v`**: se o servidor não for 8.2, definir a variável `PHP_VERSION` no
  GitHub (repo ou ambiente) com a versão dele.
- **fonte `poppins`**: `back-end/processos/CXESVEN001/PDFProposta.php` usa
  `'default_font' => 'poppins'`, que **não existe** num `vendor/` limpo. Se o
  servidor tiver TTF/`FontVariables.php` customizados dentro de
  `vendor/mpdf/mpdf`, versionar esses arquivos no repo e copiá-los no
  `docker/back/Dockerfile` — senão o PDF de proposta quebra no container.

Também capturar do Vercel (`vercel env ls` ou dashboard) todos os `NEXT_PUBLIC_*`
dos 3 projetos, em especial **`NEXT_PUBLIC_CRYPTOPASS`** (cripta a sessão em
localStorage; valor diferente = todos os usuários deslogados).

## 2. Bootstrap de cada ambiente no servidor

```bash
ENV_NAME=hom   # hom | others | prod
sudo mkdir -p /opt/linksun/$ENV_NAME/{secrets,data/{arquivos_temp,backups,sessions}}
sudo chown -R linksun:linksun /opt/linksun/$ENV_NAME

# arquivos do repo
scp -P 1723 deploy/docker-compose.yml linksun@45.163.72.178:/opt/linksun/$ENV_NAME/
scp -P 1723 deploy/env/$ENV_NAME.env.example linksun@45.163.72.178:/opt/linksun/$ENV_NAME/.env

# DB_* do ambiente (mesmo conteúdo do back-end/.env atual daquele path)
cp /var/www/html/back-end-hom/.env /opt/linksun/hom/secrets/back.env

# conteúdo runtime já existente
cp -a /var/www/html/back-end-hom/arquivos_temp/. /opt/linksun/hom/data/arquivos_temp/ 2>/dev/null || true
cp -a /var/www/html/back-end-hom/backups/.      /opt/linksun/hom/data/backups/      2>/dev/null || true

# permissão pro www-data do container escrever nos volumes
sudo chown -R 33:33 /opt/linksun/$ENV_NAME/data

# login no GHCR (uma vez por servidor; PAT com read:packages)
echo "$GHCR_PAT" | docker login ghcr.io -u <usuario-github> --password-stdin
```

## 3. Configuração no GitHub

Secrets/variáveis usadas por `.github/workflows/containers.yml`:

| Onde | Nome | Uso |
| --- | --- | --- |
| Secret (repo) | `SSH_PRIVATE_KEY` | já existe (usado pelo `prod.yml`) |
| Secret (Environment `prod`/`hom`/`others`) | `NEXT_PUBLIC_CRYPTOPASS` | build do front |
| Secret (Environment) | `NEXT_PUBLIC_WEBHOOK_LEADS_URL` | build do front |
| Variable (Environment) | `NEXT_PUBLIC_NODE_API_URL` | build do front (default `https://api.customax.inf.br`) |
| Variable (Environment) | `NEXT_APP_API_URL` | build do front (`services/api2.ts`) |
| Variable (repo) | `PHP_VERSION` | versão do PHP na imagem do back (default `8.2`) |
| Variable (repo, opcional) | `DEPLOY_HOST` / `DEPLOY_PORT` / `DEPLOY_USER` | override do destino SSH |

Criar os **Environments** `prod`, `hom` e `others` (Settings → Environments) — o
workflow resolve os secrets por ambiente através deles.

## 4. Crons

Os CLIs do back (`ChamadaAPIBold.php`, `ChamadaAPIHelte.php`, `ChamadaAPIWeg.php`,
`ChamadaTasksDistribuidores.php`, `ChamadaVerificarEtapasParadas.php`) passam a
rodar dentro do container:

```cron
*/10 * * * * docker compose -f /opt/linksun/prod/docker-compose.yml exec -T back php /var/www/app/ChamadaAPIBold.php >> /var/log/linksun-cron.log 2>&1
```

(`docker compose` precisa do `.env` do diretório — usar `--env-file` ou `cd` antes
se chamar de outro path.) Os workflows `notifica-parcelas.yml` e
`sincronizacao_rafid.yml` batem em URL pública e continuam iguais.

## 5. Smoke test (antes de mexer no nginx)

```bash
cd /opt/linksun/hom && docker compose ps          # ambos "running (healthy)"

# back: JSON (não HTML/500) prova Apache + composer + Dotenv + PDO conectado
curl -s "http://127.0.0.1:8082/back-end-hom/index.php?class=NaoExiste&action=x&sigla=undefined"
# esperado: {"status":0,...,"Caminho de classe não encontrada..."}

# front
curl -sI http://127.0.0.1:3001/ | head -1        # 200 ou 3xx
docker compose logs --tail=50
```

Depois, pelo navegador via nginx: login, menu, **gerar PDF de proposta** (mpdf +
fonte), **upload de anexo** (S3 + `arquivos_temp` + limite 1G), **exportar/backup
de base** (`mysqldump` dentro do container). Por fim
`docker compose restart back` e conferir que sessão/anexos sobreviveram.

## 6. Rollback

- Reverter o `proxy_pass` do nginx para o Apache do host — `/var/www/html/back-end*`
  segue intacto durante a transição.
- Ou voltar a tag anterior: `sed -i 's|^TAG=.*|TAG=prod-<sha-antigo>|' .env && docker compose up -d`.

## 7. Build e teste local (Windows/PowerShell)

O `docker-compose.yml` referencia imagens do GHCR (não tem `build:`), então local
use os Dockerfiles direto, com **contexto na raiz do repo**:

```powershell
docker build -f docker/back/Dockerfile  -t linksun-back:dev  .
docker build -f docker/front/Dockerfile -t linksun-front:dev `
  --build-arg NEXT_PUBLIC_CRYPTOPASS=<valor-real-do-vercel> .
```

### 7.1 Back isolado

```powershell
docker run -d --name linksun-back-dev -p 8081:80 `
  -e BACK_BASE_PATH=back-end `
  -v "${PWD}\back-end\.env:/var/www/app/.env:ro" linksun-back:dev

# 1) stack viva (Apache + composer + Dotenv + PDO): erro de classe em JSON
curl.exe -s "http://127.0.0.1:8081/back-end/index.php?class=NaoExiste&action=x&sigla=undefined"
# 2) query real no banco
curl.exe -s "http://127.0.0.1:8081/back-end/index.php?class=Menu&action=getMenuInfo&sigla=undefined"
# 3) binários e libs
docker exec linksun-back-dev sh -c "which mysqldump pdftotext"
docker exec linksun-back-dev php -r "require 'vendor/autoload.php'; var_dump(class_exists('Mpdf\\Mpdf'));"
```

### 7.2 Front isolado

```powershell
docker run -d --name linksun-front-dev -p 3100:3000 linksun-front:dev
curl.exe -s -o NUL -w "%{http_code}`n" http://127.0.0.1:3100/     # 200
docker logs linksun-front-dev                                     # "> Ready on http://localhost:3000"
```

### 7.3 Ponta a ponta local (login de verdade)

`services/api.tsx` manda o front pra `http://localhost:8000/NovoLinksun/back-end/index.php`
quando a URL do navegador contém `localhost`. Então, pra logar de verdade com os
dois containers, o back tem de atender **nesse path e nessa porta**:

```powershell
# libere a porta 8000 (aqui ela estava ocupada pelo container api-linksun de outro projeto)
docker stop api-linksun

docker run -d --name linksun-back-local -p 8000:80 `
  -e BACK_BASE_PATH=NovoLinksun/back-end `
  -v "${PWD}\back-end\.env:/var/www/app/.env:ro" linksun-back:dev

# abrir http://localhost:3100  (localhost no host, não 127.0.0.1 — o sniff é por substring)
```

Cobrir no navegador: login, menu, **gerar PDF de proposta** (mpdf + fonte poppins),
**upload de anexo** (S3 + `arquivos_temp`), **exportar/backup de base** (`mysqldump`).

Limpeza: `docker rm -f linksun-back-dev linksun-front-dev linksun-back-local`
(e `docker start api-linksun`).

### 7.4 Resultado da validação local (17/08/2026)

| Item | Resultado |
| --- | --- |
| Back: JSON de erro de classe | OK (`status:0`, `Caminho de classe não encontrada`) |
| Back: `Menu/getMenuInfo` contra MySQL `192.168.20.116` | OK, 1358 bytes de JSON |
| PHP/Apache na imagem | Apache 2.4.68 + PHP 8.2.33 |
| Extensões | `pdo_mysql gd zip intl bcmath exif mbstring curl openssl fileinfo SimpleXML opcache` |
| INI aplicado | upload/post 1G, memory 1024M, TZ America/Sao_Paulo, session em `/var/lib/php/sessions` |
| `mysqldump` / `pdftotext` | `/usr/bin/mysqldump` (MariaDB 11.8) e poppler 25.03 — ambos OK |
| Autoload mpdf / phpspreadsheet / aws-sdk / phpmailer / spatie | todos `true` |
| Escrita em `arquivos_temp`, `backups`, `sessions` como `www-data` | OK |
| `BACK_BASE_PATH` aninhado (`NovoLinksun/back-end`) | OK |
| Front: `GET /` | 200, `<title>Sistema Linksun</title>`, assets `_next/static` |
| Servidor MySQL | 8.0.42 |

Corrigido durante a validação: `mysqldump` do cliente MariaDB (≥11.4) valida o
certificado do servidor por default e falhava com
`TLS/SSL error: self-signed certificate in certificate chain`. Resolvido com
[docker/back/mysql-client.cnf](../docker/back/mysql-client.cnf)
(`ssl-verify-server-cert=0` em `[client]`), sem alterar o PHP.
