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

  1. 🎯 Objetivos
  2. 🧱 Estructura general del entorno
  3. 👤 Creación del usuario administrador para despliegue
  4. ⚙️ Instalación de Nginx y configuración firewall
  5. 🌐 Creación de la web local en Windows
  6. 🔑 Generación de clave SSH y conexión con el servidor
  7. ☁️ Primera subida del proyecto a GitHub
  8. 🤖 Configuración del workflow de GitHub Actions (Self-hosted Runner)
  9. 🛠️ Instalación y configuración del runner en RHEL
  10. 🧩 Configuración del runner como servicio systemd
  11. ⏱️ Automatización y control de la ventana horaria
  12. 📦 Automatización del despliegue con backups y logs
  13. 🧪 Modificar configuración del servidor nginx final
  14. 🛡️Dar contexto a selinux
  15. 🚀Puesta en marcha y actualización por git.
  16. 🔒 Buenas prácticas aplicadas
  17. 🧠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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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
  • 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


  1. Checkout del código
    • Primero, el workflow obtiene el código del repositorio para que podamos trabajar con los archivos más recientes.
  2. 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
  3. 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.

🛠️ 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:

  1. Registro de logs
    • Todos los pasos se registran en /var/log/deploy_nginx.log para auditoría y seguimiento.
  2. 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.
  3. Detener Nginx
    • Evita conflictos al reemplazar archivos activos del servidor web.
  4. Backup de la web actual
    • Crea un backup comprimido de /var/www/html en /var/backups/nginx_fecha-hora.
  5. Copiar nuevos archivos
    • Borra el contenido antiguo en /var/www/html y copia los nuevos archivos desde /tmp/web_new.
  6. Limpiar staging
    • Elimina /tmp/web_new para que no queden restos de despliegues anteriores.
  7. Reiniciar Nginx
    • Levanta el servicio web para que la nueva versión esté disponible.
  8. 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.

CI/CD self-hosted: gestión de Nginx con servicios, timers y backups

Post navigation