Proyecto de infraestructura: montar un pipeline CI/CD completo y autoalojado, desde cero, sin depender de servicios externos como GitHub Actions o GitLab CI.

1. Introducción

¿Qué es CI/CD? Metodología que automatiza la integración y el despliegue de código.

  • CI (Integración Continua): ejecuta tests automáticamente en cada cambio de código.
  • CD (Despliegue Continuo): despliega a producción automáticamente si los tests pasan.

Objetivo del proyecto:

  1. Almacenar el código en un servidor Git privado (Gitea)
  2. Detectar automáticamente cambios en el repositorio
  3. Ejecutar tareas de despliegue automáticamente
  4. Actualizar el servidor web sin intervención manual

2. Instalación de Gitea

Crear el contenedor LXC (Osiris)

1sudo lxc-create -n osiris -t download -- -d debian -r bookworm -a amd64
2lxc-start -n osiris
3lxc-attach -n osiris

Configurar red del contenedor

IP estática en la DMZ (192.168.0.5):

1nano /etc/systemd/network/10-eth0.network
1[Match]
2Name=eth0
3
4[Network]
5Address=192.168.0.5/24
6Gateway=192.168.0.1
7DNS=192.168.0.2
1systemctl enable systemd-networkd
2systemctl start systemd-networkd

Instalar dependencias

1apt update && apt install -y git wget sqlite3

Crear usuario para Gitea

Por seguridad, Gitea no se ejecuta como root:

1useradd -r -d /var/lib/gitea -s /bin/bash -m gitea

Descargar e instalar Gitea

1cd /tmp
2wget https://dl.gitea.com/gitea/1.21.7/gitea-1.21.7-linux-amd64 -O gitea
3chmod +x gitea
4mv gitea /usr/local/bin/

Crear los directorios necesarios

1mkdir -p /var/lib/gitea/{custom,data,log}
2mkdir -p /etc/gitea
3chown -R gitea:gitea /var/lib/gitea
4chown -R gitea:gitea /etc/gitea

Crear servicio systemd

1nano /etc/systemd/system/gitea.service
 1[Unit]
 2Description=Gitea (Git with a cup of tea)
 3After=network.target
 4
 5[Service]
 6Type=simple
 7User=gitea
 8Group=gitea
 9WorkingDirectory=/var/lib/gitea
10ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini
11Restart=always
12Environment=USER=gitea HOME=/var/lib/gitea GITEA_WORK_DIR=/var/lib/gitea
13
14[Install]
15WantedBy=multi-user.target

Explicación de cada sección:

  • [Unit] → metadatos: descripción y que arranque después de que la red esté lista (After=network.target)
  • [Service]Type=simple (proceso en primer plano), User=gitea (no root), Restart=always (se reinicia si se cae), Environment con las variables necesarias
  • [Install]WantedBy=multi-user.target, se inicia en modo multiusuario
1systemctl daemon-reload
2systemctl enable gitea
3systemctl start gitea
4systemctl status gitea

Estado del servicio gitea activo y corriendo

Configurar DNAT en RA

Para acceder a Gitea desde internet, redirigir el puerto 3000:

1sudo iptables -t nat -A PREROUTING -i ens3 -p tcp --dport 3000 -j DNAT --to-destination 192.168.0.5:3000
2sudo iptables-save > /etc/iptables/rules.v4

Configurar DNS

Para acceder con nombre de dominio en vez de IP. Conectar a ISIS:

1lxc-attach -n isis

Zona DNS externa:

1nano /etc/bind/zones/db.externa.josemanuel.gonzalonazareno.org
1git IN A 172.22.201.179

Zona DNS interna:

1nano /etc/bind/zones/db.josemanuel.gonzalonazareno.org
1git IN A 192.168.0.5
1systemctl restart bind9

Configuración inicial de Gitea

Acceder vía navegador a http://172.22.201.179:3000 y completar el instalador:

Instalador de Gitea con la configuración de base de datos y del sitio

Cuenta de administrador:

  • Usuario: josemanuel
  • Email: ***@gmail.com
  • Contraseña: ********

Página principal de Gitea ya instalado

