¿Por qué usar un servidor dedicado para CI/CD?
Los sistemas de integración continua y entrega continua compilan código, ejecutan pruebas, crean imágenes de contenedores, analizan dependencias y publican artefactos. Estas tareas suelen generar periodos breves de demanda muy alta de CPU, memoria, E/S de disco y red. Los agentes de Jenkins y los GitLab Runners también ejecutan código de muchas ramas, por lo que la contención de recursos puede hacer que la duración de los pipelines sea impredecible.
Un servidor dedicado proporciona al sistema de CI acceso exclusivo a sus núcleos de CPU, memoria, almacenamiento NVMe y puerto de red. Este aislamiento reduce el efecto de las cargas de trabajo vecinas y facilita estimar los tiempos de compilación. También permite que un administrador ajuste el sistema operativo, el almacenamiento de Docker, la caché, la política de swap y los controles de seguridad para la carga de trabajo del runner.
¿VPS o servidor dedicado?
Un VPS es suficiente cuando
- El equipo ejecuta menos de 5 trabajos simultáneos.
- La mayoría de las compilaciones finaliza en menos de 10 minutos y no compila proyectos grandes.
- Las imágenes de contenedores y las dependencias caben en 80 a 160 GB de almacenamiento NVMe.
- Los fallos de compilación causados por la contención de CPU o disco tienen poco impacto empresarial.
- El servicio de CI está separado de las bases de datos de producción y las aplicaciones orientadas al cliente.
Un VPS con 4 vCPU, 8 GB de RAM y 160 GB de almacenamiento NVMe puede admitir un controlador pequeño de Jenkins, un agente ligero o un GitLab Runner para aplicaciones web y repositorios de infraestructura. Mantén separados el controlador y los trabajos de compilación siempre que sea posible; los controladores de Jenkins deben programar el trabajo en lugar de realizar compilaciones pesadas.
Pasa a bare metal cuando
- Se ejecutan más de 10 trabajos al mismo tiempo o los retrasos en la cola superan regularmente los 5 minutos.
- Las compilaciones utilizan todas las vCPU asignadas durante periodos prolongados.
- Los proyectos compilan bases de código grandes de Java, Rust, C++, Android o .NET.
- Las compilaciones de imágenes de Docker producen muchos gigabytes de capas temporales.
- Varios equipos necesitan runners aislados o ejecutores dedicados.
- Las compilaciones requieren tiempos predecibles para lanzamientos, pruebas o ventanas de implementación.
- El sistema almacena cachés grandes de artefactos, réplicas de paquetes o registros de contenedores.
Bare metal también es una buena opción para cargas de trabajo privilegiadas. Docker-in-Docker, la virtualización anidada, las pruebas de hardware y las pruebas de integración de gran volumen pueden generar problemas de vecinos ruidosos, seguridad o E/S en un host virtual compartido.
Especificaciones recomendadas para un servidor dedicado
Para un host general de Jenkins o GitLab Runner, empieza con 8 núcleos físicos, 32 GB de RAM ECC y 480 GB o 960 GB de almacenamiento NVMe empresarial. Elige una CPU de servidor reciente con un buen rendimiento monohilo, además de suficientes núcleos para trabajos paralelos. Las compilaciones y las suites de pruebas se benefician de ambas características.
- CPU: de 8 a 16 núcleos físicos para equipos medianos; de 16 a 32 núcleos para granjas de compilación grandes. Evita dimensionar únicamente según el número de hilos, porque el multihilo simultáneo no equivale a un segundo núcleo físico.
- RAM: 32 GB para entre 10 y 20 trabajos simultáneos, 64 GB para entre 20 y 40 trabajos y 128 GB o más para compilaciones de lenguajes grandes, compilaciones de Android, pruebas de navegador o muchos contenedores.
- Almacenamiento: se prefieren los SSD NVMe. Usa 480 GB para un runner básico y de 960 GB a 1,92 TB para cachés locales, artefactos y capas de contenedores. RAID 1 protege la disponibilidad; no sustituye las copias de seguridad fuera del servidor.
- Red: 1 Gbps es suficiente para la mayoría de los equipos. Elige 10 Gbps cuando los runners descarguen imágenes grandes, repliquen paquetes o transfieran con frecuencia artefactos de varios gigabytes. Una transferencia mensual permitida de 10 TB es un punto de partida práctico.
- Sistema operativo: Debian 12, Ubuntu Server 24.04 LTS u otra distribución de Linux compatible. Mantén el host minimalista y coloca los servicios de aplicaciones en máquinas separadas.
No coloques Jenkins, GitLab, un registro de contenedores y una base de datos de producción en un solo servidor solo porque la CPU tenga núcleos sin utilizar. Los trabajos de CI ejecutan código cambiante y deben tener un límite de confianza claro respecto al control de código fuente y los sistemas de producción.
Para hasta 10 trabajos simultáneos, normalmente basta con un VPS de 4 vCPU; por encima de 20 trabajos simultáneos o con compilación sostenida, usa un servidor dedicado de 16 núcleos con 64 GB de RAM y 960 GB de NVMe.
| Trabajos de CI simultáneos | vCPU o núcleos | RAM | Disco | Ancho de banda mensual |
|---|---|---|---|---|
| Hasta 5 | 4 vCPU | 8 GB | 160 GB NVMe | 2 TB |
| De 6 a 20 | 8 núcleos físicos | 32 GB | 480 GB NVMe | 5 TB |
| De 21 a 50 | 16 núcleos físicos | 64 GB | 960 GB NVMe, RAID 1 | 10 TB |
| De 51 a 100 | 32 núcleos físicos | 128 GB | 1,92 TB NVMe, RAID 1 | 20 TB |
Configuración paso a paso
1. Prepara el DNS, el acceso y un host limpio
Crea un registro DNS dedicado, como ci.example.com, y usa claves SSH en lugar de autenticación mediante contraseña. Aprovisiona una imagen de Linux compatible y crea un administrador que no sea root. Restringe SSH con un firewall y permite el acceso únicamente desde direcciones de oficina, VPN o administración de confianza.
2. Actualiza el sistema operativo
Instala las actualizaciones de seguridad antes de añadir Jenkins o GitLab Runner:
sudo apt update && sudo apt full-upgrade -y
Configura la zona horaria correcta y habilita las actualizaciones de seguridad automáticas cuando tu política de cambios lo permita. Reinicia después de las actualizaciones del kernel.
3. Configura el firewall
Expón únicamente los puertos necesarios. Jenkins suele usar TCP 8080 detrás de un proxy inverso, mientras que GitLab Runner normalmente realiza conexiones salientes y no necesita un puerto de servicio entrante.
sudo apt install -y ufw sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow from 203.0.113.10 to any port 22 proto tcp sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw enable
4. Instala Docker para trabajos aislados
Los trabajos basados en Docker proporcionan dependencias reproducibles y facilitan la limpieza. Instala Docker desde el repositorio compatible con la distribución o desde el repositorio oficial de Docker y, después, verifica el daemon:
sudo apt install -y docker.io sudo systemctl enable --now docker sudo docker run --rm hello-world
No concedas a usuarios que no sean de confianza acceso sin restricciones al socket de Docker. Pertenecer al grupo docker equivale en la práctica a tener acceso root al host. Usa repositorios de confianza, ramas protegidas, runners aislados o alternativas rootless para código menos confiable.
5. Instala y registra GitLab Runner
Instala el paquete del runner mediante el repositorio del proveedor y, después, regístralo con un token de proyecto o de grupo. Usa etiquetas como docker, linux o privileged únicamente cuando el pipeline las necesite.
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash sudo apt install -y gitlab-runner sudo gitlab-runner register
Selecciona el ejecutor Docker cuando se te solicite. Establece un límite de concurrencia en /etc/gitlab-runner/config.toml para que el host conserve memoria y capacidad de CPU para las tareas del sistema.
6. Instala Jenkins en un controlador o agente separado
En una implementación pequeña, Jenkins puede ejecutarse en el mismo host que un agente. En instalaciones grandes, mantén el controlador en un VPS pequeño y utiliza el servidor dedicado como agente de compilación entrante o saliente. Instala la versión de Java compatible con la versión de Jenkins seleccionada y, después, instala Jenkins desde su repositorio firmado.
sudo apt install -y fontconfig openjdk-21-jre sudo systemctl enable --now jenkins sudo systemctl status jenkins
Coloca Jenkins detrás de un proxy inverso HTTPS, exige autenticación, deshabilita las compilaciones anónimas y restringe los permisos de los agentes. Prefiere agentes efímeros o contenedores desechables para solicitudes de extracción que no sean de confianza.
7. Añade caché y retención de artefactos
Guarda en caché las dependencias de paquetes y las salidas del compilador solo cuando la invalidación de la caché sea confiable. Establece reglas de retención para los registros de compilación y los artefactos; conservar todos los artefactos indefinidamente acabará llenando el volumen NVMe. Almacena los artefactos de lanzamiento en almacenamiento de objetos o en un registro dedicado con políticas de ciclo de vida.
8. Prueba la concurrencia antes de usarlo en producción
Ejecuta varios pipelines representativos a la vez y mide el uso de CPU, la presión de memoria, la latencia de disco, la transferencia de red y el tiempo en cola:
sudo apt install -y sysstat iotop mpstat -P ALL 5 iostat -xz 5 free -h
Aumenta el número de ejecutores únicamente después de confirmar que la RAM y la latencia de disco se mantienen dentro de límites aceptables.
Optimización del rendimiento
Adapta los ejecutores a los recursos físicos
Demasiados ejecutores crean pipelines sin cola, pero lentos. Empieza con un ejecutor por núcleo físico para compilaciones intensivas en CPU y, después, realiza pruebas con cuidado. Las pruebas de navegador, las pruebas de red y los trabajos intensivos en E/S pueden admitir más paralelismo, mientras que las compilaciones de Android y de proyectos C++ grandes pueden necesitar varios gigabytes de RAM por ejecutor.
Usa un almacenamiento Docker rápido y controlado
Mantén la raíz de datos de Docker en NVMe y supervisa el uso con docker system df. Limpia las capas no utilizadas según una programación, en lugar de hacerlo durante las compilaciones de máxima demanda:
docker system df sudo docker image prune -af --filter until=168h
Usa réplicas de registros o proxies de dependencias de GitLab cuando corresponda. Evita eliminar volúmenes activos y nunca ejecutes comandos de limpieza amplios sin confirmar los requisitos de retención.
Protege la memoria y el espacio en disco
Establece límites de memoria por trabajo para los contenedores, reserva RAM para el sistema operativo y genera alertas cuando el sistema de archivos supere el 80 por ciento de uso. Un archivo de swap pequeño puede evitar la terminación abrupta de un proceso, pero el swap no sustituye a una cantidad suficiente de RAM:
sudo fallocate -l 8G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile
Reduce el trabajo innecesario del pipeline
- Usa archivos de bloqueo de dependencias e imágenes base fijadas.
- Ejecuta pruebas unitarias en cada commit y pruebas de integración costosas en ramas protegidas o compilaciones programadas.
- Usa clones superficiales de Git cuando no sea necesario disponer del historial completo.
- Compila una vez y, después, promociona el artefacto probado entre entornos.
- Paraleliza las etapas independientes manteniendo la concurrencia por debajo de la capacidad probada del servidor.
Supervisa el host de CI
Controla la duración de la cola, la duración de los trabajos, el uso de los ejecutores, el promedio de carga, la presión de memoria, la latencia de disco, la capacidad del sistema de archivos y los trabajos fallidos. Genera alertas ante una carga sostenida superior al número de núcleos físicos disponibles, un uso del disco superior al 80 por ciento y eventos repetidos de falta de memoria. Centraliza los registros para que un host fallido no elimine las pruebas necesarias para el diagnóstico.
Errores comunes que debes evitar
- Ejecutar compilaciones en el controlador de Jenkins: mantén los ejecutores del controlador en cero o en un valor bajo y asigna el trabajo pesado a los agentes.
- Sobreasignar ejecutores: 32 hilos lógicos no garantizan 32 trabajos de compilación rápidos. Realiza pruebas comparativas con repositorios reales.
- Usar un único runner privilegiado compartido: separa los trabajos de lanzamiento de confianza de los trabajos de solicitudes de extracción y restringe el acceso privilegiado a Docker.
- Ignorar el crecimiento del disco: las capas de contenedores, los directorios de espacios de trabajo, los informes de pruebas y los artefactos pueden llenar rápidamente un disco de 480 GB.
- Omitir las copias de seguridad: realiza copias de seguridad de la configuración de Jenkins, los metadatos de credenciales, las definiciones de pipelines y la configuración del runner, manteniendo los secretos cifrados.
- Dejar públicas las interfaces de administración: coloca Jenkins detrás de HTTPS, exige MFA mediante el proveedor de identidad y restringe las rutas administrativas.
- Usar dependencias flotantes: los paquetes y las imágenes base sin fijar dificultan la reproducción de las compilaciones y pueden introducir riesgos en la cadena de suministro.
- Mezclar CI con producción: un trabajo de compilación comprometido no debería tener acceso directo de red a las bases de datos de producción ni a las credenciales de implementación.
Cuándo añadir más servidores
Un único servidor grande no siempre es el mejor diseño. Añade agentes separados cuando los proyectos necesiten distintos sistemas operativos, arquitecturas de CPU, acceso a GPU o niveles de confianza. Un controlador más dos o más agentes dedicados puede reducir el tiempo en cola y permitir el mantenimiento sin detener todos los pipelines. Usa etiquetas y tags de runners para dirigir los trabajos de Android, ARM, navegador, lanzamiento y análisis de seguridad al hardware adecuado.
Para obtener alta disponibilidad, mantén copias de seguridad independientes del control de código fuente y los metadatos de CI, conserva una imagen de runner de reemplazo y documenta los procedimientos de registro y rotación de secretos. La disponibilidad de CI no se mide únicamente por el tiempo de actividad del servidor, sino también por la capacidad de reproducir una compilación limpia después de un fallo.