CI/CD self-hosted: gestión de Nginx con servicios, timers y backups
Este proyecto tiene como finalidad diseñar e implementar un proceso de despliegue automatizado y seguro para una aplicación web estática en un servidor RHEL, integrando Nginx como servidor web, un flujo de CI/CD con GitHub Actions y un runner self-hosted controlado con servicios y timers de systemd.
Durante su desarrollo se abordaron todos los aspectos clave de la administración y automatización en entornos Linux:
- Gestión de usuarios: creación de un usuario administrador dedicado para ejecutar despliegues con permisos restringidos.
- Configuración del entorno: instalación de Nginx, estructura de directorios y puesta en marcha de la web local desde Windows.
- Conexión segura: generación de claves SSH, integración con GitHub y subida del proyecto al repositorio remoto.
- Automatización CI/CD: creación de un workflow en GitHub Actions y despliegue mediante un runner self-hosted instalado en RHEL.
- Orquestación de servicios: definición de un servicio
github_runner
con systemd y su ejecución programada mediante systemd-timers para controlar franjas horarias de despliegue. - Automatización del despliegue: desarrollo de un script en Bash que detiene Nginx, genera un backup de la web, copia los nuevos archivos, limpia el staging y reinicia el servicio, registrando todo en logs.
- Seguridad y disponibilidad: configuración final de Nginx, ajuste de contextos en SELinux y supervisión del servicio mediante Cockpit.
En conjunto, este proyecto representa un ejemplo práctico de infraestructura DevOps que combina administración de servidores Linux, automatización de procesos, gestión de servicios y seguridad del sistema, logrando un flujo de despliegue seguro, robusto y totalmente automatizado desde el momento en que se ejecuta un git push
hasta que la nueva versión de la web queda disponible en producción.
🧭 Índice del proyecto
- 🎯 Objetivos
- 🧱 Estructura general del entorno
- 👤 Creación del usuario administrador para despliegue
- ⚙️ Instalación de Nginx y configuración firewall
- 🌐 Creación de la web local en Windows
- 🔑 Generación de clave SSH y conexión con el servidor
- ☁️ Primera subida del proyecto a GitHub
- 🤖 Configuración del workflow de GitHub Actions (Self-hosted Runner)
- 🛠️ Instalación y configuración del runner en RHEL
- 🧩 Configuración del runner como servicio systemd
- ⏱️ Automatización y control de la ventana horaria
- 📦 Automatización del despliegue con backups y logs
- 🧪 Modificar configuración del servidor nginx final
- 🛡️Dar contexto a selinux
- 🚀Puesta en marcha y actualización por git.
- 🔒 Buenas prácticas aplicadas
- 🧠Conocimientos y herramientas aplicadas
🎯 Objetivos
El objetivo principal de este proyecto es diseñar e implementar un sistema de despliegue automatizado y seguro para una aplicación web estática utilizando Nginx sobre un servidor RHEL, con un flujo de trabajo CI/CD local basado en GitHub Actions y un runner self-hosted.
De este objetivo general se desprenden varios objetivos específicos:
- Automatización del despliegue
- Conseguir que cada cambio enviado al repositorio se refleje automáticamente en el servidor sin necesidad de intervención manual.
- Reducir errores humanos en las publicaciones mediante la estandarización de procesos.
- Gestión segura y ordenada del entorno
- Crear un usuario dedicado para la administración de despliegues, reforzando la seguridad del sistema.
- Configurar Nginx correctamente, incluyendo virtual hosts y parámetros básicos.
- Ajustar el contexto de SELinux para garantizar que la aplicación pueda servirse sin vulnerar la seguridad del sistema.
- Uso de servicios y timers para control del despliegue
- Implementar un servicio systemd para el runner de GitHub, asegurando su ejecución como un proceso controlado y confiable.
- Establecer timers automáticos que gestionen las franjas horarias de ejecución del runner, optimizando recursos y evitando que quede activo innecesariamente.
- Fiabilidad y recuperación ante errores
- Implementar un sistema de backups automáticos que guarde cada versión anterior de la web antes de aplicar cambios.
- Generar logs de despliegue que permitan auditar el proceso y detectar fallos en cualquier etapa.
- Supervisión y administración
- Integrar la herramienta Cockpit para monitorizar el estado del servidor, los servicios y el despliegue.
- Proporcionar un entorno fácilmente gestionable y replicable en otros servidores o proyectos futuros.
El proyecto busca demostrar cómo la combinación de administración de servidores Linux, automatización de procesos, seguridad del sistema y prácticas DevOps permite construir un flujo de despliegue eficiente, seguro, escalable y totalmente automatizado, apto para un entorno real de producción.
🧱 Estructura general del entorno
- Servidor RHEL 9 con IP local: 192.168.1.141
- Dependencias:
- Cockpit
- Nginx
- Systemd
- Services github-runner
- Timers start stop github-runner
- Firewall
- Selinux
- Dependencias:
- PC local con Windows donde trabajaremos el desarrollo web.
- Dependencias:
- git bash
- visual studio code
- Repositorio GitHub
👤 Creación del usuario administrador
Creamos el usuario eric_nginx para la administración del servidor y administrar la infraestructura en el servidor RHEL.
sudo useradd -m -s /bin/bash eric_nginx
sudo passwd eric_nginx
sudo usermod -aG wheel eric_nginx


