Este proyecto es un laboratorio de aprendizaje para implementar un pipeline de CI/CD completo usando GitHub Actions, Docker y despliegue automatizado en un servidor remoto.
Contribuir al proyecto? Consulta la Guia de Contribucion para conocer el flujo de trabajo con GitFlow.
El objetivo fue construir desde cero un sistema que permita:
A lo largo del desarrollo se encontraron multiples problemas que fueron resueltos iterativamente, documentados en la seccion Problemas Resueltos.
workflow_dispatch para ejecucion manual con parametrosneeds)if) para controlar ejecucion$GITHUB_OUTPUTincludeIf)Problema: Al clonar el repo desde el servidor, fallaba con "Host key verification failed".
Causa: El servidor no tenia configurada la llave SSH para GitHub.
Solucion: Cambiar de SSH a HTTPS para el clone:
# Antes (fallaba)
git clone git@github.com:user/repo.git
# Despues (funciona)
git clone https://github.com/user/repo.git
Problema: docker compose fallaba con "permission denied" al socket de Docker.
Causa: El usuario SSH no estaba en el grupo docker.
Solucion: Agregar usuario al grupo docker en el servidor:
sudo usermod -aG docker $USER
newgrp docker
Problema: El health check a localhost:3000 fallaba aunque el contenedor estaba corriendo.
Causa: Con Traefik, el puerto 3000 no esta expuesto al host, solo accesible via la red de Docker.
Solucion: Cambiar health check para usar el dominio de Traefik:
# Antes (fallaba)
curl http://localhost:3000/health
# Despues (funciona)
curl https://test.monghit.com/health
Problema: Despues de un rollback, el siguiente deploy restauraba la version anterior.
Causa: git pull no sobrescribe cambios locales en docker-compose.yml.
Solucion: Agregar reset del archivo antes del pull:
git checkout -- docker-compose.yml
git pull
Problema: Las imagenes con tag de version (:1.0.5) no existian en ghcr.io.
Causa: El tag de Git se creaba en el mismo workflow, pero GitHub no re-dispara el workflow para evitar loops.
Solucion: Crear los tags de imagen directamente en el build:
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.pkg_version.outputs.version }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
Problema: docker compose pull fallaba con "denied" al intentar descargar la imagen.
Causa: El workflow de rollback no hacia login a ghcr.io.
Solucion: Agregar login antes del pull:
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker compose pull
Problema: El rollback a v1.0.0 fallaba la verificacion aunque el contenedor corria bien.
Causa: La v1.0.0 no tenia el campo version en el endpoint /health.
Solucion: Aceptar respuestas sin campo version:
if [ -z "$DEPLOYED_VERSION" ]; then
echo "Version antigua sin info de version en /health"
echo "Health check OK"
exit 0
fi
Problema: docker compose ps | grep -q "running" fallaba aunque el contenedor estaba corriendo.
Causa: El formato de salida de docker compose ps cambio en versiones recientes.
Solucion: Confiar en el health check en lugar del grep:
# Antes (no confiable)
if ! docker compose ps | grep -q "running"; then
# Despues (confiable)
docker compose ps # Solo informativo
# Confiar en el health check HTTP
test-deploy/
├── .github/
│ └── workflows/
│ ├── deploy.yml # Deploy automatico en push a main
│ ├── rollback.yml # Rollback manual a version especifica
│ └── validate-version.yml # Validacion y auto-bump de version en PRs
├── .dockerignore # Excluir node_modules y .git
├── Dockerfile # Imagen Node.js Alpine con usuario no-root
├── docker-compose.yml # Configuracion con labels de Traefik
├── index.js # Servidor Express con Actuator y docs
├── package.json # Metadata y version de la app
├── CONTRIBUTING.md # Guia de contribucion con GitFlow
└── README.md
La aplicacion sirve documentacion markdown renderizada como HTML con soporte para diagramas Mermaid:
| Endpoint | Descripcion |
|---|---|
/ |
Pagina principal con lista de documentos |
/README |
README.md renderizado como HTML |
/CONTRIBUTING |
CONTRIBUTING.md renderizado como HTML |
Los diagramas Mermaid se renderizan automaticamente usando Mermaid.js.
Versiones visuales e interactivas de la documentacion:
| Endpoint | Descripcion |
|---|---|
/infografia/ |
Indice de infografias |
/infografia/README.html |
Infografia del README |
/infografia/CONTRIBUTING.html |
Infografia de CONTRIBUTING |
| Endpoint | Descripcion |
|---|---|
/health |
Health check simple: {"status": "ok", "app": "...", "version": "..."} |
Endpoints estilo Spring Boot Actuator para monitoreo y diagnostico:
| Endpoint | Descripcion |
|---|---|
/actuator |
Indice HAL-style con links a todos los endpoints |
/actuator/health |
Estado de salud (UP/DOWN) con componentes |
/actuator/info |
Info de la app, git commit y build |
/actuator/metrics |
Metricas del sistema (CPU, memoria, uptime) |
/actuator/env |
Variables de entorno (sensibles filtradas) |
{
"status": "UP",
"components": {
"diskSpace": { "status": "UP", "details": { "total": "31 GB", "free": "23 GB" } },
"memory": { "status": "UP", "details": { "used": "23%", "heap": "8 MB" } },
"process": { "status": "UP", "details": { "uptime": "5s", "pid": 1234 } }
}
}
{
"app": { "name": "test-deploy", "version": "1.1.6" },
"git": {
"commit": { "hash": "abc123...", "message": "feat: ...", "author": {...} },
"branch": "main"
},
"build": { "time": "2026-01-16T...", "nodeVersion": "v24.x.x" }
}
{
"measurements": {
"process.uptime": { "value": 123, "unit": "seconds" },
"system.cpu.count": { "value": 12, "unit": "cores" },
"system.cpu.load": { "value": [1.5, 1.2, 0.9], "unit": "average" },
"process.memory.heap.used": { "value": 8748160, "formatted": "8.34 MB" },
"system.memory.total": { "formatted": "31.04 GB" },
"system.memory.free": { "formatted": "23.64 GB" }
}
}
Variables de entorno con valores sensibles (password, token, key, secret, auth, etc.) filtrados automaticamente como ******.
Este proyecto utiliza workflows reutilizables centralizados en el repositorio monghit-server/.github. Esto permite:
| Workflow | Ubicacion | Descripcion |
|---|---|---|
docker-build.yml |
.github/workflows/ |
Build y push de imagen Docker a ghcr.io |
deploy.yml |
.github/workflows/ |
Deploy via SSH con health check |
validate-pr.yml |
.github/workflows/ |
Validacion de PR con auto-bump de version |
| Action | Ubicacion | Descripcion |
|---|---|---|
notify-telegram |
actions/ |
Notificaciones a Telegram via n8n |
docker-build |
actions/ |
Build de imagen con tags estandar |
deploy-ssh |
actions/ |
Deploy via SSH |
deploy.yml (68 lineas vs 188 originales):
jobs:
build:
uses: monghit-server/.github/.github/workflows/docker-build.yml@main
deploy:
uses: monghit-server/.github/.github/workflows/deploy.yml@main
with:
app_name: test-deploy
health_url: https://test.monghit.com/health
validate-version.yml (12 lineas vs 102 originales):
jobs:
validate:
uses: monghit-server/.github/.github/workflows/validate-pr.yml@main
.github/workflows/ci.yml:name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
validate:
if: github.event_name == 'pull_request'
uses: monghit-server/.github/.github/workflows/validate-pr.yml@main
build:
if: github.event_name == 'push'
permissions:
contents: read
packages: write
uses: monghit-server/.github/.github/workflows/docker-build.yml@main
deploy:
if: github.event_name == 'push'
needs: build
uses: monghit-server/.github/.github/workflows/deploy.yml@main
with:
app_name: mi-app
app_path: /opt/apps/mi-app
health_url: https://mi-app.monghit.com/health
secrets:
SERVER_HOST: ${{ secrets.SERVER_HOST }}
SERVER_USER: ${{ secrets.SERVER_USER }}
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
Se ejecuta en push a main:
package.jsonSe ejecuta manualmente desde GitHub Actions:
Se ejecuta en PRs a main usando el workflow centralizado:
npm auditCada build crea tres tags en ghcr.io:
| Tag | Descripcion | Ejemplo |
|---|---|---|
:main |
Ultima version de main | ghcr.io/user/repo:main |
:X.X.X |
Version especifica | ghcr.io/user/repo:1.0.10 |
:<sha> |
Commit especifico | ghcr.io/user/repo:abc123 |
Las imagenes se almacenan en ghcr.io y se descargan al servidor en cada deploy.
Tags de version en el servidor:
El deploy actualiza automaticamente el docker-compose.yml del servidor para usar el tag de version especifico (ej: :1.1.5). Esto tiene dos ventajas:
Limpieza manual (si es necesario):
# Eliminar imagenes sin tag (dangling)
docker image prune -f
# Ver imagenes actuales
docker images
Las capas base de Docker son compartidas entre versiones, por lo que el espacio real utilizado es menor al mostrado.
| Medida | Descripcion |
|---|---|
| npm audit | Escaneo de vulnerabilidades en dependencias (HIGH/CRITICAL) |
| Trivy | Escaneo de vulnerabilidades en imagen Docker |
| Docker no-root | Contenedor ejecuta como usuario nodejs (UID 1001) |
| Versiones fijadas | GitHub Actions usan versiones especificas (no @latest) |
| Branch protection | PRs requeridos, checks obligatorios |
# 1. Actualizar version en package.json
# 2. Commit y push
git add -A
git commit -m "Bump version to X.X.X"
git push origin main
1.0.8) o dejar vacio para ver disponiblesLas notificaciones se envian a un webhook de n8n que las redirige a Telegram. Se activan cuando falla cualquier paso de los workflows.
Las notificaciones estan centralizadas en una Composite Action reutilizable ubicada en el repositorio central monghit-server/.github/actions/notify-telegram/. Esto permite:
Uso en workflows:
- name: Notify Telegram on failure
if: failure()
uses: monghit-server/.github/actions/notify-telegram@main
with:
webhook_url: ${{ secrets.N8N_WEBHOOK_TO_TELEGRAM_URL }}
event: deploy_failed
repository: ${{ github.repository }}
actor: ${{ github.actor }}
run_url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
| Evento | Workflow | Descripcion |
|---|---|---|
create_tag_failed |
deploy.yml | Fallo al crear el tag de version |
build_failed |
deploy.yml | Fallo al construir o subir la imagen Docker |
deploy_failed |
deploy.yml | Fallo al desplegar en el servidor |
rollback_failed |
rollback.yml | Fallo en cualquier paso del rollback |
version_check_failed |
validate-version.yml | Version invalida o tag ya existe |
security_audit_failed |
validate-version.yml | Vulnerabilidades encontradas en npm audit |
{
"event": "deploy_failed",
"repository": "user/repo",
"branch": "main",
"commit": "abc123...",
"version": "1.0.0",
"actor": "username",
"run_url": "https://github.com/user/repo/actions/runs/123"
}
| Secret | Descripcion |
|---|---|
SERVER_HOST |
IP o dominio del servidor |
SERVER_USER |
Usuario SSH |
SSH_PRIVATE_KEY |
Llave privada SSH |
N8N_WEBHOOK_TO_TELEGRAM_URL |
URL del webhook de n8n para notificaciones a Telegram |
SERVER_HOST) y su valorEjemplo para SSH_PRIVATE_KEY:
# Copiar el contenido de la llave privada
cat ~/.ssh/id_ed25519
Pegar el contenido completo incluyendo -----BEGIN OPENSSH PRIVATE KEY----- y -----END OPENSSH PRIVATE KEY-----.
Ejemplo para N8N_WEBHOOK_TO_TELEGRAM_URL:
https://tu-instancia-n8n.com/webhook/xxx-xxx-xxx
Esta URL se obtiene del nodo Webhook en tu workflow de n8n que envia mensajes a Telegram.
Configurar en Settings > Branches > main:
check-version, security-auditPara usar diferentes usuarios de Git por carpeta:
# ~/.gitconfig
[includeIf "gitdir:~/Git/personal/monghithub/"]
path = ~/.gitconfig-monghit
# ~/.gitconfig-monghit
[user]
name = usuario
email = email@example.com
[core]
sshCommand = ssh -i ~/.ssh/id_ed25519_github2 -o IdentitiesOnly=yes
| Categoria | Tecnologia |
|---|---|
| Runtime | Node.js 20 Alpine |
| Framework | Express |
| Contenedor | Docker + Docker Compose |
| Proxy | Traefik v3 con Let's Encrypt |
| CI/CD | GitHub Actions |
| Registry | GitHub Container Registry (ghcr.io) |
| Seguridad | Trivy, npm audit |
Disponibles versiones visuales e interactivas de la documentacion:
Ver la version visual de este documento para un resumen interactivo.
MIT