diff --git a/Dockerfile b/Dockerfile index 8c456d6..1490600 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,14 +19,16 @@ RUN apt-get update \ libgomp1 \ && rm -rf /var/lib/apt/lists/* \ && groupadd --system app \ - && useradd --system --gid app --home-dir /home/app --create-home app + && useradd --system --gid app --home-dir /home/app --create-home app \ + && mkdir -p /app \ + && chown app:app /app WORKDIR /app -COPY --from=uv /uv /uvx /bin/ +COPY --from=uv --chown=app:app /uv /uvx /bin/ # Resolve dependências antes do código para aproveitar o cache de camadas. -COPY pyproject.toml uv.lock README.md ./ +COPY --chown=app:app pyproject.toml uv.lock README.md ./ RUN uv sync --frozen --no-dev --group inference --no-install-project COPY --chown=app:app src ./src @@ -34,7 +36,7 @@ COPY --chown=app:app config ./config COPY --chown=app:app scripts ./scripts RUN uv sync --frozen --no-dev --group inference -COPY docker/entrypoint.sh /usr/local/bin/medtrack-entrypoint +COPY --chown=app:app docker/entrypoint.sh /usr/local/bin/medtrack-entrypoint RUN chmod 755 /usr/local/bin/medtrack-entrypoint ENV MEDTRACK_FETCH_MODEL_ON_START=false \ @@ -44,7 +46,7 @@ ENV MEDTRACK_FETCH_MODEL_ON_START=false \ EXPOSE 8000 HEALTHCHECK --interval=30s --timeout=5s --start-period=45s --retries=3 \ - CMD python -c "from urllib.request import urlopen; urlopen('http://127.0.0.1:8000/healthz', timeout=3)" + CMD python -c "import os; from urllib.request import urlopen; urlopen(f\"http://127.0.0.1:{os.environ.get('PORT', '8000')}/healthz\", timeout=3)" ENTRYPOINT ["/usr/local/bin/medtrack-entrypoint"] CMD ["sh", "-c", "uvicorn medtrack_ai.api.main:app --host 0.0.0.0 --port ${PORT:-8000}"] diff --git a/README.md b/README.md index c351a88..429a8e2 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ descritos em [docs/CI_CD.md](docs/CI_CD.md). | Artefato e versão do modelo | [docs/models/README.md](docs/models/README.md) | | Contêiner local | [docs/CONTAINER.md](docs/CONTAINER.md) | | CI/CD e versionamento | [docs/CI_CD.md](docs/CI_CD.md) | -| Deploy de staging | [docs/RAILWAY.md](docs/RAILWAY.md) | +| Deploy em nuvem | [docs/CLOUD_DEPLOYMENT.md](docs/CLOUD_DEPLOYMENT.md) | | Decisões arquiteturais | [docs/adr/README.md](docs/adr/README.md) | | Governança | [docs/GOVERNANCE.md](docs/GOVERNANCE.md) | diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index f3393de..88baf11 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -4,8 +4,8 @@ set -eu artifact_directory="$(dirname "${MEDTRACK_MODEL_URI}")" easyocr_directory="${EASYOCR_MODULE_PATH:-/home/app/.EasyOCR}" -# Volumes Railway são montados no início do container. Preparamos permissões -# como root e reexecutamos o entrypoint com o usuário sem privilégios. +# Volumes são montados no início do contêiner. Preparamos as permissões como +# root e reexecutamos o entrypoint com o usuário sem privilégios. if [ "$(id -u)" = "0" ]; then mkdir -p "${artifact_directory}" "${easyocr_directory}" chown -R app:app "${artifact_directory}" "${easyocr_directory}" diff --git a/docs/CLOUD_DEPLOYMENT.md b/docs/CLOUD_DEPLOYMENT.md new file mode 100644 index 0000000..5bb59eb --- /dev/null +++ b/docs/CLOUD_DEPLOYMENT.md @@ -0,0 +1,54 @@ +# Deploy em plataforma de nuvem + +Este repositório pode ser implantado em uma plataforma que execute imagens +Docker, como Render ou outro provedor compatível. A plataforma deve construir o +`Dockerfile` da raiz e não deve substituir o `ENTRYPOINT` nem o `CMD` da imagem. + +## Configuração do serviço + +1. Crie um serviço web a partir deste repositório. +2. Selecione o runtime Docker e a branch que será implantada. +3. Não configure um comando de início personalizado. O `CMD` da imagem inicia o + Uvicorn usando a variável `PORT`, com fallback local para `8000`. +4. Configure a verificação HTTP de saúde em `GET /healthz`. +5. Se o modelo precisar persistir entre implantações, monte um volume em + `/data`. + +## Variáveis de ambiente + +Configure os valores adequados ao ambiente pela interface segura do provedor: + +```dotenv +MEDTRACK_ENV=staging +MEDTRACK_LOG_LEVEL=INFO +MEDTRACK_MODEL_URI=/data/models/medtrack-yolo/v1.0.0/best.pt +MEDTRACK_MODEL_VERSION=v1.0.0 +MEDTRACK_MODEL_MANIFEST=config/models/medtrack-yolo-v1.0.0.json +MEDTRACK_FETCH_MODEL_ON_START=true +MEDTRACK_DEVICE=cpu +MEDTRACK_MAX_IMAGE_DIMENSION=1024 +MEDTRACK_YOLO_CONFIDENCE=0.5 +EASYOCR_MODULE_PATH=/data/easyocr +MEDTRACK_CORS_ORIGINS= +``` + +Não copie um arquivo `.env` com segredos para o Git. A variável +`MEDTRACK_MODEL_URI` é obrigatória para o entrypoint preparar o diretório do +modelo antes de reduzir seus privilégios para o usuário `app`. + +## Saúde e prontidão + +- `GET /healthz` confirma que o processo HTTP está em execução e deve ser usado + como healthcheck da plataforma. +- `GET /readyz` confirma que o modelo foi carregado e que o serviço está pronto + para inferência. + +Após o deploy, substitua `URL` pelo domínio fornecido pela plataforma: + +```powershell +Invoke-WebRequest https://URL/healthz +Invoke-WebRequest https://URL/readyz +``` + +Os logs são enviados para stdout. Antes de promover uma versão, valide os dois +endpoints e confirme que o provedor preservou o volume do modelo. diff --git a/docs/CONTAINER.md b/docs/CONTAINER.md index 84aded1..5605257 100644 --- a/docs/CONTAINER.md +++ b/docs/CONTAINER.md @@ -49,4 +49,5 @@ use `$PWD.Path` para fornecer o caminho absoluto. - O Compose é CPU. Uma variante GPU só será criada após a escolha da plataforma de deploy e do runtime NVIDIA. -Para deploy de staging na Railway, consulte [RAILWAY.md](RAILWAY.md). +Para deploy em um provedor de nuvem, consulte +[CLOUD_DEPLOYMENT.md](CLOUD_DEPLOYMENT.md). diff --git a/docs/INVENTARIO_ATUAL.md b/docs/INVENTARIO_ATUAL.md index 89fdf60..caa0fe8 100644 --- a/docs/INVENTARIO_ATUAL.md +++ b/docs/INVENTARIO_ATUAL.md @@ -12,7 +12,7 @@ Data do levantamento: 22 de julho de 2026. | Utilitários | `src/utils/` | Sanitização de texto e ferramenta de rotulagem. | | Dados | `dataset/` e fonte externa indicada no README | Dataset e pesos são locais e ignorados; apenas a configuração de treino é versionada. | | Contêiner | `Dockerfile` | Instala `requirements.txt` e inicia `src.api.api:app`, mas não recebe o modelo. | -| Testes | `tests/test_api.py` | Mistura teste HTTP e modelo real; cria artefato local; alguns casos são ignorados quando não há imagem. | +| Testes | `../tests/integration/test_api.py` | Mistura teste HTTP e modelo real; cria artefato local; alguns casos são ignorados quando não há imagem. | ## Contrato HTTP observado (baseline) diff --git a/docs/RAILWAY.md b/docs/RAILWAY.md deleted file mode 100644 index 9d01ddf..0000000 --- a/docs/RAILWAY.md +++ /dev/null @@ -1,73 +0,0 @@ -# Deploy de staging na Railway - -Este repositório está preparado para ser implantado como um serviço Railway a -partir da branch `main`. A Railway detecta o `Dockerfile` na raiz e usa -`railway.toml` para aguardar a prontidão real em `/readyz`. - -## Preparação da conta e do projeto - -1. Entre na Railway usando sua conta GitHub e crie o projeto `medtrack`. -2. Crie o ambiente `staging` e conecte o repositório `MedTrack-Project/MedTrack-IA` - à branch `main`. -3. Crie um serviço a partir do repositório. Não defina Start Command: o `CMD` do - Dockerfile usa automaticamente a porta `PORT` injetada pela Railway. -4. Em **Volumes**, adicione um volume de ao menos 0,5 GB no caminho `/data`. -5. Em **Networking**, gere um domínio público Railway. Registre a URL no canal - interno do projeto; o app Android deve recebê-la por configuração de build, - nunca como literal no código-fonte. - -## Variáveis do ambiente `staging` - -Use o Raw Editor da aba **Variables** e cole os valores abaixo. Não copie um -`.env` com segredos para o Git. - -```dotenv -MEDTRACK_ENV=staging -MEDTRACK_LOG_LEVEL=INFO -MEDTRACK_MODEL_URI=/data/models/medtrack-yolo/v1.0.0/best.pt -MEDTRACK_MODEL_VERSION=v1.0.0 -MEDTRACK_MODEL_MANIFEST=config/models/medtrack-yolo-v1.0.0.json -MEDTRACK_FETCH_MODEL_ON_START=true -MEDTRACK_DEVICE=cpu -MEDTRACK_MAX_IMAGE_DIMENSION=1024 -MEDTRACK_YOLO_CONFIDENCE=0.5 -EASYOCR_MODULE_PATH=/data/easyocr -MEDTRACK_CORS_ORIGINS= -RAILWAY_HEALTHCHECK_TIMEOUT_SEC=300 -RAILWAY_DEPLOYMENT_DRAINING_SECONDS=30 -``` - -O entrypoint prepara as permissões do volume, baixa o peso apenas se ele ainda -não passar no checksum e inicia a API como usuário sem privilégios. O volume -também preserva os modelos auxiliares baixados pelo EasyOCR entre reinícios. - -## Verificação após deploy - -Substitua `URL` pelo domínio público gerado: - -```powershell -Invoke-WebRequest https://URL/healthz -Invoke-WebRequest https://URL/readyz -``` - -`/healthz` confirma o processo; `/readyz` só retorna `200` quando o modelo foi -carregado. A Railway usa `/readyz` durante o deploy e aguarda até 300 segundos. - -## Logs, operação e rollback - -- Logs são JSON no stdout e incluem `request_id`, rota, status, duração e versão - do modelo; não registram a imagem nem o texto extraído. -- Para correlacionar uma ocorrência do mobile, envie o valor de `X-Request-ID` - junto ao relato. -- Antes de promover uma mudança, confira o workflow `Quality` e o endpoint - `/readyz` em staging. -- Para rollback, em **Deployments** escolha o deployment estável anterior e use - **Redeploy**. O volume contém o mesmo modelo versionado; para reverter modelo, - altere explicitamente `MEDTRACK_MODEL_*` para outro manifesto/versão e faça - novo deploy. - -## Limitações intencionais - -O ambiente Trial/Free tem créditos e políticas de restart limitadas. Ele é -adequado para staging acadêmico, não para alegações de disponibilidade clínica. -O serviço não deve decidir administração de medicamentos sem conferência humana. diff --git a/docs/TESTING.md b/docs/TESTING.md index 8afa2e3..5189614 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -6,8 +6,8 @@ somente os níveis rápidos; testes que requerem artefato real são opt-in. | Nível | Localização | Dependências | Quando executar | | --- | --- | --- | --- | | Unitário | `tests/unit/` | Nenhuma rede, peso ou GPU | Todo commit e pull request | -| Integração | `tests/test_api.py` | FastAPI e adaptador falso | Todo commit e pull request | -| Manifesto | `tests/test_model_manifest.py` | Arquivos pequenos | Todo commit e pull request | +| Integração | `../tests/integration/test_api.py` | FastAPI e adaptador falso | Todo commit e pull request | +| Manifesto | `../tests/unit/test_model_manifest.py` | Arquivos pequenos | Todo commit e pull request | | Smoke de artefato | `tests/model/` | Peso local já verificado | Antes de deploy ou promoção | | Inferência real | futuro fixture de imagens aprovada | Peso + grupo `inference` | Pipeline manual dedicado | diff --git a/railway.toml b/railway.toml deleted file mode 100644 index c5209a8..0000000 --- a/railway.toml +++ /dev/null @@ -1,8 +0,0 @@ -[build] -builder = "DOCKERFILE" - -[deploy] -healthcheckPath = "/readyz" -healthcheckTimeout = 300 -restartPolicyType = "ON_FAILURE" -restartPolicyMaxRetries = 10 diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_api.py b/tests/integration/test_api.py similarity index 100% rename from tests/test_api.py rename to tests/integration/test_api.py diff --git a/tests/test_model_manifest.py b/tests/unit/test_model_manifest.py similarity index 100% rename from tests/test_model_manifest.py rename to tests/unit/test_model_manifest.py