Añadiremos el usuario en sudoers para darle permisos de ejecucion del script de automatización y la administracion de systemctl.
visudo

⚙️ Instalación de Nginx y configuración firewall
Instalamos Nginx al servidor,configuramos y recargamos el firewall:
sudo dnf install nginx -y
sudo systemctl enable –now nginx
sudo firewall-cmd –add-service=http –permanent
sudo firewall-cmd –reload
Verificamos en el navegador web si podemos observar la pagina de test de nginx http://192.168.1.141

🌐 Creación de la web local en Windows
En lugar de crear una web desde cero, he reutilizado un proyecto propio de desarrollo web previo:
🔗 Juego de la Vida de Conway Por lo que he decidido descargarlo mediente FTP desde el hosting actual
y lo he adaptado para el proyecto.

🔑 Generación de clave SSH y conexión con el servidor
Para facilitar la autenticación con GitHub y evitar el uso de contraseñas en cada operación git push o git pull, creamos una clave SSH en el entorno local de desarrollo (Windows).
1 – Generación del par de claves SSH (pública y privada)
Abrimos una terminal powershell y creamos las claves con el siguiente comando
ssh-keygen -t ed25519 -C “M CORREO ELECTRONICO”

2 – Registro de la clave pública en GitHub
Copiamos el contenido del archivo id_ed25519.pub en GitHub en:
Settings → SSH and GPG keys → New SSH Key

3 – Modificamos el archivo de config de .ssh
Abrimos desde la terminal el archivo con notepad y añadimos el siguiente contenido:

Host github-nginx
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_nginx
El editor de texto nos guarda el archivo por defecto con la extension .txt, para solucionar el problema podemos usar Rename-Item:

4 – Verificamos la conexion ssh con GitHub
En la terminal ejecutamos una conexion por ssh a github con:
ssh -T git@github.com
Si se han seguido correctamente los pasos anteriores nos devolvera un succesfully

