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.jsony los scriptstestybuild. El ejemplo supone quenpm run buildgenera la carpetadist/. - Un servidor de destino con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con Nginx sirviendo el sitio desde
/var/www/tu_dominioy un usuario consudo. - 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:
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:
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):
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:
sudo gitlab-runner verify
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:
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:
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:
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:
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:
ssh -i ~/.ssh/gitlab_deploy deploy@ip_de_tu_servidor 'whoami'
deploy
Por último, obtén la huella del servidor para que el pipeline pueda verificar que se conecta al equipo correcto:
ssh-keyscan -t ed25519 ip_de_tu_servidor
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.
NotaGitLab 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:
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 detest, no se ejecutanbuildnideploy.default.image: todos los trabajos se ejecutan en un contenedornode:22-alpine, salvo que indiquen otra imagen.cache: guarda la caché de npm (.npm/) entre ejecuciones. La clave depende depackage-lock.json, así que la caché se invalida sola cuando cambian las dependencias. Se usanpm ci, que instala exactamente lo que dice el lockfile.artifacts: el trabajobuildguardadist/en GitLab; el trabajodeploylo descarga automáticamente porque lo declara enneeds. Los artefactos caducan a la semana para no ocupar espacio indefinidamente.rules: el despliegue solo se crea en pipelines de la rama por defecto (normalmentemain). 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 sincronizadist/con el servidor.--deleteelimina 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:
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í:
$ 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:
ls -l --time-style=long-iso /var/www/tu_dominio | head
Y desde tu equipo, que el sitio responde:
curl -I https://tu_dominio
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:
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:
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:
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.