Problema

En entornos homelab o pequeñas infraestructuras se combina a menudo Proxmox como hipervisor, LXC para aislar servicios y Docker Compose para orquestar contenedores. Cuando se usa Renovate para mantener actualizadas las imágenes, el flujo típico es:

  1. Renovate detecta una nueva versión.
  2. Crea un Pull Request (PR) que actualiza el archivo docker-compose.yml.
  3. El administrador revisa y fusiona el PR.

El punto crítico aparece después del merge: los cambios deben llegar a cada LXC que ejecuta Docker, y el contenedor debe ser recreado con la nueva imagen. Hacer esto manualmente (login en cada LXC, git pull, docker compose up -d) es tedioso y propenso a errores, sobre todo cuando hay varios hosts. Necesitamos un mecanismo que:

  • Detecte el merge en GitHub.
  • Propague el archivo actualizado a los LXC correspondientes.
  • Ejecute los comandos de Docker de forma automática.
  • Mantenga GitHub como única fuente de verdad.
  • Permita limpiar imágenes obsoletas.

Causa

Los fallos habituales en este tipo de flujo provienen de tres áreas:

  1. Ausencia de orquestación post‑merge – GitHub no tiene por defecto un agente que notifique a los hosts. Sin un webhook o CI, los LXC nunca saben que el archivo cambió.
  2. Desalineación de credenciales – Cada LXC necesita acceso SSH sin contraseña al repositorio y al servidor de CI. Configuraciones incompletas provocan fallos de autenticación.
  3. Inconsistencias de entorno – Si el docker-compose.yml depende de variables de entorno locales, la ejecución remota puede fallar al no encontrar esas variables.

Solución

Un enfoque reusable combina Renovate, GitHub Actions y SSH para ejecutar docker compose en los LXC justo después del merge. Los pasos clave son:

  1. Repositorio único – Mantén todos los docker-compose.yml en un solo repo. Cada LXC tiene su propia carpeta (media/, adguard/, proxy/, …) para evitar colisiones.
  2. Clave SSH compartida – Genera una clave RSA dedicada al CI, añádela al archivo authorized_keys de cada LXC y como secret DEPLOY_KEY en GitHub. Así el workflow puede conectarse sin interacción.
  3. Workflow de GitHub Actions – Un job que se dispara con on: push a la rama main (o master). El job:
    • Detecta los archivos modificados con git diff --name-only ${{ github.event.before }} ${{ github.sha }}.
    • Por cada carpeta afectada, ejecuta un bloque SSH que:
      • cd al directorio del proyecto dentro del LXC.
      • git pull para sincronizar el compose.
      • docker compose pull para descargar la nueva imagen.
      • docker compose up -d --remove-orphans para recrear los contenedores.
      • docker image prune -f para limpiar imágenes no usadas.
  4. Manejo de variables – Usa un archivo .env versionado o almacenado como secret en GitHub y copiado al LXC mediante scp antes de ejecutar docker compose.

Ejemplo de workflow

name: Deploy Docker Compose updates

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repo
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Detect changed services
        id: changes
        run: |
          git diff --name-only ${{ github.event.before }} ${{ github.sha }} |
          grep '/docker-compose.yml$' |
          cut -d'/' -f1 |
          sort -u > services.txt
          echo "services=$(cat services.txt | tr '\n' ' ')" >> $GITHUB_OUTPUT

      - name: Deploy to each LXC
        if: steps.changes.outputs.services != ''
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
        run: |
          while read service; do
            echo "Deploying $service"
            ssh -i <(echo "$DEPLOY_KEY") -o StrictHostKeyChecking=no root@${service}.lxc <<'EOF'
              cd /opt/${service}
              git pull
              docker compose pull
              docker compose up -d --remove-orphans
              docker image prune -f
EOF
          done <<< "${{ steps.changes.outputs.services }}"

Detalles importantes

  • Naming convention: Cada LXC debe resolverse mediante DNS interno (media.lxc, adguard.lxc, …) o IP estática. El workflow usa ${service}.lxc para construir el host.
  • Privilegios: El usuario root simplifica la ejecución de Docker, pero se puede crear un usuario con membresía en el grupo docker y usar sudo.
  • Idempotencia: docker compose up -d --remove-orphans garantiza que los contenedores que ya no aparecen en el compose se eliminen automáticamente.

Cuándo aplicar esta solución

  • Múltiples hosts LXC que ejecutan Docker y comparten el mismo repositorio de compose.
  • Renovate o cualquier herramienta que genere PRs y requiere aprobación manual.
  • Entorno controlado donde los LXC están en la misma red y pueden ser alcanzados por SSH desde los runners de GitHub.
  • No es adecuada si:
    • Los contenedores se ejecutan en un clúster Kubernetes (K8s tiene su propio mecanismo de despliegue).
    • Los LXC están aislados detrás de firewalls sin acceso SSH desde la nube.
    • Se necesita un control de versiones de base de datos o migraciones complejas que requieran pasos adicionales.

Código

# Generar clave para CI (ejecutar una sola vez)
ssh-keygen -t rsa -b 4096 -C "github-actions-deploy" -f github_deploy_key -N ""

# Copiar clave pública a cada LXC
for host in media.lxc adguard.lxc proxy.lxc; do
  ssh-copy-id -i github_deploy_key.pub root@$host
done

# Añadir la clave privada como secret en GitHub (DEPLOY_KEY)
# No subir nunca el archivo a ningún repositorio.

Verificación

  1. Prueba de conectividad – Desde el runner de GitHub (puedes usar act localmente) ejecuta ssh -i github_deploy_key [email protected] echo ok. Debería responder ok.
  2. Simular cambio – Modifica manualmente un docker-compose.yml en una carpeta, haz commit y push a main. Observa el log del workflow; debe listar la carpeta como cambiada y ejecutar los comandos remotos.
  3. Validar contenedor – En el LXC, ejecuta docker ps y verifica que la etiqueta de la imagen corresponde a la versión esperada.
  4. Limpiar imágenes – Ejecuta docker image ls -a antes y después del despliegue; la lista de <none> debería reducirse.

Notas adicionales

  • Gestión de secretos: Si cada servicio necesita variables distintas, crea secrets con prefijo (MEDIA_DB_PASS, ADGUARD_API_KEY) y pásalos al workflow con env:. Dentro del bloque SSH, escribe los valores en un .env temporal antes de lanzar docker compose.
  • Rollback rápido – Si la actualización rompe algo, revertir el PR y volver a ejecutar el workflow restaura la versión anterior sin tocar los LXC manualmente.
  • Escalabilidad – Para más de diez LXC, considera agruparlos por zona y ejecutar despliegues en paralelo usando matrix en GitHub Actions.
  • Auditoría – Cada ejecución del workflow queda registrada en GitHub Actions, proporcionando un historial claro de quién aprobó y cuándo se desplegó cada cambio.

Con este patrón, la cadena de confianza se mantiene en GitHub, Renovate sigue proporcionando la visibilidad de actualizaciones y el proceso de despliegue se vuelve completamente automático una vez que el PR es mergeado. La solución es ligera, reutilizable y se adapta a la mayoría de los homelabs que combinan Proxmox, LXC y Docker Compose.