Mis deploys a producción toman 15 minutos. No los miro. No contengo la respiración. Preparo un té, y para cuando está listo, la nueva versión ya está en vivo.
No siempre fue así.
Hace dos años, los deploys eran un ritual. Bloqueabas una hora. Avisabas al equipo. Corrías el script a mano. Mirabas los logs. Rezabas. Arreglabas lo que se rompió. Lo corrías de nuevo. Rezabas más fuerte. Todo el proceso tomaba 2-3 horas y dejaba a todos destruidos. Deployábamos los viernes porque nos odiábamos.
Si eso te suena familiar, estás haciendo deploys con adrenalina en vez de sistemas. A marzo de 2026, no hay ninguna razón para eso. Así fue como convertí un festival de ansiedad de 3 horas en 15 minutos aburridos. ⚙️
El problema: once pasos en la cabeza de alguien
Deploy — el proceso de enviar código nuevo desde tu computadora al servidor donde los usuarios realmente lo ven — significaba 11 cosas distintas a la vez. Jalar el código. Instalar dependencias (bibliotecas externas que tu aplicación necesita). Correr migraciones (actualizar la estructura de la base de datos para que coincida con el código nuevo). Compilar assets. Reiniciar el servicio. Limpiar el caché. Actualizar el reverse proxy. Verificar el health endpoint.
Todo eso vivía en una página de wiki que estaba 40% desactualizada. Los nuevos del equipo estaban aterrados. Los seniors simplemente estaban cansados.
La solución no fue una herramienta sofisticada. Fue disciplina.
Paso 1: Un script, un comando
Escribí un solo bash script — un pequeño programa que ejecuta comandos en secuencia, como una receta que la computadora sigue línea por línea. Lo llamé deploy.sh. Hace las 11 cosas en orden. Si algún paso falla, todo se detiene y te dice qué paso se rompió y por qué.
#!/bin/bash
set -euo pipefail
APP=$1
DEPLOY_LOG="/var/log/deploys/${APP}-$(date +%Y%m%d-%H%M%S).log"
echo "Deploying ${APP}..." | tee "$DEPLOY_LOG"
# Cada paso: nombre, comando, rollback
deploy_step "Pull code" "git -C /srv/${APP} pull origin main"
deploy_step "Install deps" "cd /srv/${APP} && pip install -r requirements.txt"
deploy_step "Run migrations" "cd /srv/${APP} && python manage.py migrate"
deploy_step "Build assets" "cd /srv/${APP} && npm run build"
deploy_step "Restart service" "systemctl restart ${APP}"
deploy_step "Health check" "curl -sf http://localhost:8080/health"
echo "Deploy complete: ${APP}" | tee -a "$DEPLOY_LOG"
La función deploy_step envuelve cada comando con logging, medición de tiempo y manejo de errores. Si el health check falla, automáticamente reinicia la versión anterior — un rollback, que significa "deshacer y volver a lo que estaba funcionando."
Tiempo de configuración total: 45 minutos. Esos 45 minutos han ahorrado cientos de horas desde entonces. La idea clave viene directo de la metodología Twelve-Factor App — un codebase, un build, un comando de deploy.
Paso 2: CI/CD — el robot que deploya por ti
CI/CD significa Continuous Integration / Continuous Deployment. En español llano: un sistema que automáticamente prueba tu código cuando subes cambios (esa es la parte de CI), y si las pruebas pasan, automáticamente lo deploya al servidor (esa es la parte de CD). Tú subes código, el robot se encarga del resto.
Para un equipo pequeño, no necesitas Jenkins (un servidor de automatización pesado), ArgoCD (una herramienta de deploy para Kubernetes), ni un clúster de Kubernetes (un sistema de orquestación de contenedores — si esas palabras no te dicen nada, perfecto, no lo necesitas). Necesitas GitHub Actions y un servidor con acceso SSH. GitHub Actions se lanzó en 2019 y ha madurado hasta convertirse en la opción por defecto de CI/CD para equipos que ya usan GitHub — estable, bien documentado y gratuito para cargas pequeñas.
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt
- run: pytest --tb=short
deploy:
needs: test
runs-on: ubuntu-latest
if: success()
steps:
- name: Deploy to production
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: deploy
key: ${{ secrets.SSH_KEY }}
script: bash /srv/myapp/deploy.sh myapp
Ese es todo el pipeline — una secuencia de pasos automatizados por los que tu código viaja desde tu laptop hasta producción. Push a main (la rama principal de tu código). Las pruebas corren en los servidores de GitHub. Si pasan, GitHub se conecta a tu servidor vía SSH (un protocolo de conexión remota segura) y ejecuta deploy.sh.
Sin contenedores. Sin orquestadores. Sin archivos YAML de 200 líneas. El tier gratuito de GitHub Actions te da 2,000 minutos al mes — más que suficiente para un equipo pequeño deployando 2-3 veces al día.
Paso 3: Un health check que realmente revisa la salud
La mayoría de los health checks son una URL /health que devuelve {"status": "ok"}. Eso te dice que el servidor web está corriendo. No te dice nada sobre si la app realmente funciona. Es como preguntarle a alguien "¿estás vivo?" cuando deberías preguntar "¿puedes ver, oír y mover los dedos?"
Mis health checks verifican tres cosas:
@app.get("/health")
async def health():
checks = {
"database": await check_db_connection(),
"cache": await check_redis_ping(),
"disk": check_disk_space_above(20), # percent
}
all_ok = all(checks.values())
return JSONResponse(
status_code=200 if all_ok else 503,
content={"status": "healthy" if all_ok else "degraded", "checks": checks}
)
¿Base de datos caída? El health check falla. ¿Redis (un caché en memoria — una capa de almacenamiento temporal rápido) inalcanzable? Falla. ¿Disco al 95%? Falla. El script de deploy llama a este endpoint después del reinicio. Si devuelve 503 (el código HTTP que significa "servicio no disponible"), el deploy hace rollback automáticamente.
Este solo endpoint ha atrapado más problemas post-deploy que todas nuestras pruebas manuales combinadas. ⚙️
Paso 4: Notificaciones, no dashboards
No me siento a mirar dashboards de monitoreo. El script de deploy envía exactamente dos mensajes:
- Deploy iniciado — "Deployando myapp a las 14:32. Disparado por commit abc123."
- Deploy terminado — "myapp deployado en 14m 22s" o "Deploy de myapp FALLÓ en paso: health check. Rollback ejecutado."
Estos van a un canal de Telegram. Si veo una notificación de falla, investigo. Si veo un éxito, le doy un sorbo a mi té.
Sin Grafana (una herramienta popular de dashboards). Sin PagerDuty (un servicio de alertas que te despierta a las 3 AM). Para un equipo pequeño corriendo 2-3 servicios, un mensaje de Telegram es el nivel correcto de complejidad. La complejidad debe coincidir con el tamaño del problema, no con el tamaño de tu ambición. 🫶
Paso 5: Deploya el lunes por la mañana
Dejamos de deployar los viernes. Ahora deployamos los lunes y miércoles por la mañana.
¿Por qué? Porque si algo se rompe el lunes, tienes cinco días para arreglarlo con el cerebro fresco. Los deploys del viernes significan debugging el fin de semana. Debugging el fin de semana significa burnout. Burnout significa errores. Errores significan más debugging el fin de semana.
Eso es un ciclo de retroalimentación negativa — un ciclo donde cada resultado malo causa el siguiente. No es una estrategia de deploy.
El calendario aburrido de deploys: lunes y miércoles por la mañana. Nada después de las 3 PM. Nada los viernes. Nada durante la comida. Si no está listo para la ventana del lunes, espera. Esperar es barato. El burnout es caro.
Lo que estás sacrificando
Momento de honestidad. Esta configuración tiene tradeoffs.
Sin deploys con cero downtime. Cuando systemctl restart corre, hay una brecha de 2-5 segundos donde la app no está disponible. Para un servicio manejando millones de requests, eso importa. Para la mayoría de los equipos pequeños, nadie lo nota. Si esto se te queda corto, investiga los blue-green deployments (correr dos copias de tu app y cambiar el tráfico entre ellas) — pero no empieces por ahí.
Un servidor, un punto único de falla. Si el servidor muere, todo muere. La redundancia cuesta dinero. Para la mayoría de los proyectos con menos de $10K de ingreso mensual, un solo servidor bien mantenido con backups diarios es la respuesta correcta. Los problemas de escala son buenos problemas.
Sin contenedores. Docker (una herramienta que empaqueta tu app con todas sus dependencias en una unidad aislada) es maravilloso para entornos complejos. Para una app de Python con una base de datos y Redis en un servidor, es overhead. Siempre puedes agregarlo después.
La investigación de métricas DORA de Google muestra que los equipos élite deployean bajo demanda con tiempos de entrega menores a una hora. Pero también muestra que llegas ahí incrementalmente — no adoptando todas las herramientas a la vez.
El resultado
Antes: Deploys de 3 horas, 1-2 veces por semana, equipo de 3 personas bloqueado. Después: Deploys de 15 minutos, 2-3 veces por semana, cero personas mirando.
Tiempo mensual ahorrado: aproximadamente 24 horas de tiempo colectivo del equipo. Costo anual del setup de CI/CD: $0 (tier gratuito de GitHub Actions más un bash script). Tiempo de configuración: un fin de semana.
Hazlo aburrido
La meta de una buena operación no es hacer los deploys emocionantes. Es hacerlos tan aburridos que te olvides de que pasan.
Cuando deployeas sin adrenalina, deployeas más seguido. Cuando deployeas más seguido, cada deploy es más pequeño. Cuando cada deploy es más pequeño, menos cosas se rompen. Cuando menos cosas se rompen, duermes mejor.
Esa es toda la filosofía. Un script. Un pipeline. Un health check. Un canal de notificaciones. Una mañana tranquila por semana.
Hazlo aburrido. Hazlo pequeño. Hazlo automático. Ve a preparar un té. 🫶





