From 0a83c0f5157d075c2a39acf72cac0c772a089d01 Mon Sep 17 00:00:00 2001 From: VPS 4 Builder Date: Fri, 10 Jul 2026 16:46:21 +0200 Subject: [PATCH] build: registry pela VPN (10.99.0.3:3000) + fail-fast na tag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O proxy HTTPS de git.clube67.com corta uploads em ~60 s, a ~0,75 MB/s: blobs acima de ~45 MB não publicam (o Gitea recebe o corpo truncado e responde 500). O maior blob daqui tem 39,5 MB — publicava raspando, e qualquer dependência nova quebraria o deploy. Pelo WireGuard são ~2,4 MB/s, sem corte. Mesmo Gitea, mesmo repositório, mesmas tags: só muda o caminho de rede. Verificado na VPS1 que `10.99.0.3:3000/ruicesar/scoreodonto-backend:v1.0.39` entrega o mesmo image id que o container em execução — o rollback para tags antigas segue válido. - docker-compose.prod.yml / deploy-prod.sh / workflow: registry passa a ser 10.99.0.3:3000. Requer `insecure-registries` no daemon da VPS1 e do runner (já aplicado; a porta 3000 não é exposta à internet, só pelo túnel). - promote: `buildx imagetools` não fala com registry HTTP, então o manifest é copiado pela API v2 do registry. Byte a byte ⇒ digest idêntico, que continua sendo conferido antes de deployar. - promote: autentica com o `docker login` do runner. `secrets.REGISTRY_TOKEN` não existe neste repo — o `docker login` mascarava isso; o curl não. - docker-compose.prod.yml: `${SCOREODONTO_TAG:?}` no lugar de `:-latest`. Sem a tag o deploy agora ABORTA, em vez de subir uma imagem indeterminada em produção. Co-Authored-By: Claude Opus 4.8 --- .gitea/workflows/build.yml | 56 +++++++++++++++++++++++++++++--------- deploy-prod.sh | 17 ++++++++++-- docker-compose.prod.yml | 15 ++++++++-- 3 files changed, 70 insertions(+), 18 deletions(-) diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index e664b6a..ac3b2f1 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -6,6 +6,10 @@ name: build-and-promote # valida integridade e deploya na VPS1. # A versão semântica NÃO entra no build: nasce da tag e é injetada no deploy em runtime # (docker-compose.prod.yml). Não há mais APP_VERSION/constants.ts. +# +# REGISTRY PELA VPN (10.99.0.3:3000). O proxy HTTPS de git.clube67.com corta uploads em +# ~60 s a ~0,75 MB/s: blobs acima de ~45 MB não sobem (o maior daqui tem 39,5 MB — passava +# raspando). Mesmo Gitea e mesmo storage; só muda o caminho de rede. on: push: branches: [main] @@ -29,7 +33,7 @@ jobs: - name: Login no registry run: | if [ -n "${{ secrets.REGISTRY_TOKEN }}" ]; then - echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.clube67.com -u ruicesar --password-stdin + echo "${{ secrets.REGISTRY_TOKEN }}" | docker login 10.99.0.3:3000 -u ruicesar --password-stdin else echo "REGISTRY_TOKEN ausente — usando a credencial já presente no host." fi @@ -48,7 +52,7 @@ jobs: - name: Publicar por COMMIT (: + :latest), com retry p/ 499 run: | set -e - REG=git.clube67.com/ruicesar + REG=10.99.0.3:3000/ruicesar C="${{ steps.vars.outputs.commit }}" for app in backend frontend; do SRC="scoreodontocom-scoreodonto-${app}:latest" @@ -82,36 +86,62 @@ jobs: - name: Login no registry run: | if [ -n "${{ secrets.REGISTRY_TOKEN }}" ]; then - echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.clube67.com -u ruicesar --password-stdin + echo "${{ secrets.REGISTRY_TOKEN }}" | docker login 10.99.0.3:3000 -u ruicesar --password-stdin fi + - name: Credencial do registry (reusa o docker login do runner) + run: | + set -e + # `secrets.REGISTRY_TOKEN` não está definido neste repo; o `docker login` acima + # cai na credencial do host. O curl abaixo precisa dela explicitamente. + if [ -n "${{ secrets.REGISTRY_TOKEN }}" ]; then + AUTH=$(printf 'ruicesar:%s' "${{ secrets.REGISTRY_TOKEN }}" | base64 -w0) + else + AUTH=$(python3 -c "import json,os;print(json.load(open(os.path.expanduser('~/.docker/config.json')))['auths']['10.99.0.3:3000']['auth'])") + fi + [ -n "$AUTH" ] || { echo "ERRO: sem credencial (rode: docker login 10.99.0.3:3000)"; exit 1; } + echo "::add-mask::$AUTH" + echo "REG_AUTH=$AUTH" >> "$GITHUB_ENV" + echo "ACCEPT=application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json" >> "$GITHUB_ENV" + - name: Integridade — a imagem do commit existe? (nunca buildar na tag) run: | set -e - REG=git.clube67.com/ruicesar C="${{ steps.vars.outputs.commit }}" for app in backend frontend; do - if ! docker buildx imagetools inspect "${REG}/scoreodonto-${app}:$C" >/dev/null 2>&1; then - echo "ERRO: ${REG}/scoreodonto-${app}:$C não existe no registry." + code=$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Basic ${REG_AUTH}" \ + -H "Accept: ${ACCEPT}" "http://10.99.0.3:3000/v2/ruicesar/scoreodonto-${app}/manifests/$C") + [ "$code" = "200" ] || { + echo "ERRO: scoreodonto-${app}:$C não existe no registry (HTTP $code)." echo " Faça 'git push origin main' (que builda o commit $C) ANTES de criar a tag." exit 1 - fi + } done echo "OK: backend:$C e frontend:$C presentes — mesmo commit $C (front+back atômicos)." - - name: Promover — re-tag commit → versão (SEM rebuild) + conferir digest + - name: Promover — copiar manifest commit → versão (SEM rebuild) + conferir digest run: | set -e - REG=git.clube67.com/ruicesar C="${{ steps.vars.outputs.commit }}"; V="${{ steps.vars.outputs.version }}" + A="Authorization: Basic ${REG_AUTH}" + # `docker buildx imagetools` não fala com registry HTTP (e a versão instalada não + # tem --insecure), então copiamos o manifest pela API v2. Byte a byte ⇒ mesmo digest. for app in backend frontend; do - DST="${REG}/scoreodonto-${app}" - docker buildx imagetools create -t "$DST:$V" "$DST:$C" - dC=$(docker buildx imagetools inspect "$DST:$C" --format '{{.Manifest.Digest}}') - dV=$(docker buildx imagetools inspect "$DST:$V" --format '{{.Manifest.Digest}}') + REPO="ruicesar/scoreodonto-${app}" + CT=$(curl -s -H "$A" -H "Accept: ${ACCEPT}" \ + -o manifest.json -w '%{content_type}' "http://10.99.0.3:3000/v2/${REPO}/manifests/$C") + dC=$(curl -s -H "$A" -H "Accept: ${ACCEPT}" -o /dev/null -D - \ + "http://10.99.0.3:3000/v2/${REPO}/manifests/$C" | tr -d '\r' \ + | awk '/^[Dd]ocker-[Cc]ontent-[Dd]igest:/{print $2}') + curl -sf -H "$A" -X PUT -H "Content-Type: ${CT}" \ + --data-binary @manifest.json "http://10.99.0.3:3000/v2/${REPO}/manifests/$V" >/dev/null + dV=$(curl -s -H "$A" -H "Accept: ${ACCEPT}" -o /dev/null -D - \ + "http://10.99.0.3:3000/v2/${REPO}/manifests/$V" | tr -d '\r' \ + | awk '/^[Dd]ocker-[Cc]ontent-[Dd]igest:/{print $2}') [ "$dC" = "$dV" ] || { echo "ERRO: digest divergiu em $app ($V=$dV != $C=$dC)"; exit 1; } echo "$app: $V -> $C (digest $dV)" done + rm -f manifest.json echo "Promovido sem rebuild: a imagem :$V E a imagem :$C (mesmo artefato)." - name: Deploy na VPS1 (pull :versao + up --no-build; versao injetada em runtime) diff --git a/deploy-prod.sh b/deploy-prod.sh index 4358591..f912b91 100755 --- a/deploy-prod.sh +++ b/deploy-prod.sh @@ -30,8 +30,21 @@ echo "⚠️ Recomendado: backup do banco de PRODUÇÃO antes (migrações roda echo " ssh deploy@10.99.0.3 'pg_dump -Fc scoreodonto -f ~/backups/scoreodonto_\$(date +%Y%m%d_%H%M).dump'" echo -echo "[1/3] pull das imagens $TAG ..." -docker compose $FILES pull scoreodonto-backend scoreodonto-frontend +echo "[1/3] pull das imagens $TAG (registry pela VPN) ..." +# Registry = 10.99.0.3:3000 (WireGuard). Se o pull falhar (VPN fora do ar), exigimos +# a imagem local COM A TAG EXATA — nunca caímos para :latest. +if ! docker compose $FILES pull scoreodonto-backend scoreodonto-frontend 2>/dev/null; then + echo " pull indisponível — conferindo imagens locais com a tag $TAG ..." + for app in backend frontend; do + img="10.99.0.3:3000/ruicesar/scoreodonto-${app}:${TAG}" + docker image inspect "$img" >/dev/null 2>&1 || { + echo "ERRO: $img não existe nem no registry nem localmente." + echo " Confira a VPN (ping 10.99.0.3)." + exit 1 + } + echo " ok: $img (local)" + done +fi echo "[2/3] subindo (sem build) ..." docker compose $FILES up -d --no-build scoreodonto-backend scoreodonto-frontend nginx diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 42a2ad0..c21ca11 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -12,14 +12,23 @@ # MESMA imagem pode ser promovida sob qualquer tag sem rebuild — pré-requisito do # "build once, promote the same artifact". O frontend não precisa de versão aqui: é estático # e lê tudo de /api/version (backend) em runtime. +# +# REGISTRY PELA VPN (10.99.0.3:3000), não por git.clube67.com: o proxy HTTPS corta +# uploads em ~60 s e a banda por ele é ~0,75 MB/s, então blobs acima de ~45 MB não +# publicam (o maior daqui tem 39,5 MB — passava raspando). Mesmo Gitea, mesmo +# repositório, mesmas tags: só o caminho de rede muda, e o rollback para tags +# antigas continua funcionando. Exige `insecure-registries: ["10.99.0.3:3000"]` +# no daemon da VPS1 e do runner. services: scoreodonto-backend: - image: git.clube67.com/ruicesar/scoreodonto-backend:${SCOREODONTO_TAG:-latest} + # `:?` (e não `:-latest`): sem SCOREODONTO_TAG o deploy ABORTA, em vez de subir + # silenciosamente uma imagem indeterminada em produção. + image: "10.99.0.3:3000/ruicesar/scoreodonto-backend:${SCOREODONTO_TAG:?defina SCOREODONTO_TAG}" # Mescla com o environment do compose base (por chave). Sobrescreve o APP_VERSION/APP_ENV # que vieram embutidos na imagem pelo build, fixando a versão = tag e o ambiente = PROD. environment: - - APP_VERSION=${SCOREODONTO_TAG:-dev} + - APP_VERSION=${SCOREODONTO_TAG} - APP_ENV=PROD scoreodonto-frontend: - image: git.clube67.com/ruicesar/scoreodonto-frontend:${SCOREODONTO_TAG:-latest} + image: "10.99.0.3:3000/ruicesar/scoreodonto-frontend:${SCOREODONTO_TAG:?defina SCOREODONTO_TAG}"