Despliegue continuo con GitHub Actions y Docker Compose en VPS: guía completa con secrets, healthcheck y rollback
Configura un pipeline CI/CD real que haga deploy automático en tu VPS cada vez que haces push a main. Con SSH seguro, manejo de secretos, variables de entorno, healthcheck de servicios, notificaciones en Discord y rollback manual. Todo con Docker Compose y GitHub Actions, listo para producción.
Por Equipo Starbyte
Despliegue continuo con GitHub Actions y Docker Compose en VPS: guía completa con secrets, healthcheck y rollback
Problema real: Desarrollas una aplicación con varios servicios (backend, frontend, base de datos) definidos en Docker Compose. Cada vez que fusionas cambios en la rama main, debes conectarte por SSH al VPS, hacer git pull, reconstruir imágenes, lanzar los contenedores y verificar que todo funcione. Es repetitivo, propenso a errores humanos y te roba tiempo valioso. Peor aún, si algo falla en producción, no tienes un mecanismo rápido de rollback.
Este post te guía para montar un pipeline de despliegue continuo (CD) usando GitHub Actions para automatizar el deploy en tu VPS cada push a main, con gestión segura de secretos, verificación de salud de los servicios y una estrategia simple de vuelta atrás.
Requisitos previos
- Un VPS con Linux (Ubuntu 22.04/24.04) y Docker + Docker Compose v2 instalados.
- Tu código fuente en un repositorio privado (o público si no hay secretos sensibles en el código, aunque lo habitual es privado) en GitHub.
- La aplicación definida en un archivo
docker-compose.yml(o varios) en la raíz del repo. - Clave SSH pública/privada para acceder al VPS sin contraseña.
- Un webhook o bot de Discord (opcional) para notificaciones de deploy.
1. Preparar el VPS: usuario de despliegue dedicado
Por seguridad, crea un usuario específico para los deploys, sin privilegios totales pero con acceso a Docker.
sudo adduser deploy --disabled-password
sudo usermod -aG docker deploy
Verifica que puede ejecutar Docker sin sudo:
sudo -u deploy docker ps
Crea el directorio donde residirá la aplicación:
sudo mkdir -p /opt/app
sudo chown deploy:deploy /opt/app
2. Configurar acceso SSH sin contraseña desde GitHub Actions
Genera un par de claves SSH (en tu máquina local o en el VPS, mejor en entorno aislado):
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ./deploy_key
Esto crea deploy_key (privada) y deploy_key.pub (pública).
En el VPS, añade la pública al authorized_keys del usuario deploy:
sudo -u deploy mkdir -p /home/deploy/.ssh
echo "contenido-de-deploy_key.pub" | sudo tee -a /home/deploy/.ssh/authorized_keys
sudo chmod 600 /home/deploy/.ssh/authorized_keys
sudo chown -R deploy:deploy /home/deploy/.ssh
Prueba la conexión desde tu máquina local (con la clave privada):
ssh -i deploy_key deploy@IP_DEL_VPS
3. Configurar secretos en GitHub
En tu repositorio, ve a Settings > Secrets and variables > Actions y añade estos secretos:
VPS_HOST: la IP o dominio del VPS.VPS_USER:deployVPS_SSH_KEY: el contenido completo del archivodeploy_key(incluyendo cabecera y pie).REGISTRY_USERyREGISTRY_PASSWORD(opcional, si usas un registro privado como Docker Hub o GHCR).DISCORD_WEBHOOK_URL(opcional).
Ejemplo de cómo pegar la clave:
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
4. Workflow de GitHub Actions: .github/workflows/deploy.yml
Crea el archivo en tu rama principal con este contenido:
name: Deploy to VPS
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-24.04
steps:
- name: Checkout código
uses: actions/checkout@v4
- name: Configurar SSH
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.VPS_SSH_KEY }}
- name: Desplegar en VPS
env:
VPS_USER: ${{ secrets.VPS_USER }}
VPS_HOST: ${{ secrets.VPS_HOST }}
run: |
ssh -o StrictHostKeyChecking=accept-new $VPS_USER@$VPS_HOST << 'EOF'
set -e
cd /opt/app
git pull origin main
docker compose pull
docker compose up -d --remove-orphans
docker compose ps
EOF
- name: Healthcheck básico
env:
VPS_USER: ${{ secrets.VPS_USER }}
VPS_HOST: ${{ secrets.VPS_HOST }}
run: |
ssh $VPS_USER@$VPS_HOST "docker compose -f /opt/app/docker-compose.yml ps -q | xargs docker inspect -f '{{.Name}} {{.State.Health.Status}}'"
- name: Notificar en Discord (éxito)
if: success()
uses: sarisia/actions-status-discord@v1
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_URL }}
title: "✅ Deploy exitoso en ${{ github.repository }}"
description: "Commit ${{ github.sha }} desplegado en VPS."
color: 0x00ff00
- name: Notificar en Discord (fallo)
if: failure()
uses: sarisia/actions-status-discord@v1
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_URL }}
title: "❌ Deploy FALLIDO en ${{ github.repository }}"
description: "Revisa los logs del workflow: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
color: 0xff0000
Explicación de partes clave:
webfactory/ssh-agentcarga la clave privada en el agente SSH del runner, así el comandosshno necesita-i.- El heredoc (
<< 'EOF') ejecuta comandos en el VPS de forma segura. docker compose pullactualiza las imágenes (si usas una etiquetalatesto una fija, pero mejor etiqueta versionada; ver buenas prácticas).--remove-orphanslimpia contenedores de servicios que ya no estén en el compose.- El healthcheck básico asume que tus servicios tienen
healthcheckconfigurado en el compose; si no, puedes hacercurla un endpoint de estado.
5. Configurar healthchecks en Docker Compose (ejemplo)
En tu docker-compose.yml, añade para un servicio web:
services:
backend:
image: mi-backend:latest
ports:
- "3000:3000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
Así, el paso de healthcheck del workflow leerá el estado real.
6. Rollback rápido en caso de emergencia
El pipeline no sobrescribe el directorio git ni las imágenes antiguas, solo actualiza. Para volver atrás manualmente:
ssh deploy@VPS "cd /opt/app && git log --oneline -5"
Elige el commit anterior bueno y ejecuta:
ssh deploy@VPS "cd /opt/app && git checkout <commit-hash> && docker compose up -d"
Si la imagen de Docker es la que falla y no el código, puedes volver a una versión anterior de la imagen (si seguiste la práctica de etiquetar con versión explícita). Cambia la etiqueta en docker-compose.yml o ejecuta:
docker compose up -d backend=mi-backend:v1.2.3
O simplemente edita el compose y vuelve a hacer up.
7. Errores frecuentes y soluciones
| Error | Causa | Solución |
|---|---|---|
Permission denied (publickey) |
Clave SSH mal copiada, formato incorrecto, o no añadida a authorized_keys en el usuario correcto |
Revisa que el secreto VPS_SSH_KEY tenga los saltos de línea exactos; en VPS asegura permisos 600 y propiedad correcta de .ssh. |
docker: Got permission denied al ejecutar docker ps |
Usuario deploy no está en grupo docker o no ha hecho logout/login |
Ejecuta newgrp docker o reinicia sesión; también puede requerir reinicio del servicio docker. |
docker compose no encontrado en VPS |
Docker Compose v1 en lugar de v2, o no instalado como plugin | Instala Docker Compose v2 o usa docker-compose (con guión). Ajusta el comando en el workflow. |
git pull falla por conflictos locales |
Cambios manuales en VPS (logs, configuraciones) que interfieren con el pull | Nunca modifiques archivos rastreados en /opt/app. Si es necesario, usa .gitignore o volúmenes externos. |
Healthcheck muestra "starting" o "unhealthy" tras deploy |
El contenedor necesita más tiempo para iniciar | Aumenta start_period o interval en el healthcheck. |
| El webhook de Discord no recibe notificaciones | Token del webhook inválido o falta la acción en el workflow | Verifica que el secreto DISCORD_WEBHOOK_URL esté completo y que la acción sarisia/actions-status-discord esté correctamente escrita. |
8. Casos prácticos de uso
8.1 Aplicación multi-repo
Puedes tener un repositorio solo de infraestructura (compose, configuraciones) y que el pipeline se dispare allí, trayendo imágenes de varios registros. El paso git pull solo actualiza ese repo, y docker compose pull descarga las nuevas imágenes.
8.2 Deploy solo si cambia una carpeta específica
Si usas monorepo, añade filtros:
on:
push:
branches: [main]
paths:
- 'backend/**'
8.3 Variables de entorno secretas
Para pasar secretos que no deben estar en el repo (como claves de API), crea un archivo .env en el VPS previamente y no lo versiones. En el workflow, tras git pull, copia ese archivo si es necesario o bien incluye un step que escriba las variables desde secretos de GitHub (menos seguro porque quedan en logs si no se oculta).
Mejor práctica: archivo .env manejado directamente en el servidor y respaldado aparte.
9. Buenas prácticas
- Usa siempre etiquetas de imagen explícitas (con versión semántica) en lugar de
latesten producción. Así sabes exactamente qué versión está corriendo y puedes hacer rollback más seguro. - Nunca almacenes secretos en el repositorio (ni en variables de entorno no ignoradas). Usa GitHub Secrets para la configuración del pipeline y
.envlocal en el VPS con permisos 600. - Haz que el pipeline sea idempotente (
docker compose up -dlo es). Evita lógica que dependa de estado previo. - Configura healthchecks reales en tus servicios, no solo
curla una ruta; que verifiquen conexiones a base de datos, caché, etc. - Protege el acceso SSH: además de clave sin passphrase, restringe el origen en el firewall (solo IPs de GitHub Actions runners, o usa
actions/checkoutcon ssh sin exponer puerto 22 globalmente). Los runners tienen IPs dinámicas, así que una opción es un túnel inverso o Cloudflare Tunnel, pero escapa del alcance. Al menos, usaStrictHostKeyChecking=accept-newsolo la primera vez. - Mantén los logs limpios: en el step
Desplegar en VPS, no imprimas variables sensibles (evitaset -xsi incluye secretos). Usaset -epara detener ante errores, pero asegura que los comandos críticos no fallen en silencio.
10. Cierre con idea clave
Dejar de hacer deploys manuales no es un lujo, es una necesidad para escalar tu productividad y reducir riesgos. Con GitHub Actions y Docker Compose en un VPS, puedes tener un flujo de entrega continua sólido, seguro y verificable en pocos pasos. El beneficio inmediato: cada nuevo merge a main se convierte en una actualización en producción sin fricción y con la garantía de que los servicios están sanos.