Crear un repositorio de prueba

En la interfaz de Gitea: botón + → Nuevo repositorio → Nombre: Web-Demo → Descripción: “Sitio web de prueba para CI/CD” → marcar “Inicializar repositorio” → Licencia: MIT License → .gitignore vacío → Crear repositorio.

Repositorio Web-Demo recién creado en Gitea

3. Instalación de Drone CI

Crear el contenedor LXC (Thoth)

1lxc-create -n thoth -t download -- -d debian -r bookworm -a amd64
2lxc-start -n thoth
3lxc-attach -n thoth

Configurar la red

IP estática 192.168.0.6:

1nano /etc/systemd/network/10-eth0.network
1[Match]
2Name=eth0
3
4[Network]
5Address=192.168.0.6/24
6Gateway=192.168.0.1
7DNS=192.168.0.2
1systemctl enable systemd-networkd
2systemctl start systemd-networkd

Instalar Docker

Drone se ejecuta dentro de contenedores Docker:

1apt update && apt install -y ca-certificates curl gnupg
2install -m 0755 -d /etc/apt/keyrings
3curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
4chmod a+r /etc/apt/keyrings/docker.asc
5echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
6apt update && apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

Configurar LXC para Docker anidado

Docker dentro de LXC requiere configuración especial; si no, Thoth no deja crear contenedores Docker. En RA, editar la configuración del contenedor:

1nano /var/lib/lxc/thoth/config
1lxc.apparmor.profile = unconfined
2lxc.cgroup2.devices.allow = a
3lxc.cap.drop =
4lxc.mount.auto = proc:rw sys:rw cgroup:rw
1lxc-stop -n thoth
2lxc-start -n thoth
3lxc-attach -n thoth

Generar una contraseña compartida

Drone Server y Runner se comunican mediante un secreto compartido:

1openssl rand -hex 16

Generación del secreto RPC compartido

Crear aplicación OAuth en Gitea

Drone necesita autenticarse con Gitea vía OAuth:

  1. Abrir Gitea → login como josemanuel
  2. Avatar → Configuración → Aplicaciones
  3. Sección “Administrar aplicaciones OAuth2” → Crear una nueva aplicación OAuth2
  4. Nombre: Drone CI; URL de redirección: http://drone.josemanuel.gonzalonazareno.org:8080/login
  5. Crear aplicación

OAuth es un protocolo de autorización que permite a Drone acceder a Gitea sin necesitar la contraseña del usuario.

Formulario de creación de la aplicación OAuth2 Drone CI Client ID y Client Secret generados tras crear la aplicación Aplicación Drone CI listada entre las OAuth2 autorizadas Pantalla de autorización de la aplicación Drone CI Aplicaciones OAuth2 autorizadas, con Drone CI listado

Crear configuración de Drone Server

1mkdir -p /var/lib/drone
2nano /root/drone.env
 1DRONE_GITEA_SERVER=http://git.josemanuel.gonzalonazareno.org:3000
 2DRONE_GITEA_CLIENT_ID=********
 3DRONE_GITEA_CLIENT_SECRET=********
 4DRONE_RPC_SECRET=********
 5DRONE_SERVER_HOST=drone.josemanuel.gonzalonazareno.org:8080
 6DRONE_SERVER_PROTO=http
 7DRONE_DATABASE_DRIVER=sqlite3
 8DRONE_DATABASE_DATASOURCE=/data/database.sqlite
 9DRONE_USER_CREATE=username:josemanuel,admin:true
10DRONE_SERVER_PORT=:8080

Explicación de cada variable:

  • DRONE_GITEA_SERVER → URL de Gitea
  • DRONE_GITEA_CLIENT_ID / DRONE_GITEA_CLIENT_SECRET → credenciales OAuth de Gitea
  • DRONE_RPC_SECRET → secreto para la comunicación Server↔Runner
  • DRONE_SERVER_HOST / DRONE_SERVER_PROTO → dominio público y protocolo de Drone
  • DRONE_DATABASE_DRIVER / DRONE_DATABASE_DATASOURCE → base de datos (SQLite)
  • DRONE_USER_CREATE → crea automáticamente un usuario admin
  • DRONE_SERVER_PORT → puerto interno