Esta configuración mejora la seguridad del proyecto y simplifica el flujo de trabajo diario, eliminando la necesidad de autenticaciones manuales con usuario y contraseña cada vez que se interactúa con el repositorio remoto.
☁️ Primera subida del proyecto a GitHub
Abrimos el proyeco con visual studio code, posicionando la terminal en el directorio donde almacenamos el proyecto. Seguidamente iniciamos git y añadimos los archivos del proyecto al repositorio y lo subimos.
git init
git remote add origin https://github.com/Eric-SanchezMarch/project-web-nginx-ci-cd-local-conwaylife.git
git add .
git commit -m “Importar Juego de la Vida de Conway”
git branch -M main
git push -u origin main
🤖 Organización del repositorio y configuración del workflow CI/CD
Dentro del proyecto, devemos crear el directorio .github/workflows/deploy.yml
una vez creado añadimos las ordenes del workflow.
name: CI/CD Local Deploy on: push: branches: [main] jobs: deploy: runs-on: self-hosted steps: – name: Checkout code uses: actions/checkout@v3 – name: Copy web files to staging folder run: | mkdir -p /tmp/web_new cp -r ./web/* /tmp/web_new/ – name: Run local deploy script run: | sudo /usr/local/bin/deploy_nginx.sh |
Con esta configuracion logramos que cada vez que hacemos push
a la rama principal main
se despliegue de manera automática en nuestro entorno local siguiendo los siguientes pasos:
Para aladir al repositorio la nueva estructura de carpetas y el deploy.yml ejecutamos las siguientes ordenes:
git add web .github/workflows/deploy.yml
git commit -m “Mover archivos web a carpeta web y creacion workflow”
git push
git rm background.jpg estilos.css gl.mp4 index.html juegomain.js script.js
git commit -m “eliminar archivos de la raiz anteriormente movidos a web”
git push
Pasos del workflow
- Checkout del código
- Primero, el workflow obtiene el código del repositorio para que podamos trabajar con los archivos más recientes.
- Copiar archivos al staging
- Después, copiamos todos los archivos de la carpeta web a un directorio temporal llamado /tmp/web_new. Esta carpeta actúa como una zona de preparación (‘staging’) antes de actualizar la web real. Así evitamos que la web en producción quede en un estado intermedio mientras copiamos archivos
- Ejecutar el script de despliegue
- Finalmente, llamamos a un script llamado deploy_nginx.sh que se encarga de:
- Hacer un backup de la web actual.
- Detener Nginx para evitar servir archivos mientras se actualiza.
- Copiar los archivos nuevos desde staging al directorio de producción.
- Limpiar la carpeta de staging.
- Levantar Nginx nuevamente con la nueva versión de la web.
- Finalmente, llamamos a un script llamado deploy_nginx.sh que se encarga de:
🛠️ Instalación del runner en RHEL
Instalaremos el runner de github actions utilizando el usuario que hemos creado especificamente para la administracion del servicio y proyecto.

Accedemos al repositorio de github, y navegamos en el menú settings a actions -> runners y creamos un new self-hosted runner.

Seleccionaremos Linux con arquitectura de x64
Seguidamente github nos dara unas instrucciones, nosotros ya hemos creado la descarga, vamos a seguir con el configure y el Using your self-hosted runner

En el servidor ejecutamos las ordenes que nos indica de ./config.sh y ./run.sh

🧩 Configuración del runner como servicio systemd
Creamos un servicio systemd llamado github_runner.service para que el runner de GitHub Actions funcione en segundo plano.
Esto permite que el runner esté siempre activo, incluso después de cerrar la terminal, y se integra con los timers del sistema para mantener el flujo de CI/CD automatizado.
Además, configuramos que se reinicie automáticamente en caso de fallo, asegurando que el despliegue local nunca se interrumpa.

Recargamos systemd y reiniciamos el servicio creado.
sudo systemctl daemon-reload
sudo systemctl restart github_runner
sudo systemctl status github_runner

Explicación línea por línea
- [Unit] → Información general del servicio.
- Description → Texto descriptivo que verás al hacer systemctl status.
- After=network.target → Asegura que el runner arranque después de que la red esté disponible.
- [Service] → Cómo se ejecuta.
- ExecStart → Comando que arranca el runner (run.sh).
- WorkingDirectory → Carpeta donde se ejecuta el comando.
- User → Usuario que lo ejecuta (eric_nginx), así evitamos usar root.
- Restart=always → Si el runner se cae, systemd lo reinicia automáticamente.
- [Install] → Cómo integrarlo en el arranque.
- WantedBy=multi-user.target → Hace que el servicio se inicie en el modo normal multiusuario.
⏱️Automatización y control de la ventana horaria
Para este proyecto queremos emular un entorno critico 24×7 y queremos añadir una seguridad extra para que solo se pueda actualizar el servidor en una franja horaria acordada. de 10:00 a 15:00.
Systemd permite automatizar tareas usando .timer, que funcionan como un cron moderno.
Cada timer se asocia a un servicio .service que define la acción a ejecutar.
Por convención, el timer y el servicio deben tener nombres relacionados; de lo contrario, no funcionará la ejecución.
Servicio para iniciar el runner:
nano /etc/systemd/system/github_runner_start.service
[Unit] Description=Start GitHub Runner [Service] Type=oneshot ExecStart=/bin/systemctl start github_runner |
Timer de inicio a las 10:00
nano /etc/systemd/system/github_runner_start.timer
[Unit] Description=Start runner at 10:00 [Timer] OnCalendar=*-*-* 10:00:00 Persistent=true [Install] WantedBy=timers.target |
Servicio para detener el runner
nano /etc/systemd/system/github_runner_stop.service
[Unit] Description=Stop GitHub Runner [Service] Type=oneshot ExecStart=/bin/systemctl stop github_runner |
Timer para detener el runner a las 15:00
nano /etc/systemd/system/github_runner_stop.timer
[Unit] Description=Stop runner at 15:00 [Timer] OnCalendar=*-*-* 15:00:00 Persistent=true [Install] WantedBy=timers.target |
Activamos y arrancamos los timers
Recargamos systemd y configuramos como enable los timers creados.
sudo systemctl daemon-reload
sudo systemctl enable –now github_runner_start.timer
sudo systemctl enable –now github_runner_stop.timer
Verificamos que estan activos:
systemctl list-timers –all | grep github_runner

📦 Script de despliegue automatizado
Este script se ejecuta en el servidor mediante GitHub Actions cada vez que se realiza un git push
y el runner está activo. Su función es desplegar los nuevos archivos de la web de manera segura, garantizando respaldo y continuidad del servicio.
nano /usr/local/bin/deploy_nginx.sh
#!/bin/bash LOGFILE=”/var/log/deploy_nginx.log” BACKUP_DIR=”/var/backups/nginx_$(date +%F_%H-%M-%S)” STAGING_DIR=”/tmp/web_new” TARGET_DIR=”/var/www/html” RUNNER_SERVICE=”github_runner” log() { echo “[$(date +%F_%T)] $1” | tee -a “$LOGFILE” } log “Inicio del despliegue” if [ ! -d “$STAGING_DIR” ] || [ -z “$(ls -A $STAGING_DIR)” ]; then log “ERROR: No hay contenido en $STAGING_DIR” exit 1 fi log “Deteniendo nginx…” systemctl stop nginx || { log “ERROR al detener nginx”; exit 1; } log “Creando copia de seguridad…” mkdir -p “$BACKUP_DIR” tar -czf “$BACKUP_DIR/html_backup.tar.gz” -C “$TARGET_DIR” . || { log “ERROR en backup”; exit 1; } log “Copiando nuevos archivos…” rm -rf “$TARGET_DIR”/* cp -r “$STAGING_DIR”/* “$TARGET_DIR” || { log “ERROR al copiar”; exit 1; } log “Limpiando staging…” rm -rf “$STAGING_DIR” log “Iniciando nginx…” systemctl start nginx || { log “ERROR al iniciar nginx”; exit 1; } log “Despliegue finalizado correctamente” |
Damos permisos de ejecuccion al script creado:
chmod +x /usr/local/bin/deploy_nginx.sh
Funciones y flujo del script
El script cumple con los siguientes pasos:
- Registro de logs
- Todos los pasos se registran en
/var/log/deploy_nginx.log
para auditoría y seguimiento.
- Todos los pasos se registran en
- Verificación del staging
- Comprueba que el directorio
/tmp/web_new
exista y contenga archivos. - Si no hay contenido, detiene el despliegue y registra un error.
- Comprueba que el directorio
- Detener Nginx
- Evita conflictos al reemplazar archivos activos del servidor web.
- Backup de la web actual
- Crea un backup comprimido de
/var/www/html
en/var/backups/nginx_fecha-hora
.
- Crea un backup comprimido de
- Copiar nuevos archivos
- Borra el contenido antiguo en
/var/www/html
y copia los nuevos archivos desde/tmp/web_new
.
- Borra el contenido antiguo en
- Limpiar staging
- Elimina
/tmp/web_new
para que no queden restos de despliegues anteriores.
- Elimina
- Reiniciar Nginx
- Levanta el servicio web para que la nueva versión esté disponible.
- Finalizar con log
- Registra que el despliegue se completó correctamente o reporta errores en cualquier paso crítico.
Ponemos a prueba el script añadiendo un primer index.html de prueba en tmp y inicializando otro en el directorio www

El script funciona correctamente creando los backups y moviendo los archivos del nuevo repositorio al despliegue del servidor. Aun asi, no se puede visualizar aun en el navegador servidor por falta de configuración de nginx y SELinux. Si revisamos la IP del servidor nos saldra la pagina TEST de nginx.

🧪 Modificar configuración del servidor nginx final
Nuestro script de despliegue deploy_nginx.sh copia los archivos al directorio /var/www/html
. Sin embargo. Para que Nginx pueda mostrar nuestra página correctamente, es necesario crear un archivo de configuracion.
En RHEL, Nginx carga automáticamente todos los archivos .conf dentro de /etc/nginx/conf.d/
Por ello vamos a crear el nuestro propio para este proyecto:
nano /etc/nginx/conf.d/project.conf
server { listen 80; server_name 192.168.1.141; root /var/www/html; # Directorio donde tu script despliega los archivos index index.html; location / { try_files $uri $uri/ =404; } } |
Esta configuración declara lo siguiente:
- listen 80; → indica que escucha en el puerto HTTP estándar.
- server_name 192.168.1.141; → la IP o dominio donde quieres servir tu web.
- root /var/www/html; → donde tu script despliega los archivos.
- try_files $uri $uri/ =404; → devuelve 404 si no encuentra el archivo solicitado.
Comprobamos si la configuración del servidor es correcto ejecutando:
nginx -t

Si no da ningun error podremos recargar el servicio para aplicar la configuración:
systemctl reload nginx
🛡️Dar contexto a selinux
En RHEL, SELinux controla el acceso a los recursos a través de contextos de seguridad.
Aunque el directorio /var/www/html
existe y tenga permisos correctos, si no tiene el contexto SELinux adecuado, Nginx (que corre bajo el dominio httpd
) no podrá acceder a los archivos y devolverá un error 403 (Forbidden).
Comprobamos el contexto de la ruta del proyecto
ls -Zd /var/www/html
Nos devuelve: unconfined_u:object_r:var_t:s0 /var/www/html
Asignamos el contexto adecuado:
sudo semanage fcontext -a -t httpd_sys_content_t “/var/www/html(/.*)?”
sudo restorecon -Rv /var/www/html
Esto cambiará tanto el directorio como todo lo que haya dentro a httpd_sys_content_t.

Con esto el servidor ya muestra el texto de prueba ejecutado en Script de despliegue automatizado

🚀Puesta en marcha y actualización por github actions
Para que el despliegue funcione deveremos asegurar que hacemos push entre las 10 y las 15 horas.
En el momento de hacer la prueba definitiva eran las 3 del medio dia pasadas haciendo que el timer stop parara el servicio. Eso evidencia que los servicios y los timers funcionan correctamente.
Procedemos a conectarnos a la consola cockpit del servidor para una administracion mas grafica del servicio github_runner, y lo activamos.
Desde nuestro ordenador windows, podemos crear una modificacion en el archivo index.html, crear un nuevo commit y subirlo de nuevo.

Desde github también podemos evidenciar si el runner tiene conectividad entre el servidor y github.

Ejecutamos el git push para que se actualice el repositorio y el workflow ejecute las ordenes y el automatismo interno del servidor para la actualización del contenido al servidor.

En cockpit, en servicios, si accedemos al servicio github_runner.service podemos observar en directo como en Bitacoras del servicio, este recibe la tarea de deploy

Accedemos a la IP y podemos observar como por fin se ha cargado el contenido del servidor.

🔒 Buenas prácticas aplicadas
Principio de menor privilegio
Creación de un usuario específico (eric_nginx) para gestionar despliegues, evitando operar como root. Esto limita el impacto ante un posible fallo o brecha.
Automatización robusta y repetible
Uso de GitHub Actions junto a un runner self-hosted, systemd-services y timers para automatizar todo el pipeline de CI/CD, reduciendo errores humanos y favoreciendo la consistencia.
Control horario del despliegue
Configuración de timers para que el runner solo esté activo entre las 10:00 y las 15:00, evitando ejecuciones fuera de ventana y aportando control y predictibilidad.
Backups antes del despliegue
El script deploy_nginx.sh primero crea una copia comprimida de la versión actual, permitiendo recuperación ante errores o fallos, lo que refuerza la resiliencia del sistema.
Registro de logs detallados
Cada paso crítico (inicio, errores, fin) se anota en un log (/var/log/deploy_nginx.log), facilitando la auditoría y el diagnóstico de problemas.
Manejo de errores en el script
Uso de condicionales para detener el proceso si algo falla (por ejemplo, si el staging está vacío o falla detener Nginx), evitando inconsistencias en producción.
Configuración modular de servicios systemd
Separación clara entre .service y .timer, con nombres explícitos que facilitan su comprensión y mantenimiento.
Seguridad en servidor web
Aplicación correcta del contexto SELinux (httpd_sys_content_t) al contenido servido, evitando errores 403 y manteniendo riguroso control de acceso.
Monitoreo mediante interfase gráfica (Cockpit)
Integración visual para revisar el estado del runner y los despliegues en tiempo real, mejorando el control operativo y la visibilidad del sistema.
Despliegue seguro con staging
Uso de un directorio temporal (/tmp/web_new) como staging, evitando inconsistencias en el contenido servido durante el despliegue.
🧠 Conocimientos y herramientas aplicadas
Linux / RHEL 9
Dominio de usuarios y permisos, administración del sistema, manejo de paquetes, firewalls.
Shell scripting (Bash)
Creación y estructuración de scripts robustos (deploy_nginx.sh) con control de errores y logging.
Nginx
Instalación, configuración de virtual hosts, recarga de configuración (nginx -t, systemctl reload nginx).
Git y GitHub Actions
Control de versiones, automatización de flujos con CI/CD, configuración de workflows .yml en GitHub.
GitHub Self-Hosted Runner
Registro y configuración del runner en servidor propio, integración con systemd y timers.
systemd: Services y Timers
Servicios junto a timers para control automático y horario de ejecución, uso de systemctl, archivos .service y .timer.
SELinux
Gestión de contextos de seguridad con semanage fcontext y restorecon.
Copias de seguridad (Backups)
Creación de backups automáticos de la versión actual antes de desplegar nueva versión.
Logging y auditoría
Generación de registros secuenciales de las acciones del despliegue.
Seguridad y experiencia de usuario restringida
Aplicación del principio de menor privilegio creando un usuario dedicado con permisos mínimos necesarios.
Monitoreo con Cockpit
Visualización y control interactivo de servicios y despliegues.
Red, Firewall, SSH
Configuración del firewall (firewall-cmd), autenticación segura vía SSH con par de claves y configuración en .ssh/config.
DevOps e Infraestructura como Código
Flujo de integración continua, automatización de despliegue y configuración reproducible.