--- title: "Cómo configurar un pipeline de GitLab CI/CD con despliegue" description: "Crea un pipeline de GitLab CI/CD con .gitlab-ci.yml que prueba, compila y despliega por SSH en un servidor Ubuntu 24.04, con caché, artefactos y variables." category: "CI/CD DevOps" language: es source: https://cubepath.com/es/docs/ci-cd-devops/configuracion-del-pipeline-cicd-de-gitlab updated: 2026-09-25 --- # Cómo configurar un pipeline de GitLab CI/CD con despliegue GitLab CI/CD ejecuta automáticamente las tareas que definas en un archivo `.gitlab-ci.yml` cada vez que haces push a tu repositorio. En este tutorial construirás un pipeline completo para una aplicación Node.js: ejecutará las pruebas, generará la versión de producción y la desplegará por SSH en un servidor Ubuntu 24.04, solo desde la rama principal. Por el camino verás cómo funcionan las etapas, la caché, los artefactos, las variables protegidas y los runners. ## Requisitos previos Para seguir esta guía necesitas: - Un proyecto en GitLab (GitLab.com o una instancia propia) con una aplicación que tenga `package.json`, `package-lock.json` y los scripts `test` y `build`. El ejemplo supone que `npm run build` genera la carpeta `dist/`. - Un servidor de destino con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con Nginx sirviendo el sitio desde `/var/www/tu_dominio` y un usuario con `sudo`. - Un runner para ejecutar los trabajos. En GitLab.com puedes usar los runners de instancia incluidos; si prefieres uno propio, el paso 1 explica cómo instalarlo. Para el runner propio necesitas otro servidor Ubuntu 24.04 con Docker Engine instalado. ## Conceptos básicos Antes de escribir el archivo, conviene tener claros cuatro términos: | Concepto | Qué es | | --- | --- | | Pipeline | El conjunto de trabajos que se ejecuta con cada push, merge request o programación. | | Etapa (`stage`) | Un grupo de trabajos. Las etapas se ejecutan en orden; los trabajos de una misma etapa, en paralelo. | | Trabajo (`job`) | Una tarea con un `script`, que se ejecuta dentro de un contenedor limpio en un runner. | | Runner | El proceso (`gitlab-runner`) que recoge los trabajos y los ejecuta. | Como cada trabajo empieza en un entorno limpio, los archivos que un trabajo necesita de otro se pasan como **artefactos**, y las dependencias que se descargan en cada ejecución se reutilizan con la **caché**. ## Paso 1: Registrar un runner propio (opcional) Si usas los runners de instancia de GitLab.com, salta al paso 2. Un runner propio te da más control sobre los recursos y no consume minutos de cómputo. En el servidor del runner, descarga el script oficial que añade el repositorio de GitLab Runner, revísalo y ejecútalo: ```bash curl -fsSL -o script.deb.sh https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh less script.deb.sh sudo bash script.deb.sh ``` Instala el runner: ```bash sudo apt install gitlab-runner ``` En GitLab, abre tu proyecto y ve a **Settings > CI/CD > Runners**. Pulsa **New project runner**, marca **Run untagged jobs**, crea el runner y copia el token de autenticación, que empieza por `glrt-`. Registra el runner con el ejecutor Docker, sustituyendo `tu_token` (y la URL si usas una instancia propia): ```bash sudo gitlab-runner register --non-interactive --url "https://gitlab.com" --token "tu_token" --executor "docker" --docker-image "alpine:latest" ``` Comprueba que el runner está registrado y conecta con GitLab: ```bash sudo gitlab-runner verify ``` ```output Runtime platform arch=amd64 os=linux pid=5123 revision=... version=18.4.0 Running in system-mode. Verifying runner... is valid runner=AbCdEf12 ``` En **Settings > CI/CD > Runners**, el runner aparece ahora con un círculo verde. Si no quieres que el proyecto use también los runners de instancia, desactívalos en esa misma página. ## Paso 2: Preparar el servidor de destino El pipeline se conectará al servidor por SSH con un usuario sin privilegios que solo puede escribir en el directorio del sitio. En el servidor de destino, crea el usuario `deploy` y dale la propiedad del directorio: ```bash sudo adduser --disabled-password --gecos "" deploy sudo chown -R deploy:deploy /var/www/tu_dominio ``` En tu equipo local, genera un par de claves exclusivo para el despliegue, sin frase de paso porque lo usará un proceso automático: ```bash ssh-keygen -t ed25519 -f ~/.ssh/gitlab_deploy -N "" -C "gitlab-ci-deploy" ``` Instala la clave pública en el usuario `deploy` del servidor. Copia el contenido de `~/.ssh/gitlab_deploy.pub` y, en el servidor, ejecuta: ```bash sudo install -d -m 700 -o deploy -g deploy /home/deploy/.ssh sudo nano /home/deploy/.ssh/authorized_keys ``` Pega la clave pública, guarda y ajusta los permisos: ```bash sudo chown deploy:deploy /home/deploy/.ssh/authorized_keys sudo chmod 600 /home/deploy/.ssh/authorized_keys ``` Comprueba desde tu equipo que puedes entrar con esa clave: ```bash ssh -i ~/.ssh/gitlab_deploy deploy@ip_de_tu_servidor 'whoami' ``` ```output deploy ``` Por último, obtén la huella del servidor para que el pipeline pueda verificar que se conecta al equipo correcto: ```bash ssh-keyscan -t ed25519 ip_de_tu_servidor ``` ```output ip_de_tu_servidor ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH... ``` ## Paso 3: Definir las variables de CI/CD Los secretos nunca deben ir en `.gitlab-ci.yml`, porque queda en el historial del repositorio. En GitLab, ve a **Settings > CI/CD > Variables** y pulsa **Add variable** para cada una de estas: | Clave | Tipo | Valor | Opciones | | --- | --- | --- | --- | | `SSH_PRIVATE_KEY` | File | Contenido de `~/.ssh/gitlab_deploy` (con una línea en blanco al final) | Protected | | `SSH_KNOWN_HOSTS` | File | La línea que devolvió `ssh-keyscan` | Protected | | `DEPLOY_HOST` | Variable | `ip_de_tu_servidor` | Protected | | `DEPLOY_USER` | Variable | `deploy` | Protected | El tipo **File** hace que GitLab escriba el valor en un archivo temporal y ponga su ruta en la variable, que es justo lo que necesitan `ssh` y `rsync`. La opción **Protected** hace que la variable solo esté disponible en ramas y etiquetas protegidas, así que una rama cualquiera no puede leer la clave. La rama principal está protegida por defecto. > Nota: GitLab no puede enmascarar valores con varias líneas, como una clave privada. Por eso es importante que sea una clave exclusiva para el despliegue, marcada como protegida, y que no imprimas su contenido en ningún trabajo. ## Paso 4: Escribir el archivo .gitlab-ci.yml Crea el archivo `.gitlab-ci.yml` en la raíz del repositorio: ```yaml stages: - test - build - deploy default: image: node:22-alpine variables: npm_config_cache: "$CI_PROJECT_DIR/.npm" cache: key: files: - package-lock.json paths: - .npm/ test: stage: test script: - npm ci --prefer-offline - npm test build: stage: build script: - npm ci --prefer-offline - npm run build artifacts: paths: - dist/ expire_in: 1 week deploy: stage: deploy image: alpine:3.22 needs: - build cache: [] environment: name: production url: https://tu_dominio rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH before_script: - apk add --no-cache openssh-client rsync - chmod 600 "$SSH_PRIVATE_KEY" - mkdir -p ~/.ssh - chmod 700 ~/.ssh - cp "$SSH_KNOWN_HOSTS" ~/.ssh/known_hosts script: - rsync -az --delete -e "ssh -i $SSH_PRIVATE_KEY" dist/ "$DEPLOY_USER@$DEPLOY_HOST:/var/www/tu_dominio/" ``` Qué hace cada parte: - **`stages`**: define el orden. Si falla un trabajo de `test`, no se ejecutan `build` ni `deploy`. - **`default.image`**: todos los trabajos se ejecutan en un contenedor `node:22-alpine`, salvo que indiquen otra imagen. - **`cache`**: guarda la caché de npm (`.npm/`) entre ejecuciones. La clave depende de `package-lock.json`, así que la caché se invalida sola cuando cambian las dependencias. Se usa `npm ci`, que instala exactamente lo que dice el lockfile. - **`artifacts`**: el trabajo `build` guarda `dist/` en GitLab; el trabajo `deploy` lo descarga automáticamente porque lo declara en `needs`. Los artefactos caducan a la semana para no ocupar espacio indefinidamente. - **`rules`**: el despliegue solo se crea en pipelines de la rama por defecto (normalmente `main`). En otras ramas y merge requests se ejecutan solo las pruebas y la compilación. - **`environment`**: registra cada despliegue en **Operate > Environments**, con el historial y un botón para volver a desplegar una versión anterior. - **`deploy`**: usa una imagen Alpine mínima, instala el cliente SSH y rsync, y sincroniza `dist/` con el servidor. `--delete` elimina del servidor los archivos que ya no existen en la compilación. ## Paso 5: Validar la configuración Antes de hacer push, valida la sintaxis. En GitLab, ve a **Build > Pipeline editor**: el editor comprueba el archivo mientras escribes y muestra **Pipeline syntax is correct** cuando es válido. La pestaña **Visualize** dibuja las etapas y las dependencias entre trabajos, y **Validate** simula un pipeline en la rama que elijas para ver qué trabajos se crearían. Los errores más habituales son la indentación (YAML usa espacios, nunca tabuladores) y las referencias a etapas que no están declaradas en `stages`. ## Paso 6: Ejecutar el pipeline Añade el archivo al repositorio y súbelo a la rama principal: ```bash git add .gitlab-ci.yml git commit -m "Add CI/CD pipeline" git push origin main ``` En **Build > Pipelines** aparecerá un pipeline nuevo con las tres etapas. Pulsa sobre cualquier trabajo para ver su registro en tiempo real. El trabajo `deploy` debería terminar así: ```output $ rsync -az --delete -e "ssh -i $SSH_PRIVATE_KEY" dist/ "$DEPLOY_USER@$DEPLOY_HOST:/var/www/tu_dominio/" Cleaning up project directory and file based variables Job succeeded ``` Comprueba en el servidor que los archivos se han actualizado: ```bash ls -l --time-style=long-iso /var/www/tu_dominio | head ``` Y desde tu equipo, que el sitio responde: ```bash curl -I https://tu_dominio ``` ```output HTTP/2 200 server: nginx/1.24.0 (Ubuntu) ``` Para ver el comportamiento en otra rama, crea una rama nueva, haz un cambio y súbela: el pipeline tendrá solo los trabajos `test` y `build`. ## Paso 7: Ampliar el pipeline Con esta base, las ampliaciones más útiles son pequeñas: **Informes de pruebas en los merge requests.** Si tu herramienta de pruebas genera un informe JUnit (por ejemplo `jest-junit` escribe `junit.xml`), decláralo como artefacto de informe en el trabajo `test` y GitLab mostrará las pruebas fallidas en el propio merge request: ```yaml test: stage: test script: - npm ci --prefer-offline - npm test artifacts: when: always reports: junit: junit.xml ``` **Despliegue manual a producción.** Si prefieres que alguien apruebe cada despliegue, añade `when: manual` a la regla. El pipeline se detendrá en `deploy` hasta que alguien pulse el botón de ejecutar: ```yaml rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH when: manual ``` **Pruebas en paralelo con varias versiones.** `parallel:matrix` ejecuta el mismo trabajo con distintos valores de una variable: ```yaml test: stage: test image: node:${NODE_VERSION}-alpine parallel: matrix: - NODE_VERSION: ["20", "22"] script: - npm ci --prefer-offline - npm test ``` ## Solución de problemas **El trabajo se queda en `pending` con el mensaje "This job is stuck"**: no hay ningún runner disponible para el proyecto. Revisa en **Settings > CI/CD > Runners** que tu runner esté en línea y acepte trabajos sin etiquetas, o que los runners de instancia estén activos. **`Host key verification failed` en `deploy`**: el contenido de `SSH_KNOWN_HOSTS` no corresponde al servidor o se generó con otro nombre. Vuelve a ejecutar `ssh-keyscan` con la misma dirección que pones en `DEPLOY_HOST`. **`Load key ... error in libcrypto`**: la variable `SSH_PRIVATE_KEY` no es de tipo File o le falta el salto de línea final. Vuelve a pegar la clave completa, incluida la línea en blanco del final. **Las variables llegan vacías en otras ramas**: es el comportamiento esperado de las variables protegidas. Solo están disponibles en ramas y etiquetas protegidas. **`rsync: failed to set times` o `Permission denied`**: el usuario `deploy` no es propietario de `/var/www/tu_dominio` o de alguno de sus archivos. Repite el `chown` del paso 2. ## Conclusión Tienes un pipeline de GitLab CI/CD que prueba cada push, compila la aplicación con caché de dependencias y despliega automáticamente la rama principal en tu servidor por SSH, con la clave protegida en variables de CI/CD. Como siguientes pasos, añade un entorno de staging con su propia regla de rama, publica imágenes de contenedor en el GitLab Container Registry si tu aplicación corre en Docker, y programa pipelines periódicos en **Build > Pipeline schedules** para detectar dependencias rotas.