# Despliega con GitLab CI y reinicia un servicio
Este ejemplo compila un proyecto Node.js, sube el resultado por SFTP con contraseña y reinicia un servicio de Zaroz Cloud mediante la API. La contraseña SFTP transfiere archivos; un token de API granular e independiente solo controla el reinicio.

Las llamadas usadas son `POST /service-instances/{id}/restart` y `GET /service-instances/{id}/resources` de la [referencia de la API](/api). Para desplegar una imagen de contenedor en lugar de subir archivos, consulta [Despliega desde CI/CD](/es/guides/ci/overview).

## Qué necesitas

Desde la página del servicio en la consola, obtén el host, puerto, usuario, contraseña y ruta de destino SFTP.

Crea un token dedicado con estos dos permisos, ambos limitados al servicio exacto (su id de instancia de servicio, como en la URL de la consola). Consulta [Tokens de API](/es/api-tokens/getting-started) para ver cómo limitar un token:

- `services.read`: permite esperar a que el servidor vuelva.
- `services.power`: permite reiniciarlo.

No le des acceso a terminal, facturación, bases de datos ni a todos los servicios.

## Añade variables CI/CD protegidas

En GitLab, abre **Settings → CI/CD → Variables** y añade:

| Variable | Tipo | Opciones recomendadas |
| --- | --- | --- |
| `ZAROZ_API_TOKEN` | Variable | Masked, protected |
| `ZAROZ_SERVICE_ID` | Variable | Protected |
| `ZAROZ_SFTP_HOST` | Variable | Protected |
| `ZAROZ_SFTP_PORT` | Variable | Protected |
| `ZAROZ_SFTP_USER` | Variable | Protected |
| `ZAROZ_SFTP_PATH` | Variable | Protected |
| `ZAROZ_SFTP_PASSWORD` | Variable | Masked, protected |
| `ZAROZ_SFTP_KNOWN_HOSTS` | File | Protected |

Genera `ZAROZ_SFTP_KNOWN_HOSTS` desde un equipo de confianza y comprueba la huella del host antes de guardarlo. No uses `StrictHostKeyChecking=no` en producción.

:::warning{title="Protege el entorno de despliegue"}

Las variables protegidas solo se entregan a ramas o tags protegidos. Protege también el entorno de producción para que un merge request no fiable no pueda extraer las credenciales.

:::

## Añade `.gitlab-ci.yml`

El ejemplo espera que `npm run build` genere `dist/`. Adapta ambos a tu proyecto.

```yaml title=".gitlab-ci.yml"
stages:
  - build
  - deploy

build:
  stage: build
  image: node:22-alpine
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 hour

deploy-production:
  stage: deploy
  image: alpine:3.22
  needs:
    - job: build
      artifacts: true
  environment:
    name: production
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
  before_script:
    - apk add --no-cache curl jq lftp openssh-client
    - install -d -m 700 ~/.ssh
    - cp "$ZAROZ_SFTP_KNOWN_HOSTS" ~/.ssh/known_hosts
    - chmod 600 ~/.ssh/known_hosts
  script:
    - |
      export LFTP_PASSWORD="$ZAROZ_SFTP_PASSWORD"
      lftp -e "
        set cmd:fail-exit true;
        set sftp:connect-program \"ssh -a -x -o StrictHostKeyChecking=yes\";
        open --env-password --user \"$ZAROZ_SFTP_USER\" -p \"$ZAROZ_SFTP_PORT\" \"sftp://$ZAROZ_SFTP_HOST\";
        mirror --reverse --verbose --no-perms dist/ \"$ZAROZ_SFTP_PATH\";
        bye
      "
      unset LFTP_PASSWORD
    - |
      api() {
        curl --fail-with-body --silent --show-error \
          -H "Authorization: Bearer $ZAROZ_API_TOKEN" \
          -H 'Accept: application/json' \
          "$@"
      }

      API="https://kernel.zaroz.cloud/api/v2/service-instances/$ZAROZ_SERVICE_ID"

      # A restart in place: no stopped state to race against.
      api -X POST "$API/restart" > /dev/null
      sleep 5

      ready=false
      for attempt in $(seq 1 30); do
        state="$(api "$API/resources" | jq -r '.data[] | select(.is_declared_primary) | .status')"
        echo "Waiting for the server: $state"

        case "$state" in
          running)
            ready=true
            break
            ;;
          failed|crash_looping)
            echo "The service is $state after the restart"
            exit 1
            ;;
        esac

        sleep 2
      done

      if [ "$ready" != true ]; then
        echo "The service did not come back within about 65 seconds"
        exit 1
      fi

      echo "Deployment uploaded and service restarted"
```

## Por qué reinicia en el sitio y después espera

`restart` reinicia el servidor sin pasar por un estado detenido que tu script tendría que observar, así que no hay carrera entre parada y arranque. El reinicio tarda en terminar, por lo que el job lee el estado del recurso principal hasta que es `running`, y deja un error útil en GitLab si pasa a `failed` o `crash_looping`.

Si tu software admite una recarga gradual, prefiérela. Un ciclo de parada y arranque interrumpe a jugadores o usuarios activos.

## Adapta la subida

El lote SFTP sobrescribe archivos coincidentes, pero no elimina archivos remotos que ya no estén en `dist/`. Si necesitas una copia exacta, despliega en directorios versionados y cambia la versión activa desde el script de arranque de tu aplicación.

No subas secretos desde el repositorio. Guárdalos en el panel de Zaroz o como variables CI/CD protegidas.

## Solución de problemas

<details>
<summary>La API devuelve 403</summary>

Comprueba que ambos permisos están limitados al UUID de `ZAROZ_SERVICE_ID` y que el propietario del token aún puede controlar el servicio.

</details>

<details>
<summary>SFTP muestra un error de clave de host</summary>

Actualiza `ZAROZ_SFTP_KNOWN_HOSTS` solo después de verificar la nueva huella por un canal de confianza.

</details>

<details>
<summary>La subida funciona pero el reinicio no</summary>

Comprueba que elegiste `services.power`, además de `services.read`, y que el token no ha caducado.

</details>

<details>
<summary>El servidor no vuelve a running en aproximadamente un minuto</summary>

Deja que el job falle en lugar de reiniciarlo repetidamente. Revisa el servicio en la consola (y sus [logs](/es/guides/services/operate#logs)) y reintenta cuando se haya corregido la causa.

</details>