Ejecutar Drone Server con Docker

 1docker network create drone-network
 2
 3docker run \
 4  --privileged \
 5  --network=drone-network \
 6  --volume=/var/lib/drone:/data \
 7  --env-file=/root/drone.env \
 8  --publish=8080:8080 \
 9  --restart=always \
10  --detach=true \
11  --name=drone \
12  drone/drone:2

Configurar DNAT y DNS para Drone

1sudo iptables -t nat -A PREROUTING -i ens3 -p tcp --dport 8080 -j DNAT --to-destination 192.168.0.6:8080
2sudo iptables-save > /etc/iptables/rules.v4
1lxc-attach -n isis
2nano /etc/bind/zones/db.externa.josemanuel.gonzalonazareno.org
1drone IN A 172.22.201.179
1nano /etc/bind/zones/db.josemanuel.gonzalonazareno.org
1drone IN A 192.168.0.6
1systemctl restart bind9

Acceder a Drone

1http://drone.josemanuel.gonzalonazareno.org:8080

Pantalla de bienvenida de Drone Formulario de registro completando el email y nombre

Instalar Drone Runner

El Runner es el que ejecuta las tareas:

1lxc-attach -n thoth
2nano /root/drone-runner.env
1DRONE_RPC_PROTO=http
2DRONE_RPC_HOST=drone:8080
3DRONE_RPC_SECRET=********
4DRONE_RUNNER_CAPACITY=2
5DRONE_RUNNER_NAME=docker-runner

Explicación:

  • DRONE_RPC_PROTO / DRONE_RPC_HOST → protocolo y nombre del contenedor Drone Server (resuelto por la red Docker drone-network)
  • DRONE_RPC_SECRET → mismo secreto que el Server
  • DRONE_RUNNER_CAPACITY → máximo de builds simultáneos
  • DRONE_RUNNER_NAME → nombre identificativo del runner
1docker run \
2  --privileged \
3  --network=drone-network \
4  --volume=/var/run/docker.sock:/var/run/docker.sock \
5  --env-file=/root/drone-runner.env \
6  --restart=always \
7  --detach=true \
8  --name=runner \
9  drone/drone-runner-docker:1

4. Configuración del Pipeline

Configurar repositorio en Anubis

Clonar el repositorio desde Gitea:

1ssh anubis
2git config --global user.name "Jose Manuel"
3git config --global user.email "***@gmail.com"
4cd /var/www/html
5sudo git clone http://git.josemanuel.gonzalonazareno.org:3000/josemanuel/Web-Demo.git web-demo

Clonado del repositorio Web-Demo en Anubis

Configurar Apache para servir este directorio:

1sudo nano /etc/httpd/conf.d/web-demo.conf
 1<VirtualHost *:80>
 2    ServerName www.josemanuel.gonzalonazareno.org
 3    DocumentRoot /var/www/html/web-demo
 4
 5    <Directory /var/www/html/web-demo>
 6        AllowOverride All
 7        Require all granted
 8    </Directory>
 9
10    ErrorLog /var/log/httpd/web-demo-error.log
11    CustomLog /var/log/httpd/web-demo-access.log combined
12</VirtualHost>
1sudo chown -R apache:apache /var/www/html/web-demo
2sudo chmod -R 755 /var/www/html/web-demo
3sudo systemctl restart httpd

Crear un index con una plantilla

