En un artículo anterior montamos un servidor Git minimalista usando solo SSH: usuario dedicado, repositorios bare, claves restringidas y cero dependencias externas.
Ahora vamos a darle el siguiente nivel de utilidad: despliegue automático cada vez que hagas push.
Con un solo hook vas a lograr que cada git push actualice directamente tu aplicación en producción. Sin GitHub Actions, sin GitLab CI, sin Docker Compose interminable, sin YAML de 400 líneas. Solo Bash, Git y tu VPS.
El hook post-receive: qué es y por qué es útil
Git tiene varios puntos donde puedes «colocar» scripts personalizados (los famosos hooks). El que nos interesa hoy es post-receive, que se ejecuta en el servidor, justo después de que el push haya sido aceptado con éxito.
Ubicación típica en nuestro setup anterior:
/home/git/repositories/personal/mi-proyecto.git/hooks/post-receive
Ventajas principales de usar este hook:
- No bloquea al desarrollador: el push ya terminó cuando el script empieza a correr
- Lógica separada: perfecto para builds, migraciones, reinicios o lo que necesites
- Cero dependencias externas: es Bash puro (o el lenguaje que prefieras)
Importante sobre los ejemplos que vienen a continuación
Los casos A, B y C son versiones muy simplificadas solo para que entiendas la mecánica. En producción casi siempre querrás filtrar por rama (desplegar solo main / master / lo que uses). Para eso mira el script completo recomendado al final del artículo.
Casos de uso típicos (y su script básico)
Caso A – Sitio web estático (HTML + CSS + JS)
Despliegue directo con checkout forzado.
#!/usr/bin/env bash
unset $(git rev-parse --local-env-vars)
REPO="/home/git/repositories/personal/mi-web.git"
WEBROOT="/var/www/mi-web"
git --work-tree="$WEBROOT" --git-dir="$REPO" checkout -f main
echo "Desplegado en $WEBROOT → $(date)"
Caso B – Frontend con build (Node, React, Vite, etc.)
Checkout –> instalación limpia –> build –> rsync atómico.
#!/usr/bin/env bash
unset $(git rev-parse --local-env-vars)
REPO="/home/git/repositories/personal/mi-app.git"
WORKDIR="/srv/builds/mi-app"
WEBROOT="/var/www/mi-app"
mkdir -p "$WORKDIR"
git --work-tree="$WORKDIR" --git-dir="$REPO" checkout -f main
cd "$WORKDIR"
npm ci --prefer-offline --no-audit --progress=false
npm run build
rsync -a --delete "$WORKDIR/dist/" "$WEBROOT/"
echo "Build y rsync completados → $(date)"
Caso C – Backend (Python, PHP, Go, etc.)
Actualización de código + dependencias + reinicio de servicio.
#!/usr/bin/env bash
unset $(git rev-parse --local-env-vars)
REPO="/home/git/repositories/personal/mi-api.git"
APPDIR="/srv/mi-api"
git --work-tree="$APPDIR" --git-dir="$REPO" checkout -f main
cd "$APPDIR"
pip install -r requirements.txt --quiet --no-cache-dir
# Requiere configuración en sudoers (ver más abajo)
sudo systemctl restart mi-api.service
echo "API actualizada y servicio reiniciado → $(date)"
Seguridad y buenas prácticas (no te saltes esta parte)
1. Permisos elevados con sudoers (mínimos y concretos)
El usuario git normalmente no puede reiniciar servicios ni tocar ciertas carpetas. La forma correcta es crear un archivo en /etc/sudoers.d/:
# /etc/sudoers.d/git-deploy (editar con visudo -f)
git ALL=(root) NOPASSWD: /bin/systemctl restart mi-api.service
git ALL=(root) NOPASSWD: /bin/systemctl restart nginx
Importante: Nunca pongas NOPASSWD: ALL ni git ALL=(ALL) ALL. Limita al comando exacto que necesitas.
2. Secretos y variables de entorno
No dejes .env dentro del repositorio. Cárgalos desde un archivo seguro:
# Al inicio del hook
source /etc/git-deploy/mi-proyecto.env # 600, owner git
3. Logging (imprescindible para no volverte loco)
Redirige toda la salida a un log dedicado:
exec >> /var/log/git-deploy/mi-proyecto.log 2>&1
echo "=== Inicio deploy $(date '+%Y-%m-%d %H:%M:%S') ==="
# … resto del script …
echo "=== Fin deploy ==="
Crea la carpeta y dale permisos:
sudo mkdir -p /var/log/git-deploy
sudo chown git:git /var/log/git-deploy
sudo chmod 750 /var/log/git-deploy
Problemas comunes y cómo solucionarlos
Entorno Git «fantasma» –> variables raras que rompen todo
Solución: siempre poner al inicio
unset $(git rev-parse --local-env-vars)
Comandos no encontrados (npm, pip, node, etc.)
El hook no carga .bashrc ni .profile. Define PATH explícitamente:
export PATH="/usr/local/bin:/usr/bin:/bin:/home/git/.nvm/versions/node/v20.x.x/bin"
Permisos en el directorio de destino
Asegúrate de que la carpeta destino (/var/www/…, /srv/…) tenga como grupo git o permisos adecuados.
El script que realmente deberías usar (multirama + logging + seguridad)
#!/usr/bin/env bash
set -euo pipefail
# ────────────────────────────────────────────────
# Configuración básica
# ────────────────────────────────────────────────
export PATH="/usr/local/bin:/usr/bin:/bin"
unset $(git rev-parse --local-env-vars) 2>/dev/null || true
REPO="/home/git/repositories/personal/mi-proyecto.git"
WEBROOT="/var/www/mi-proyecto" # o /srv/mi-app, etc.
LOGFILE="/var/log/git-deploy/mi-proyecto.log"
exec >> "$LOGFILE" 2>&1
echo "=== Deploy started $(date '+%Y-%m-%d %H:%M:%S') ==="
# post-receive recibe por stdin: oldrev newrev refname
while read -r oldrev newrev refname; do
# Ignoramos deletes y tags por simplicidad
[[ "$newrev" = "0000000000000000000000000000000000000000" ]] && continue
BRANCH=$(git rev-parse --symbolic --abbrev-ref "$refname" 2>/dev/null || echo "")
if [[ "$BRANCH" == "main" ]]; then
echo "→ Push a main detectado. Desplegando..."
git --work-tree="$WEBROOT" --git-dir="$REPO" checkout -f main
# Aquí puedes poner build, migraciones, npm ci, etc.
# cd "$WEBROOT" && npm ci && npm run build
# cd "$WEBROOT" && pip install -r requirements.txt --quiet
# sudo systemctl restart mi-proyecto.service
echo "→ Despliegue finalizado OK"
else
echo "→ Rama '$BRANCH' ignorada (solo main despliega)"
fi
done
echo "=== Deploy finished $(date '+%Y-%m-%d %H:%M:%S') ==="
Para terminar
Un hook post-receive bien hecho es de las cosas más satisfactorias que puedes implementar en un servidor personal: liviano, predecible, bajo costo mensual y 100% bajo tu control.
Claro, cuando el proyecto crece y hay varios desarrolladores o entornos de staging/producción/review-apps, probablemente termines migrando a GitHub Actions, Woodpecker, Drone o similares. Pero para 80-90% de los proyectos que corren en un VPS barato (blogs, herramientas internas, MVPs, side projects), Git puro + un hook inteligente sigue siendo imbatible en simplicidad y velocidad.
Pruébalo en un proyecto pequeño primero. Una vez que lo tengas funcionando vas a preguntarte por qué no lo hiciste antes.