This commit is contained in:
372
docker/README.md
Normal file
372
docker/README.md
Normal file
@@ -0,0 +1,372 @@
|
||||
# Imagem do ConectaSUS V2 — API + frontend
|
||||
|
||||

|
||||
|
||||
Imagem de produção com o **código encapsulado**: o `vendor/`, os caches de boot
|
||||
(`packages.php`, `services.php`, `routes-v7.php`) e o `dist/` do frontend já vão
|
||||
prontos. Substitui o modelo de bind mount de `/var/www` + `publish.sh` dentro do
|
||||
container.
|
||||
|
||||
API e frontend ficam no **mesmo container**, como em produção: Apache serve a API
|
||||
na `:80` e o SPA na `:8080`. Uma tag, um artefato, um rollback — as duas partes não
|
||||
têm como ficar dessincronizadas.
|
||||
|
||||
## O que entra e o que não entra
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| **Na imagem** | código da API, `vendor/` (`--no-dev`), autoload otimizado, route cache, `dist/` do frontend, vhosts do Apache, configs do supervisor |
|
||||
| **Fora da imagem** | `.env`, `connections.json`, `storage/` (dados e logs) |
|
||||
|
||||
`config/database.php:36` faz `file_get_contents(base_path('connections.json'))`
|
||||
no boot — **sem esse arquivo a aplicação não sobe**. O entrypoint valida os dois
|
||||
arquivos e falha com mensagem clara em vez de deixar o Apache subir quebrado.
|
||||
|
||||
O `npm` existe **só no stage de build**. Para o runtime atravessa apenas o `dist/`:
|
||||
nem `node_modules`, nem fonte `.vue`, nem webpack. Produção deixa de compilar
|
||||
qualquer coisa — nem PHP, nem JS.
|
||||
|
||||
## Estrutura
|
||||
|
||||
O contexto do build é a raiz do `application/`, porque o build precisa enxergar
|
||||
`api/` e `frontend/` ao mesmo tempo. Por isso `Dockerfile` e `docker/` vivem na
|
||||
raiz, e não dentro de `api/`.
|
||||
|
||||
```
|
||||
application/
|
||||
├── Dockerfile # 3 stages: frontend-build | build (composer) | runtime
|
||||
├── .dockerignore
|
||||
├── api/
|
||||
├── frontend/
|
||||
└── docker/
|
||||
├── entrypoint.sh
|
||||
├── apache/
|
||||
│ ├── ports.conf # Listen 80 + Listen 8080
|
||||
│ ├── 000-default.conf # :80 -> /var/www/api/public
|
||||
│ └── frontend.conf # :8080 -> /var/www/frontend/dist
|
||||
└── supervisor/conf.d/
|
||||
├── apache.conf
|
||||
└── websockets.conf # :6001
|
||||
```
|
||||
|
||||
O `frontend.conf` traz `FallbackResource /index.html`: o `vue-router` está em
|
||||
`mode: 'history'` (`frontend/src/router/index.js`) e, sem esse fallback, F5 em
|
||||
rota interna devolve 404.
|
||||
|
||||
## Build local
|
||||
|
||||
A partir da raiz do `application/` (o contexto é `.`):
|
||||
|
||||
```bash
|
||||
docker build -f Dockerfile \
|
||||
--build-arg APP_VERSION=2.59.3 \
|
||||
-t conectasus:2.59.3 \
|
||||
.
|
||||
```
|
||||
|
||||
O stage do frontend leva ~7 min (webpack) e roda com
|
||||
`NODE_OPTIONS=--max-old-space-size=4096`. O Node 10 da imagem base limita o heap a
|
||||
~1.5GB por padrão, e este projeto estoura esse teto com
|
||||
`FATAL ERROR: JavaScript heap out of memory`.
|
||||
|
||||
O `COPY` do `dist/` é o **último** do stage de runtime, de propósito: assim uma
|
||||
alteração só de frontend não invalida as camadas pesadas da API (`vendor/`, caches
|
||||
de boot), e o push sobe só o delta.
|
||||
|
||||
## Executando
|
||||
|
||||
```bash
|
||||
docker run -d --name conectasus \
|
||||
-p 80:80 -p 8080:8080 -p 6001:6001 \
|
||||
-v /dados/producao/conectasus/api/.env:/var/www/api/.env:ro \
|
||||
-v /dados/producao/conectasus/api/connections.json:/var/www/api/connections.json:ro \
|
||||
-v /dados/producao/conectasus/config.js:/var/www/frontend/config.js:ro \
|
||||
-v /dados/producao/conectasus/storage:/var/www/api/storage \
|
||||
192.168.0.41:3001/rkm/conectasus-api:2.59.3
|
||||
```
|
||||
|
||||
São **três** arquivos de configuração, todos obrigatórios — o entrypoint recusa
|
||||
subir sem qualquer um deles. O `config.js` é a config de runtime do frontend
|
||||
(modelo em [`config.js.example`](config.js.example)): é ele que faz a mesma imagem
|
||||
servir teste, homologação e produção sem rebuild.
|
||||
|
||||
O bind mount de arquivo único exige que o arquivo **já exista** no host — se o
|
||||
caminho não existir, o Docker cria um *diretório* com aquele nome e o entrypoint
|
||||
falha como se o arquivo estivesse faltando.
|
||||
|
||||
Variáveis de runtime opcionais:
|
||||
|
||||
| Variável | Padrão | Efeito |
|
||||
| --- | --- | --- |
|
||||
| `CACHE_CONFIG` | `false` | `true` gera `bootstrap/cache/config.php` no boot. Ganha performance, mas exige restart do container a cada mudança de `.env`/`connections.json`. |
|
||||
|
||||
## Migrations
|
||||
|
||||
> O `WORKDIR` da imagem e `/var/www`, nao `/var/www/api`. Comandos artisan
|
||||
> precisam de `-w /var/www/api` (ou `--workdir`), senao falham com
|
||||
> `Could not open input file: artisan`.
|
||||
|
||||
|
||||
**Não são executadas** nem no build nem no start do container. A aplicação de
|
||||
migrations por município é uma esteira separada.
|
||||
|
||||
O `entrypoint` só roda no start, então `docker exec` passa por fora dele. Tudo o
|
||||
que o comando precisa já está na imagem: `database/migrations`,
|
||||
`database/seeders`, `database/utils` e o `doctrine/dbal` (usado nos `->change()`).
|
||||
O nome passado em `--database` vem do `connections.json` montado em runtime.
|
||||
|
||||
Em um container já rodando:
|
||||
|
||||
```bash
|
||||
docker exec -w /var/www/api -it conectasus php artisan migrate --database=municipio --force
|
||||
```
|
||||
|
||||
Em um container descartável, sem depender de nenhum já no ar — o formato que a
|
||||
esteira de migration deve usar, já que só precisa da tag da imagem e dos mounts
|
||||
de config:
|
||||
|
||||
```bash
|
||||
docker run --rm \
|
||||
-v /dados/producao/conectasus/api/.env:/var/www/api/.env:ro \
|
||||
-v /dados/producao/conectasus/api/connections.json:/var/www/api/connections.json:ro \
|
||||
-v /dados/producao/conectasus/storage:/var/www/api/storage \
|
||||
--workdir /var/www/api \
|
||||
--entrypoint php \
|
||||
192.168.0.41:3001/rkm/conectasus-api:2.59.3 \
|
||||
artisan migrate --database=municipio --force
|
||||
```
|
||||
|
||||
Pontos de atenção:
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| `--force` | Obrigatório. Com `APP_ENV=production` o `migrate` abre prompt de confirmação e aborta em execução não interativa. |
|
||||
| `--database=<nome>` | Precisa bater com uma chave do `connections.json`. Nome inexistente falha com `Database connection [<nome>] not configured`. |
|
||||
| Código executado | É o da tag da imagem, não o do `git checkout` do host — a esteira de migration deve usar a mesma tag que está em produção. |
|
||||
| Persistência | Migration escreve no banco, não no filesystem do container. Nada se perde em restart ou troca de tag. |
|
||||
|
||||
Se em algum momento for usado o `php artisan publish` (que percorre todos os
|
||||
clientes de uma vez), aí o `connections.json` precisa ter também a entrada
|
||||
`schema` — [`PublishCommand::databaseExists()`](../api/app/Console/Commands/PublishCommand.php)
|
||||
usa `DB::connection('schema')`. Com `migrate --database=` direto isso não se aplica.
|
||||
|
||||
### Workflow manual de migrations
|
||||
|
||||
O workflow [`.gitea/workflows/migrate.yml`](../.gitea/workflows/migrate.yml)
|
||||
aplica migrations em uma imagem **já publicada**, sem executar deploy nem
|
||||
construir uma imagem. Ele exige quatro dados no botão *Run workflow*:
|
||||
|
||||
| Campo | Exemplo | Regra |
|
||||
| --- | --- | --- |
|
||||
| `ambiente` | `teste` | Escolha explícita entre teste, homolog e prod. |
|
||||
| `versao` | `2.59.3` | Deve ser a tag da imagem que contém a migration. |
|
||||
| `municipios` | `anhembi,aparecida` | Lista explícita de chaves do `connections.json`; não existe opção `todos`. |
|
||||
| `confirmacao` | `MIGRAR` | Sem o texto literal, o workflow recusa executar. |
|
||||
|
||||
Para cada município, o workflow cria um container descartável com a imagem
|
||||
escolhida, os mounts `.env`, `connections.json` e `storage/` do ambiente, e roda
|
||||
`php artisan migrate --database=<municipio> --force`. O container temporário
|
||||
não é a aplicação em produção e é removido ao fim; a alteração permanece no
|
||||
banco de dados.
|
||||
|
||||
Antes de executar a primeira migration, o script verifica que **todos** os
|
||||
nomes informados existem no `connections.json`, sem imprimir o conteúdo do
|
||||
arquivo. Depois disso os municípios são processados em sequência. Uma falha
|
||||
interrompe a lista; os bancos anteriores podem já ter sido migrados e devem ser
|
||||
verificados antes de uma nova tentativa.
|
||||
|
||||
## Rollback
|
||||
|
||||
Voltar o código é trocar a tag da imagem — segundos, sem rebuild:
|
||||
|
||||
```bash
|
||||
docker stop conectasus-api && docker rm conectasus-api
|
||||
docker run -d --name conectasus-api ... 192.168.0.41:3001/rkm/conectasus-api:2.59.2
|
||||
```
|
||||
|
||||
O que **não** volta é o schema do banco, já migrado para a versão nova. Se isso
|
||||
quebra ou não depende do tipo de migration aplicada:
|
||||
|
||||
| Tipo de migration no `up()` | Rollback da imagem |
|
||||
| --- | --- |
|
||||
| Aditiva (coluna nova, tabela nova, índice) | **Seguro.** O código antigo ignora o que não conhece. |
|
||||
| Destrutiva (`dropColumn`, `dropIfExists`, `renameColumn`) | **Quebra.** O código antigo referencia o que deixou de existir. |
|
||||
|
||||
Historicamente o segundo caso é raro: 3 de 255 migrations desde out/2025. Por
|
||||
isso a esteira roda o passo **Avaliar reversibilidade das migrations**, que
|
||||
compara o release com a tag anterior e marca no job summary se o rollback é
|
||||
seguro. O passo é informativo e não bloqueia o build.
|
||||
|
||||
Quando o release for marcado como destrutivo, o rollback deixa de ser opção
|
||||
barata. As saídas são, em ordem de preferência:
|
||||
|
||||
1. **Backup do banco antes de migrar** — permite voltar imagem e schema juntos.
|
||||
2. **Hotfix para frente** — corrigir na versão nova em vez de voltar.
|
||||
3. **`migrate:rollback --database=<municipio> --step=N`** — último recurso.
|
||||
97,5% das migrations têm `down()` implementado, mas rollback de schema com
|
||||
dados em produção tem risco próprio.
|
||||
|
||||
Uma alternativa que elimina o problema na origem é adiar a parte destrutiva:
|
||||
o release N para de usar a coluna, o release N+1 a remove. Aí toda migration é
|
||||
aditiva dentro da janela em que o rollback ainda pode ser necessário.
|
||||
|
||||
## Esteira (Gitea Actions)
|
||||
|
||||
Workflow: [`.gitea/workflows/build-api-image.yml`](../.gitea/workflows/build-api-image.yml)
|
||||
(precisa ficar na raiz do repositório — é onde o Gitea procura).
|
||||
|
||||
**Gatilhos:**
|
||||
|
||||
- push em `develop` ou `feat_CI_CD_TEST` — constrói, publica e faz deploy no
|
||||
ambiente de teste; a tag é o nome da branch em minúsculas e `latest` não muda;
|
||||
- push em `release-*` — constrói, publica e faz deploy em homologação;
|
||||
- push em `master` — não constrói nem implanta; o merge só prepara o commit que
|
||||
será marcado como release;
|
||||
- push de tag `2.*` (ex.: `2.59.3`) — **sempre constrói** a imagem a partir do
|
||||
commit marcado, publica `:2.59.3` (e `latest`, se `PUSH_LATEST=true`) e para.
|
||||
Deploy e migrations de produção são ações manuais separadas.
|
||||
|
||||
Sem prefixo `v`: o filtro é `2.*`, então `2.59.3` dispara e `v2.59.3` não — e a
|
||||
ausência de execução é silenciosa, sem erro em lugar nenhum.
|
||||
|
||||
O Gitea publica a tag da imagem em **minúsculas**. Um build da branch
|
||||
`feat_CI_CD_TEST` vira `:feat_ci_cd_test` no registry.
|
||||
|
||||
### Release de produção
|
||||
|
||||
O responsável cria a tag no commit já aceito em `master`:
|
||||
|
||||
```bash
|
||||
git tag 2.60.0
|
||||
git push origin 2.60.0
|
||||
```
|
||||
|
||||
A tag constrói e publica a imagem, mas não toca em nenhum ambiente. Antes da
|
||||
liberação, valide o artefato oficial no ambiente de teste pelo workflow manual:
|
||||
|
||||
```text
|
||||
deploy → ambiente=teste, versao=2.60.0
|
||||
migrate → ambiente=teste, versao=2.60.0, municipios=<lista>, confirmacao=MIGRAR
|
||||
```
|
||||
|
||||
No horário de liberação, o operador repete a operação de deploy com
|
||||
`ambiente=prod` e a **mesma** versão `2.60.0`; caso haja migrations, aciona
|
||||
depois o workflow `migrate` com a mesma versão e os municípios de produção
|
||||
escolhidos.
|
||||
|
||||
Enquanto produção estiver em outro servidor, o caminho recomendado é o job
|
||||
manual conectar por SSH ao host de produção e executar o script de deploy lá.
|
||||
Isso ainda precisa dos dados reais do servidor (host, usuário de deploy, chave,
|
||||
fingerprint SSH, caminho do script e do diretório de configuração); eles não
|
||||
devem ser inventados ou gravados no repositório.
|
||||
|
||||
### Variables — Settings › Actions › Variables
|
||||
|
||||
Nenhum host está fixo no YAML; tudo sai daqui (com fallback embutido).
|
||||
|
||||
| Variable | Exemplo | Default |
|
||||
| --- | --- | --- |
|
||||
| `REGISTRY_HOST` | `192.168.0.41:3001` | `192.168.0.41:3001` |
|
||||
| `REGISTRY_USER` | `ci-bot` | quem disparou a tag |
|
||||
| `API_IMAGE_NAME` | `rkm/conectasus-api` | `rkm/conectasus-api` |
|
||||
| `API_BASE_IMAGE` | `tirkm/v2-application:6` | `tirkm/v2-application:6` |
|
||||
| `CONTEXT_DIR` | `.` | `.` |
|
||||
| `DOCKERFILE` | `Dockerfile` | `Dockerfile` |
|
||||
| `API_DIR` | `api` | `api` |
|
||||
| `PUSH_LATEST` | `true` / `false` | `true` |
|
||||
| `AUTO_MIGRATE_TEST` | `true` / `false` | `false` — executa migrations automaticamente só quando o ambiente da branch é `teste`. |
|
||||
| `MIGRATION_CONNECTIONS_TEST` | `anhembi,aparecida` | sem padrão — lista explícita de conexões municipais para a migration automática de teste. |
|
||||
| `TEST_SSH_HOST` | `192.168.0.44` | opcional; esse é o padrão quando o transporte é SSH. |
|
||||
| `TEST_SSH_PORT` | `22` | `22` |
|
||||
| `SSH_USER_TEST` | `ci-deploy` | obrigatório quando o transporte é SSH. |
|
||||
|
||||
`CONTEXT_DIR` é o contexto do build (a raiz, que enxerga `api/` e `frontend/`).
|
||||
`API_DIR` é o subdiretório da API, usado só nas checagens que leem arquivos do
|
||||
repositório — `composer.json` e `database/migrations`. Não confundir os dois: foi
|
||||
por herdarem o mesmo valor que essas checagens quebraram quando o contexto subiu.
|
||||
|
||||
### Secrets — Settings › Actions › Secrets
|
||||
|
||||
| Secret | Obrigatório | Para quê |
|
||||
| --- | --- | --- |
|
||||
| `REGISTRY_TOKEN` | **sim** | PAT do Gitea com escopo `write:package` (e `read:package`) |
|
||||
| `DOCKERHUB_USER` / `DOCKERHUB_TOKEN` | só se `tirkm/` for privado | pull da imagem base |
|
||||
| `SSH_KEY_TEST` | sim, para deploy de teste | chave privada do usuário SSH de deploy no host de teste. |
|
||||
| `SSH_KNOWN_HOSTS_TEST` ou `SSH_KNOWN_HOST_TEST` | sim, para deploy de teste | fingerprint SSH do host de teste, no formato de `known_hosts`. |
|
||||
|
||||
O `REGISTRY_TOKEN` é obrigatório apesar do fallback no script: neste Gitea o
|
||||
`secrets.GITEA_TOKEN` do job resolve para **string vazia**, e o `set -u` não pega
|
||||
isso (a variável está definida, só que vazia). O resultado é um `docker login` com
|
||||
senha vazia e um `unauthorized` que parece problema de rede ou de TLS.
|
||||
|
||||
Cadastre o secret no **repositório** ou na **organização**. O cofre pessoal
|
||||
(*avatar → Settings → Actions → Secrets*) não é lido por repos da organização — o
|
||||
campo chega vazio e o sintoma é idêntico ao de secret inexistente.
|
||||
|
||||
### Deploy de teste por SSH
|
||||
|
||||
Todo deploy de teste usa SSH. Configure:
|
||||
|
||||
```text
|
||||
TEST_SSH_HOST=192.168.0.44
|
||||
TEST_SSH_PORT=22
|
||||
SSH_USER_TEST=ci-deploy
|
||||
```
|
||||
|
||||
O job abre SSH para o **host** `.44`, não para o container `act_runner`. Ele
|
||||
envia temporariamente `deploy.sh` e `teste.env`, faz login do Docker remoto no
|
||||
registry, executa o deploy com health check e remove os arquivos temporários.
|
||||
`.env`, `connections.json`, `config.js` e `storage/` não atravessam SSH: eles
|
||||
continuam no `CONFIG_DIR` do host de teste.
|
||||
|
||||
Registre o host em `SSH_KNOWN_HOSTS_TEST` (ou `SSH_KNOWN_HOST_TEST`) com
|
||||
fingerprint conferido fora da esteira. Não use `StrictHostKeyChecking=no`. O
|
||||
usuário SSH deve ser dedicado ao deploy; como ele opera Docker, trate-o como
|
||||
acesso administrativo ao host.
|
||||
|
||||
### Pré-requisitos
|
||||
|
||||
1. **act_runner registrado** com acesso ao daemon Docker do host — registre em
|
||||
modo `host`, ou monte `/var/run/docker.sock` no runner. O registro é feito uma
|
||||
única vez e persiste em `/data/.runner`; o token de registro do compose só é
|
||||
lido no primeiro boot.
|
||||
2. **Registry em HTTP**: `192.168.0.41:3001` não tem TLS, então o Docker do
|
||||
runner precisa confiar nele. Em `/etc/docker/daemon.json`:
|
||||
|
||||
```json
|
||||
{ "insecure-registries": ["192.168.0.41:3001"] }
|
||||
```
|
||||
|
||||
Seguido de `systemctl reload docker` — `insecure-registries` é recarregável via
|
||||
SIGHUP, então **não** é preciso reiniciar o daemon nem derrubar containers.
|
||||
Vale para todo host que faça `push` **ou `pull`** dessa imagem, incluindo os
|
||||
servidores de deploy.
|
||||
3. **Container registry habilitado** no Gitea (`[packages] ENABLED = true` no
|
||||
`app.ini` — já responde em `/v2/`).
|
||||
4. **`Packages: Write` no time da organização.** Publicar em `rkm/` exige essa
|
||||
unidade no time ao qual a conta do CI pertence — é permissão separada da de
|
||||
repositórios, e sem ela o `docker login` passa e só o `push` falha, com
|
||||
`unauthorized: authentication required`. Configura-se em
|
||||
*organização rkm → Settings → Teams → \<time\> → Units*.
|
||||
|
||||
Para diagnosticar: o endpoint `/v2/token` **não serve** — ele devolve
|
||||
`Scope: "all"` mesmo sem permissão, porque a checagem real só acontece no
|
||||
upload do blob. O teste que decide é tentar o push num namespace pessoal
|
||||
(`<usuario>/teste`): se lá funcionar e em `rkm/` não, é permissão de org, não
|
||||
escopo de PAT.
|
||||
|
||||
### Etapas do job
|
||||
|
||||
1. Checkout
|
||||
2. Metadados (versão, tags, data)
|
||||
3. Confere `composer.json.version` contra a tag (aviso, não bloqueia)
|
||||
4. Avalia reversibilidade das migrations contra a tag anterior (informativo)
|
||||
5. Login no Docker Hub (pulado se o secret não existir) e no registry do Gitea
|
||||
6. `docker build` — frontend (`npm ci` + `npm run build`) e API (`composer install`)
|
||||
em stages paralelos, com só o `dist/` e o código atravessando para o runtime
|
||||
7. Smoke test: a app boota com config; `.env`/`connections.json` **não** estão na
|
||||
imagem; o entrypoint recusa subir sem config; o `dist/` está publicado; o Apache
|
||||
passa no `configtest` e expõe `:80` e `:8080`
|
||||
8. `docker push` de `:<versão>` e, se `PUSH_LATEST=true` e for build de tag, de
|
||||
`:latest`
|
||||
9. Resumo no job summary + limpeza (`docker rmi`, `docker logout`)
|
||||
10
docker/apache/000-default.conf
Normal file
10
docker/apache/000-default.conf
Normal file
@@ -0,0 +1,10 @@
|
||||
<VirtualHost *:80>
|
||||
ServerAdmin ti@rkmsistemas.com.br
|
||||
DocumentRoot /var/www/api/public
|
||||
|
||||
<Directory "/var/www/api/public">
|
||||
Options -Indexes +MultiViews +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
24
docker/apache/frontend.conf
Normal file
24
docker/apache/frontend.conf
Normal file
@@ -0,0 +1,24 @@
|
||||
<VirtualHost *:8080>
|
||||
ServerAdmin ti@rkmsistemas.com.br
|
||||
DocumentRoot /var/www/frontend/dist
|
||||
|
||||
<Directory "/var/www/frontend/dist">
|
||||
Options -Indexes +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
|
||||
# vue-router esta em mode: 'history' (src/router/index.js).
|
||||
# Sem este fallback, F5 em rota interna devolve 404.
|
||||
FallbackResource /index.html
|
||||
</Directory>
|
||||
|
||||
# O config.js nao tem hash no nome (nao passa pelo webpack), entao o browser
|
||||
# o cachearia como qualquer estatico — e continuaria usando a URL do
|
||||
# ambiente anterior depois de uma troca. Os demais assets podem cachear a
|
||||
# vontade: o vue-cli poe hash de conteudo no nome deles.
|
||||
<Files "config.js">
|
||||
Header set Cache-Control "no-store, must-revalidate"
|
||||
</Files>
|
||||
</VirtualHost>
|
||||
|
||||
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
|
||||
18
docker/apache/ports.conf
Normal file
18
docker/apache/ports.conf
Normal file
@@ -0,0 +1,18 @@
|
||||
# If you just change the port or add more ports here, you will likely also
|
||||
# have to change the VirtualHost statement in
|
||||
# /etc/apache2/sites-enabled/000-default.conf
|
||||
|
||||
Listen 80
|
||||
|
||||
# Frontend (SPA Vue) — servido em vhost proprio na 8080.
|
||||
Listen 8080
|
||||
|
||||
<IfModule ssl_module>
|
||||
Listen 443
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_gnutls.c>
|
||||
Listen 443
|
||||
</IfModule>
|
||||
|
||||
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
|
||||
53
docker/base-image/Dockerfile
Normal file
53
docker/base-image/Dockerfile
Normal file
@@ -0,0 +1,53 @@
|
||||
FROM ubuntu:20.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV APACHE_RUN_USER=www-data
|
||||
ENV APACHE_RUN_GROUP=www-data
|
||||
ENV TZ="America/Sao_Paulo"
|
||||
|
||||
RUN apt-get update
|
||||
|
||||
RUN apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
wget \
|
||||
lsof \
|
||||
supervisor \
|
||||
zip \
|
||||
unzip \
|
||||
apache2 \
|
||||
php7.4 \
|
||||
php7.4-mysql \
|
||||
php7.4-interbase \
|
||||
php7.4-mbstring \
|
||||
php7.4-xml \
|
||||
php7.4-zip \
|
||||
php7.4-gd \
|
||||
php7.4-curl
|
||||
|
||||
RUN a2enmod rewrite
|
||||
|
||||
RUN wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.focal_amd64.deb \
|
||||
&& apt-get install -y ./wkhtmltox_0.12.6-1.focal_amd64.deb --no-install-recommends \
|
||||
&& rm -rf wkhtmltox_0.12.6-1.focal_amd64.deb
|
||||
|
||||
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
|
||||
&& php composer-setup.php --install-dir=/bin --filename=composer \
|
||||
&& php -r "unlink('composer-setup.php');"
|
||||
|
||||
RUN apt-get autoremove -y \
|
||||
&& apt-get clean
|
||||
|
||||
RUN rm -rf /var/www/* \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& rm -rf /tmp/provisioning*
|
||||
|
||||
RUN ln -sf /dev/stdout /var/log/apache2/access.log \
|
||||
&& ln -sf /dev/stdout /var/log/apache2/other_vhosts_access.log \
|
||||
&& ln -sf /dev/stderr /var/log/apache2/error.log
|
||||
|
||||
RUN echo "umask 0003" >> /etc/bash.bashrc
|
||||
|
||||
WORKDIR /var/www
|
||||
|
||||
CMD ["supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||
41
docker/base-image/README.md
Normal file
41
docker/base-image/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Imagem base da aplicacao
|
||||
|
||||
Substitui `tirkm/v2-application:6` por uma base dedicada ao runtime do
|
||||
ConectaSUS. Node/npm continuam no stage `frontend-build` do Dockerfile da
|
||||
aplicacao e, portanto, nao fazem parte desta imagem.
|
||||
|
||||
## Build
|
||||
|
||||
Execute a partir da raiz de `application`:
|
||||
|
||||
```sh
|
||||
docker build \
|
||||
-f docker/base-image/Dockerfile \
|
||||
-t tirkm/v2-application:7 \
|
||||
.
|
||||
```
|
||||
|
||||
Depois, valide a aplicacao com a nova base sem editar o Dockerfile principal:
|
||||
|
||||
```sh
|
||||
docker build \
|
||||
--build-arg BASE_IMAGE=tirkm/v2-application:7 \
|
||||
-t conectasus:base-7 \
|
||||
.
|
||||
```
|
||||
|
||||
## O que foi removido
|
||||
|
||||
- `vim`, `lsof`, `git`, `wget`, `zip` e `unzip`;
|
||||
- Node.js e npm, que ja existem apenas no stage de build do frontend;
|
||||
- caches e indices do APT;
|
||||
- `curl` e `xz-utils`, usados temporariamente para instalar o wkhtmltopdf.
|
||||
|
||||
O Composer e copiado da imagem oficial em vez de executar um instalador remoto
|
||||
sem verificacao. O pacote do wkhtmltopdf tem o SHA-256 validado antes da
|
||||
instalacao.
|
||||
|
||||
> PHP 7.4, Ubuntu 20.04 e wkhtmltopdf 0.12.6 estao fora de suporte. Esta imagem
|
||||
> reduz o tamanho e a superficie instalada, mas nao elimina esse risco. A
|
||||
> atualizacao de runtime deve ser tratada em uma etapa separada, com testes da
|
||||
> aplicacao.
|
||||
34
docker/config.js.example
Normal file
34
docker/config.js.example
Normal file
@@ -0,0 +1,34 @@
|
||||
// Configuracao de runtime do frontend — modelo para o deploy.
|
||||
//
|
||||
// Copie para o diretorio de config do ambiente e monte no container:
|
||||
//
|
||||
// volumes:
|
||||
// - ./config/config.js:/var/www/frontend/config.js:ro
|
||||
//
|
||||
// O entrypoint valida a presenca desse arquivo e o instala em
|
||||
// /var/www/frontend/dist/config.js no start. Sem ele, o container nao sobe —
|
||||
// o mesmo contrato do .env e do connections.json da API.
|
||||
//
|
||||
// Como estes valores NAO entram no bundle, a mesma imagem serve teste, homolog
|
||||
// e producao. Promover uma versao e apontar o outro stack para a mesma tag.
|
||||
|
||||
window.__APP_CONFIG__ = {
|
||||
// URL base da API.
|
||||
ROOT_API: "http://192.168.0.44:3200",
|
||||
|
||||
// Websockets (laravel-websockets / Pusher).
|
||||
WS_APP_KEY: "1",
|
||||
WS_HOST: "192.168.0.44",
|
||||
WS_PORT: "3201",
|
||||
|
||||
// Mapa hostname -> URL da API, para atender varios dominios com uma
|
||||
// instancia. Vazio significa usar sempre ROOT_API.
|
||||
// Ex.: { "foo.com.br": "https://api-foo.com.br" }
|
||||
DNS: {},
|
||||
|
||||
// "production" faz o app extrair o cliente do subdominio
|
||||
// (window.location.hostname.split('.')[0] — ver
|
||||
// components/administracao/autenticacao/login/Index.vue).
|
||||
// Deixe "" quando o ambiente for acessado por IP, senao o cliente vira "192".
|
||||
ENV: ""
|
||||
}
|
||||
103
docker/deploy/deploy-over-ssh.sh
Executable file
103
docker/deploy/deploy-over-ssh.sh
Executable file
@@ -0,0 +1,103 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Executa deploy.sh em um host remoto via SSH.
|
||||
#
|
||||
# ./deploy-over-ssh.sh <ambiente> <tag>
|
||||
#
|
||||
# O script e o arquivo <ambiente>.env do checkout sao copiados somente para um
|
||||
# diretorio temporario no host remoto. A configuracao sensivel da aplicacao
|
||||
# continua no CONFIG_DIR daquele host; ela nunca passa pelo job nem pelo SSH.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
AMBIENTE="${1:-}"
|
||||
TAG="${2:-}"
|
||||
SSH_HOST="${SSH_HOST:-}"
|
||||
SSH_PORT="${SSH_PORT:-22}"
|
||||
SSH_USER="${SSH_USER:-}"
|
||||
SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY:-}"
|
||||
SSH_KNOWN_HOSTS="${SSH_KNOWN_HOSTS:-}"
|
||||
REGISTRY="${REGISTRY:-192.168.0.41:3001}"
|
||||
REGISTRY_TOKEN="${REGISTRY_TOKEN:-}"
|
||||
REGISTRY_USER="${REGISTRY_USER:-${ACTOR:-}}"
|
||||
|
||||
AQUI="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ENV_FILE="${AQUI}/${AMBIENTE}.env"
|
||||
|
||||
log() { echo "[deploy-ssh] $*"; }
|
||||
fail() { echo "[deploy-ssh][ERRO] $*" >&2; exit 1; }
|
||||
|
||||
[ -n "${AMBIENTE}" ] || fail "uso: $0 <ambiente> <tag>"
|
||||
[ -n "${TAG}" ] || fail "uso: $0 <ambiente> <tag>"
|
||||
[ -f "${ENV_FILE}" ] || fail "ambiente desconhecido: ${AMBIENTE} (esperado ${ENV_FILE})"
|
||||
[ -n "${SSH_HOST}" ] || fail "SSH_HOST nao definido"
|
||||
[ -n "${SSH_USER}" ] || fail "SSH_USER nao definido"
|
||||
[ -n "${SSH_PRIVATE_KEY}" ] || fail "SSH_PRIVATE_KEY nao definido"
|
||||
[ -n "${SSH_KNOWN_HOSTS}" ] || fail "SSH_KNOWN_HOSTS nao definido"
|
||||
[ -n "${REGISTRY_TOKEN}" ] || fail "REGISTRY_TOKEN nao definido"
|
||||
[ -n "${REGISTRY_USER}" ] || fail "REGISTRY_USER/ACTOR nao definido"
|
||||
|
||||
command -v ssh >/dev/null || fail "cliente ssh nao encontrado no job"
|
||||
command -v scp >/dev/null || fail "cliente scp nao encontrado no job"
|
||||
|
||||
TMP_LOCAL="$(mktemp -d)"
|
||||
KEY_FILE="${TMP_LOCAL}/id_ed25519"
|
||||
KNOWN_HOSTS_FILE="${TMP_LOCAL}/known_hosts"
|
||||
REMOTE_DIR=""
|
||||
|
||||
cleanup() {
|
||||
local status=$?
|
||||
trap - EXIT
|
||||
if [ -n "${REMOTE_DIR}" ]; then
|
||||
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" \
|
||||
"docker logout '${REGISTRY}' >/dev/null 2>&1 || true; rm -rf -- '${REMOTE_DIR}'" \
|
||||
>/dev/null 2>&1 || true
|
||||
fi
|
||||
rm -rf -- "${TMP_LOCAL}"
|
||||
exit "${status}"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
printf '%s\n' "${SSH_PRIVATE_KEY}" > "${KEY_FILE}"
|
||||
printf '%s\n' "${SSH_KNOWN_HOSTS}" > "${KNOWN_HOSTS_FILE}"
|
||||
chmod 600 "${KEY_FILE}" "${KNOWN_HOSTS_FILE}"
|
||||
|
||||
SSH_OPTS=(
|
||||
-i "${KEY_FILE}"
|
||||
-p "${SSH_PORT}"
|
||||
-o IdentitiesOnly=yes
|
||||
-o StrictHostKeyChecking=yes
|
||||
-o UserKnownHostsFile="${KNOWN_HOSTS_FILE}"
|
||||
)
|
||||
SCP_OPTS=(
|
||||
-i "${KEY_FILE}"
|
||||
-P "${SSH_PORT}"
|
||||
-o IdentitiesOnly=yes
|
||||
-o StrictHostKeyChecking=yes
|
||||
-o UserKnownHostsFile="${KNOWN_HOSTS_FILE}"
|
||||
)
|
||||
|
||||
log "conectando em ${SSH_USER}@${SSH_HOST}:${SSH_PORT}"
|
||||
REMOTE_DIR="$(ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" 'umask 077; mktemp -d /tmp/conectasus-deploy.XXXXXX')"
|
||||
case "${REMOTE_DIR}" in
|
||||
/tmp/conectasus-deploy.*) ;;
|
||||
*) fail "diretorio temporario remoto inesperado: ${REMOTE_DIR}" ;;
|
||||
esac
|
||||
|
||||
log "enviando script de deploy temporariamente"
|
||||
scp "${SCP_OPTS[@]}" \
|
||||
"${AQUI}/deploy.sh" \
|
||||
"${ENV_FILE}" \
|
||||
"${SSH_USER}@${SSH_HOST}:${REMOTE_DIR}/"
|
||||
|
||||
# O login e feito no host remoto porque o docker pull sera executado por ele.
|
||||
# A senha trafega pelo stdin e nao aparece na linha de comando nem nos logs.
|
||||
printf '%s' "${REGISTRY_TOKEN}" | \
|
||||
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" \
|
||||
"docker login '${REGISTRY}' -u '${REGISTRY_USER}' --password-stdin" >/dev/null
|
||||
|
||||
log "executando deploy remoto de ${AMBIENTE}:${TAG}"
|
||||
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" \
|
||||
"chmod 700 '${REMOTE_DIR}/deploy.sh' && '${REMOTE_DIR}/deploy.sh' '${AMBIENTE}' '${TAG}'"
|
||||
|
||||
log "deploy remoto concluido"
|
||||
159
docker/deploy/deploy.sh
Executable file
159
docker/deploy/deploy.sh
Executable file
@@ -0,0 +1,159 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Deploy de um ambiente do ConectaSUS V2.
|
||||
#
|
||||
# ./deploy.sh <ambiente> <tag>
|
||||
#
|
||||
# Le os parametros do ambiente de docker/deploy/<ambiente>.env, troca a imagem
|
||||
# do container e so considera o deploy bem-sucedido depois de verificar que a
|
||||
# aplicacao responde. Se nao responder, volta sozinho para a imagem anterior.
|
||||
#
|
||||
# Roda a partir do runner, que usa o docker.sock do host — por isso todos os
|
||||
# caminhos de mount sao caminhos do HOST, nao do job.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
AMBIENTE="${1:-}"
|
||||
TAG="${2:-}"
|
||||
REGISTRY="${REGISTRY:-192.168.0.41:3001}"
|
||||
IMAGE_NAME="${IMAGE_NAME:-rkm/conectasus-api}"
|
||||
HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-90}"
|
||||
|
||||
AQUI="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
log() { echo "[deploy] $*"; }
|
||||
fail() { echo "[deploy][ERRO] $*" >&2; exit 1; }
|
||||
|
||||
[ -n "${AMBIENTE}" ] || fail "uso: $0 <ambiente> <tag>"
|
||||
[ -n "${TAG}" ] || fail "uso: $0 <ambiente> <tag>"
|
||||
|
||||
ENV_FILE="${AQUI}/${AMBIENTE}.env"
|
||||
[ -f "${ENV_FILE}" ] || fail "ambiente desconhecido: ${AMBIENTE} (esperado ${ENV_FILE})"
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
set -a; . "${ENV_FILE}"; set +a
|
||||
|
||||
# Gabarito nao preenchido: melhor recusar do que implantar em lugar errado.
|
||||
if grep -q 'TROCAR' "${ENV_FILE}"; then
|
||||
fail "${ENV_FILE} ainda tem valores de gabarito (TROCAR)."
|
||||
fi
|
||||
|
||||
for v in CONTAINER_NAME CONFIG_DIR PORT_API PORT_SPA PORT_WS; do
|
||||
[ -n "${!v:-}" ] || fail "${v} nao definido em ${ENV_FILE}"
|
||||
done
|
||||
|
||||
IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}"
|
||||
|
||||
log "ambiente : ${AMBIENTE}"
|
||||
log "container: ${CONTAINER_NAME}"
|
||||
log "imagem : ${IMAGE}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Guarda a imagem atual para poder voltar. Vale o digest, nao a tag: a tag e
|
||||
# mutavel e pode ja apontar para outra coisa na hora do rollback.
|
||||
# ---------------------------------------------------------------------------
|
||||
ANTERIOR=""
|
||||
if docker inspect "${CONTAINER_NAME}" >/dev/null 2>&1; then
|
||||
ANTERIOR=$(docker inspect "${CONTAINER_NAME}" --format '{{.Image}}')
|
||||
log "imagem atual: ${ANTERIOR}"
|
||||
else
|
||||
log "container ainda nao existe; primeiro deploy"
|
||||
fi
|
||||
|
||||
log "puxando ${IMAGE}"
|
||||
docker pull "${IMAGE}" >/dev/null || fail "pull falhou (registry acessivel? credencial presente?)"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. Config do host. O shell do job pode rodar dentro de um container do
|
||||
# act_runner e, portanto, nao enxergar os caminhos do host. A verificacao
|
||||
# precisa passar pelo daemon Docker: e ele que avaliara o bind mount no
|
||||
# host e depois subira a aplicacao com esses mesmos arquivos.
|
||||
#
|
||||
# --mount, diferentemente de -v, recusa uma origem inexistente em vez de
|
||||
# criar um diretorio vazio silenciosamente.
|
||||
# ---------------------------------------------------------------------------
|
||||
log "validando configuracao no host Docker"
|
||||
if ! docker run --rm \
|
||||
--mount "type=bind,src=${CONFIG_DIR},dst=/config,readonly" \
|
||||
--entrypoint sh \
|
||||
"${IMAGE}" \
|
||||
-c '
|
||||
set -eu
|
||||
for f in .env connections.json config.js; do
|
||||
[ -f "/config/${f}" ] || {
|
||||
echo "arquivo obrigatorio ausente: /config/${f}" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
[ -d /config/storage ] || {
|
||||
echo "diretorio obrigatorio ausente: /config/storage" >&2
|
||||
exit 1
|
||||
}
|
||||
'
|
||||
then
|
||||
fail "configuracao invalida ou inacessivel no host Docker: ${CONFIG_DIR}"
|
||||
fi
|
||||
|
||||
subir() {
|
||||
local img="$1"
|
||||
docker rm -f "${CONTAINER_NAME}" >/dev/null 2>&1 || true
|
||||
docker run -d \
|
||||
--name "${CONTAINER_NAME}" \
|
||||
--restart unless-stopped \
|
||||
${NETWORK:+--network "${NETWORK}"} \
|
||||
-p "${PORT_API}:80" \
|
||||
-p "${PORT_SPA}:8080" \
|
||||
-p "${PORT_WS}:6001" \
|
||||
-v "${CONFIG_DIR}/.env:/var/www/api/.env:ro" \
|
||||
-v "${CONFIG_DIR}/connections.json:/var/www/api/connections.json:ro" \
|
||||
-v "${CONFIG_DIR}/config.js:/var/www/frontend/config.js:ro" \
|
||||
-v "${CONFIG_DIR}/storage:/var/www/api/storage" \
|
||||
"${img}" >/dev/null
|
||||
}
|
||||
|
||||
saudavel() {
|
||||
local fim=$((SECONDS + HEALTH_TIMEOUT))
|
||||
while [ ${SECONDS} -lt ${fim} ]; do
|
||||
if [ "$(docker inspect -f '{{.State.Running}}' "${CONTAINER_NAME}" 2>/dev/null)" != "true" ]; then
|
||||
return 1
|
||||
fi
|
||||
local spa api
|
||||
spa=$(docker exec "${CONTAINER_NAME}" curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/ 2>/dev/null || echo 000)
|
||||
api=$(docker exec "${CONTAINER_NAME}" curl -s -o /dev/null -w '%{http_code}' http://localhost:80/ 2>/dev/null || echo 000)
|
||||
# A API nao tem rota "/", entao 404 e resposta valida; 000 e que nao e.
|
||||
if [ "${spa}" = "200" ] && [ "${api}" != "000" ]; then
|
||||
log "saudavel (spa=${spa} api=${api})"
|
||||
return 0
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
log "subindo ${CONTAINER_NAME}"
|
||||
subir "${IMAGE}"
|
||||
|
||||
if saudavel; then
|
||||
log "deploy de ${AMBIENTE} concluido em ${IMAGE}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Nao respondeu. Um container no ar mas quebrado e pior do que a versao
|
||||
# anterior no ar, entao volta.
|
||||
# ---------------------------------------------------------------------------
|
||||
echo "::error::deploy de ${AMBIENTE} falhou na verificacao de saude"
|
||||
docker logs "${CONTAINER_NAME}" 2>&1 | tail -30 || true
|
||||
|
||||
if [ -z "${ANTERIOR}" ]; then
|
||||
fail "sem versao anterior para voltar; container deixado no ar para diagnostico"
|
||||
fi
|
||||
|
||||
log "revertendo para ${ANTERIOR}"
|
||||
subir "${ANTERIOR}"
|
||||
|
||||
if saudavel; then
|
||||
fail "deploy revertido: ${AMBIENTE} voltou para a versao anterior"
|
||||
fi
|
||||
|
||||
fail "deploy falhou E a reversao tambem nao respondeu — ${AMBIENTE} precisa de atencao manual"
|
||||
16
docker/deploy/homolog.env
Normal file
16
docker/deploy/homolog.env
Normal file
@@ -0,0 +1,16 @@
|
||||
# Ambiente de HOMOLOG — parametros de deploy.
|
||||
#
|
||||
# TROCAR os valores abaixo. A esteira recusa implantar enquanto encontrar essa
|
||||
# palavra aqui — melhor falhar do que implantar no lugar errado.
|
||||
#
|
||||
# CONFIG_DIR precisa conter, no HOST: .env, connections.json, config.js e
|
||||
# storage/. O config.js e o que define para qual API o frontend aponta.
|
||||
|
||||
CONTAINER_NAME=conectasus-homolog
|
||||
CONFIG_DIR=/TROCAR/caminho/no/host/homolog/config
|
||||
|
||||
NETWORK=TROCAR
|
||||
|
||||
PORT_API=TROCAR
|
||||
PORT_SPA=TROCAR
|
||||
PORT_WS=TROCAR
|
||||
103
docker/deploy/migrate.sh
Executable file
103
docker/deploy/migrate.sh
Executable file
@@ -0,0 +1,103 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Aplica migrations do ConectaSUS para uma lista explicita de municipios.
|
||||
#
|
||||
# ./migrate.sh <ambiente> <tag> <municipio[,municipio...]>
|
||||
#
|
||||
# A migration roda em um container descartavel da imagem informada. Assim ela
|
||||
# nao depende de qual versao esta no ar e usa os mesmos .env,
|
||||
# connections.json e storage montados no host do ambiente.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
AMBIENTE="${1:-}"
|
||||
TAG="${2:-}"
|
||||
MUNICIPIOS_CSV="${3:-}"
|
||||
REGISTRY="${REGISTRY:-192.168.0.41:3001}"
|
||||
IMAGE_NAME="${IMAGE_NAME:-rkm/conectasus-api}"
|
||||
|
||||
AQUI="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
log() { echo "[migrate] $*"; }
|
||||
fail() { echo "[migrate][ERRO] $*" >&2; exit 1; }
|
||||
|
||||
[ -n "${AMBIENTE}" ] || fail "uso: $0 <ambiente> <tag> <municipio[,municipio...]>"
|
||||
[ -n "${TAG}" ] || fail "uso: $0 <ambiente> <tag> <municipio[,municipio...]>"
|
||||
[ -n "${MUNICIPIOS_CSV}" ] || fail "informe ao menos um municipio"
|
||||
|
||||
ENV_FILE="${AQUI}/${AMBIENTE}.env"
|
||||
[ -f "${ENV_FILE}" ] || fail "ambiente desconhecido: ${AMBIENTE} (esperado ${ENV_FILE})"
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
set -a; . "${ENV_FILE}"; set +a
|
||||
|
||||
if grep -q 'TROCAR' "${ENV_FILE}"; then
|
||||
fail "${ENV_FILE} ainda tem valores de gabarito (TROCAR)."
|
||||
fi
|
||||
|
||||
for v in CONFIG_DIR; do
|
||||
[ -n "${!v:-}" ] || fail "${v} nao definido em ${ENV_FILE}"
|
||||
done
|
||||
|
||||
IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}"
|
||||
|
||||
# Nomes de conexao do Laravel nao devem conter espacos nem caracteres que
|
||||
# possam alterar a linha de comando. A lista e propositalmente explicita:
|
||||
# conexoes tecnicas (por exemplo, sigtap) nao devem receber migrations por
|
||||
# acidente.
|
||||
IFS=',' read -r -a MUNICIPIOS_BRUTOS <<< "${MUNICIPIOS_CSV}"
|
||||
declare -a MUNICIPIOS=()
|
||||
declare -A VISTOS=()
|
||||
for bruto in "${MUNICIPIOS_BRUTOS[@]}"; do
|
||||
municipio="$(echo "${bruto}" | tr -d '[:space:]')"
|
||||
[ -n "${municipio}" ] || fail "ha um municipio vazio na lista: ${MUNICIPIOS_CSV}"
|
||||
[[ "${municipio}" =~ ^[A-Za-z0-9_-]+$ ]] || fail "nome de municipio invalido: ${municipio}"
|
||||
[ -z "${VISTOS[${municipio}]:-}" ] || fail "municipio repetido na lista: ${municipio}"
|
||||
VISTOS["${municipio}"]=1
|
||||
MUNICIPIOS+=("${municipio}")
|
||||
done
|
||||
|
||||
log "ambiente : ${AMBIENTE}"
|
||||
log "imagem : ${IMAGE}"
|
||||
log "municipios: ${MUNICIPIOS[*]}"
|
||||
|
||||
log "puxando ${IMAGE}"
|
||||
docker pull "${IMAGE}" >/dev/null || fail "pull falhou (registry acessivel? credencial presente?)"
|
||||
|
||||
# --mount valida as fontes no host do Docker. Isso e essencial quando o job
|
||||
# roda em container do act_runner, cujo filesystem nao contem CONFIG_DIR.
|
||||
MOUNTS=(
|
||||
--mount "type=bind,src=${CONFIG_DIR}/.env,dst=/var/www/api/.env,readonly"
|
||||
--mount "type=bind,src=${CONFIG_DIR}/connections.json,dst=/var/www/api/connections.json,readonly"
|
||||
--mount "type=bind,src=${CONFIG_DIR}/storage,dst=/var/www/api/storage"
|
||||
)
|
||||
|
||||
PHP_RUN=(docker run --rm "${MOUNTS[@]}" --workdir /var/www/api --entrypoint php "${IMAGE}")
|
||||
|
||||
# Confere o JSON e todas as conexoes antes de alterar qualquer banco. Nao lista
|
||||
# nem imprime o conteudo do arquivo, que pode conter credenciais.
|
||||
for municipio in "${MUNICIPIOS[@]}"; do
|
||||
if ! "${PHP_RUN[@]}" -r '
|
||||
$connections = json_decode(file_get_contents("connections.json"), true);
|
||||
if (!is_array($connections) || !array_key_exists($argv[1], $connections)) {
|
||||
fwrite(STDERR, "conexao nao encontrada no connections.json: " . $argv[1] . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
' "${municipio}"; then
|
||||
fail "validacao da conexao falhou; nenhum banco recebeu migration"
|
||||
fi
|
||||
done
|
||||
|
||||
MIGRATE_RUN=("${PHP_RUN[@]}")
|
||||
if [ -n "${NETWORK:-}" ]; then
|
||||
MIGRATE_RUN=(docker run --rm --network "${NETWORK}" "${MOUNTS[@]}" --workdir /var/www/api --entrypoint php "${IMAGE}")
|
||||
fi
|
||||
|
||||
total="${#MUNICIPIOS[@]}"
|
||||
for i in "${!MUNICIPIOS[@]}"; do
|
||||
municipio="${MUNICIPIOS[${i}]}"
|
||||
log "[$((i + 1))/${total}] aplicando migrations em ${municipio}"
|
||||
"${MIGRATE_RUN[@]}" artisan migrate --database="${municipio}" --force
|
||||
done
|
||||
|
||||
log "migrations concluidas para ${total} municipio(s)"
|
||||
16
docker/deploy/prod.env
Normal file
16
docker/deploy/prod.env
Normal file
@@ -0,0 +1,16 @@
|
||||
# Ambiente de PROD — parametros de deploy.
|
||||
#
|
||||
# TROCAR os valores abaixo. A esteira recusa implantar enquanto encontrar essa
|
||||
# palavra aqui — melhor falhar do que implantar no lugar errado.
|
||||
#
|
||||
# CONFIG_DIR precisa conter, no HOST: .env, connections.json, config.js e
|
||||
# storage/. O config.js e o que define para qual API o frontend aponta.
|
||||
|
||||
CONTAINER_NAME=conectasus-prod
|
||||
CONFIG_DIR=/TROCAR/caminho/no/host/prod/config
|
||||
|
||||
NETWORK=TROCAR
|
||||
|
||||
PORT_API=TROCAR
|
||||
PORT_SPA=TROCAR
|
||||
PORT_WS=TROCAR
|
||||
12
docker/deploy/teste.env
Normal file
12
docker/deploy/teste.env
Normal file
@@ -0,0 +1,12 @@
|
||||
# Ambiente de TESTE — parametros de deploy.
|
||||
# Valores extraidos do container que ja roda hoje (stack antonio-santos-v2).
|
||||
|
||||
CONTAINER_NAME=conectasus-test
|
||||
CONFIG_DIR=/dados/home/antonio.santos/development/V2/conectasus_test/config
|
||||
|
||||
# Rede do stack, para o container enxergar o banco pelo nome do servico.
|
||||
NETWORK=antonio-santos-v2_default
|
||||
|
||||
PORT_API=3200
|
||||
PORT_SPA=3202
|
||||
PORT_WS=3201
|
||||
63
docker/entrypoint.sh
Normal file
63
docker/entrypoint.sh
Normal file
@@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
APP_PATH="/var/www/api"
|
||||
FRONTEND_PATH="/var/www/frontend"
|
||||
|
||||
log() { echo "[entrypoint] $*"; }
|
||||
fail() { echo "[entrypoint][ERRO] $*" >&2; exit 1; }
|
||||
|
||||
cd "$APP_PATH"
|
||||
|
||||
if [ ! -f "${APP_PATH}/.env" ]; then
|
||||
fail "${APP_PATH}/.env nao encontrado. Monte o arquivo em runtime (bind mount/secret)."
|
||||
fi
|
||||
|
||||
# Config de runtime do frontend. Fica FORA do dist de proposito: o dist traz um
|
||||
# config.js vazio (placeholder de desenvolvimento), e exigir o arquivo montado
|
||||
# num caminho separado e o que permite distinguir "montado" de "nao montado".
|
||||
# Sem isso, um deploy sem o mount subiria silenciosamente apontando para lugar
|
||||
# nenhum, em vez de falhar.
|
||||
if [ ! -f "${FRONTEND_PATH}/config.js" ]; then
|
||||
fail "${FRONTEND_PATH}/config.js nao encontrado. Monte a config de runtime do frontend (modelo em docker/config.js.example)."
|
||||
fi
|
||||
|
||||
if ! grep -q '__APP_CONFIG__' "${FRONTEND_PATH}/config.js"; then
|
||||
fail "${FRONTEND_PATH}/config.js nao define window.__APP_CONFIG__."
|
||||
fi
|
||||
|
||||
# Instala no dist, de onde o Apache serve e o index.html carrega.
|
||||
cp "${FRONTEND_PATH}/config.js" "${FRONTEND_PATH}/dist/config.js"
|
||||
|
||||
if [ ! -f "${APP_PATH}/connections.json" ]; then
|
||||
fail "${APP_PATH}/connections.json nao encontrado. config/database.php le esse arquivo no boot; sem ele a aplicacao nao sobe."
|
||||
fi
|
||||
|
||||
if ! php -r 'json_decode(file_get_contents("connections.json"), true); exit(json_last_error() === JSON_ERROR_NONE ? 0 : 1);'; then
|
||||
fail "connections.json presente mas com JSON invalido."
|
||||
fi
|
||||
|
||||
mkdir -p \
|
||||
storage/framework/cache \
|
||||
storage/framework/views \
|
||||
storage/framework/sessions \
|
||||
storage/logs \
|
||||
storage/app/public \
|
||||
storage/app/documento \
|
||||
storage/app/intranet \
|
||||
bootstrap/cache
|
||||
|
||||
chown -R www-data:www-data storage bootstrap/cache 2>/dev/null || \
|
||||
log "AVISO: nao foi possivel ajustar o dono de storage/ (mount read-only?). Seguindo."
|
||||
chmod -R ug+rwX storage bootstrap/cache 2>/dev/null || true
|
||||
|
||||
php artisan config:clear >/dev/null 2>&1 || true
|
||||
|
||||
if [ "${CACHE_CONFIG:-false}" = "true" ]; then
|
||||
log "CACHE_CONFIG=true -> gerando bootstrap/cache/config.php"
|
||||
php artisan config:cache
|
||||
fi
|
||||
|
||||
log "API ${APP_VERSION:-dev} pronta. Iniciando: $*"
|
||||
|
||||
exec "$@"
|
||||
8
docker/supervisor/conf.d/apache.conf
Normal file
8
docker/supervisor/conf.d/apache.conf
Normal file
@@ -0,0 +1,8 @@
|
||||
[program:apache]
|
||||
command=/usr/sbin/apache2ctl -DFOREGROUND
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
10
docker/supervisor/conf.d/websockets.conf
Normal file
10
docker/supervisor/conf.d/websockets.conf
Normal file
@@ -0,0 +1,10 @@
|
||||
[program:websockets]
|
||||
directory=/var/www/api
|
||||
command=/usr/bin/php /var/www/api/artisan websockets:serve
|
||||
user=www-data
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
Reference in New Issue
Block a user