1cd web-demo
2sudo nano index.html
 1<!DOCTYPE html>
 2<html lang="es">
 3<head>
 4<meta charset="UTF-8">
 5<meta name="viewport" content="width=device-width, initial-scale=1.0">
 6<title>Web Demo - CI/CD Pipeline</title>
 7<style>
 8body {
 9  font-family: Arial, sans-serif;
10  max-width: 800px;
11  margin: 50px auto;
12  padding: 20px;
13  background: #f0f0f0;
14}
15.container {
16  background: white;
17  padding: 30px;
18  border-radius: 10px;
19  box-shadow: 0 2px 10px rgba(0,0,0,0.1);
20}
21h1 { color: #2c3e50; }
22.version { color: #7f8c8d; font-size: 14px; }
23</style>
24</head>
25<body>
26<div class="container">
27  <h1>Pipeline CI/CD Funcionando</h1>
28  <p>Esta página se despliega automáticamente desde Gitea.</p>
29</div>
30</body>
31</html>
1sudo git add index.html
2sudo git commit -m "Añadido index.html - versión 1.0"
3sudo git push origin main

Al hacer push, pide las credenciales de Gitea.

Commit y push del index.html inicial

Configurar clave SSH para despliegue

Para que Drone pueda desplegar automáticamente en Anubis, necesita autenticarse por SSH:

1ssh-keygen -t ed25519 -f ~/.ssh/drone_deploy -N ""

Configurar secreto en Drone

  1. En Drone → repositorio josemanuel/Web-Demo → Settings → Secrets → New Secret
  2. Rellenar: Name: ssh_key; Value: la clave privada completa; Pull Request Access: Disabled
  3. Create

Los secretos en Drone se almacenan encriptados y solo están disponibles durante la ejecución del pipeline — es una forma segura de manejar credenciales.

Generación de la clave SSH y creación del secreto ssh_key en Drone Secreto ssh_key ya listado en la configuración del repositorio

Activar repositorio en Drone

En Drone → repositorio → Settings → General: Project Visibility: Public; Configuration: .drone.yml; Timeout: 60 minutes → Guardar cambios → Activate Repository.

Configuración general y activación del repositorio en Drone

Crear archivo .drone.yml

Define qué hacer cuando hay un push. En Anubis, dentro de web-demo:

1cd /var/www/html/web-demo
2sudo nano .drone.yml
 1kind: pipeline
 2type: docker
 3name: deploy-web
 4
 5steps:
 6- name: deploy-to-anubis
 7  image: appleboy/drone-ssh
 8  settings:
 9    host: 172.16.0.200
10    username: josemanuel
11    key:
12      from_secret: ssh_key
13    port: 22
14    script:
15      - cd /var/www/html/web-demo
16      - git pull origin main
17      - echo "Desplegado exitosamente en $(date)"

Explicación del fichero:

  • kind: pipeline / type: docker → pipeline que se ejecuta en contenedores Docker
  • name: deploy-web → nombre descriptivo
  • steps → lista de pasos; aquí un único paso deploy-to-anubis
  • image: appleboy/drone-ssh → imagen que ejecuta comandos por SSH
  • host / username / port → datos de conexión al servidor de despliegue
  • key: from_secret: ssh_key → usa la clave SSH guardada como secreto
  • script → comandos remotos: entra al directorio, hace git pull y confirma con un mensaje

Subir .drone.yml a Gitea

1sudo git add .drone.yml
2sudo git commit -m "Añadido pipeline CI/CD"
3sudo git push origin main

Este push dispara automáticamente el pipeline en Drone.

5. Pruebas y Verificación

Verificar build en Drone

Drone → josemanuel/Web-Demo → Builds. Debería verse un build ejecutándose o completado.

Dashboard de Drone con el build completado en verde

Ver logs del build

Al hacer clic en el build se ven los logs detallados de cada paso (clone, deploy-to-anubis).

Logs detallados del pipeline, con clone y deploy-to-anubis en verde

Despliegue automático completo

Prueba de extremo a extremo: modificar index.html en Anubis y comprobar que se despliega solo.

1cd /var/www/html/web-demo
2sudo nano index.html
3sudo git add index.html
4sudo git commit -m "Actualizado a versión xxx"
5sudo git push origin main

Verificación: en Drone aparece un build nuevo automáticamente, y http://www.josemanuel.gonzalonazareno.org muestra la versión nueva sin haber copiado archivos a mano.

Commit “Actualizado a versión 2.0” subido a Gitea Build correspondiente reflejado en el repositorio Web-Demo

Conclusión

Con esto queda montado un pipeline CI/CD autoalojado y funcional: Gitea como servidor Git privado, Drone como motor de CI/CD (Server + Runner sobre Docker), y un despliegue automático por SSH a un servidor Apache cada vez que se hace push a main.