Referencia rápida de comandos para situaciones frecuentes en producción. Para procedimientos completos (gestión de secretos, GDPR, rotación de credenciales), ver el runbook completo.
Todos los comandos asumen que estás en el servidor, en el directorio del proyecto.
| Nivel | Descripción | Tiempo de respuesta |
|---|---|---|
| P0 | Servicio completamente inaccesible o pérdida de datos en curso | Inmediato |
| P1 | Funcionalidad crítica degradada (login, invitaciones, emails) | < 30 min |
| P2 | Funcionalidad no crítica degradada (embeddings, discovery) | < 4 horas |
Síntomas: {"status":"error","db":"error"} en /api/health/, errores 500 generalizados.
# 1. Verificar estado del contenedor
dc ps db
# 2. Ver logs
dc logs db --tail=50
# 3. Si el contenedor está detenido, reiniciar
dc start db
# 4. Si falla al iniciar (datos corruptos), restaurar desde backup
# Ver sección "Restore desde backup" más abajo
# 5. Verificar conectividad desde backend
dc exec backend python manage.py dbshell -- --command="SELECT 1;"
# 6. Confirmar recovery
curl -s http://127.0.0.1:8000/api/health/ | python3 -m json.toolEscalado: Si el volumen postgres_data está corrupto → restaurar último backup.
Síntomas: Workers Celery desconectados, sessions JWT sin poder refrescarse, errores en throttling.
# 1. Verificar estado
dc ps redis
dc logs redis --tail=30
# 2. Reiniciar (Redis es stateless en este stack; los datos en memoria se pierden)
dc restart redis
# 3. Verificar que responde
dc exec redis redis-cli ping # debe responder PONG
# 4. Reiniciar workers para reconectar
dc restart celery celery-beat
# 5. Verificar health
curl -s http://127.0.0.1:8000/api/health/ | python3 -m json.toolImpacto esperado: Los rate limits se resetean (son en-memoria). Los JWT access tokens siguen siendo válidos hasta que expiran; el refresh necesita Redis para funcionar.
Síntomas: Tareas en estado PENDING por más de 10 minutos, embeddings no se generan, emails no se envían.
# 1. Verificar estado del worker
dc exec celery celery -A exogram inspect ping
dc exec celery celery -A exogram inspect active
dc exec celery celery -A exogram inspect stats
# 2. Ver cola pendiente
dc exec celery celery -A exogram inspect reserved
# 3. Verificar heartbeat del beat (si es 0 o muy viejo, el beat no funciona)
dc exec redis redis-cli get celerybeat:heartbeat
# 4. Reiniciar worker (las tareas pendientes vuelven a la cola)
dc restart celery
# 5. Si la cola tiene tareas corruptas que bloquean el worker, purgar
dc exec celery celery -A exogram purge
# ⚠️ Purgar elimina todas las tareas pendientes. Evaluar impacto antes de ejecutar.Síntomas: Invitaciones y resets de contraseña no llegan, logs muestran errores SMTP.
# 1. Ver logs del backend para errores SMTP
dc logs backend --tail=100 | grep -i "smtp\|email\|postal\|SMTPException"
# 2. Verificar que Postal está levantado (si usás el profile mail)
dc --profile mail ps postal
dc --profile mail logs postal --tail=30
# 3. Test manual desde Django
dc exec backend python manage.py shell -c "
from django.core.mail import send_mail
send_mail('Test P1', 'Diagnóstico', 'no-reply@exogram.app', ['tu@email.com'])
print('Enviado')
"
# 4. Si Postal está caído, reiniciar
dc --profile mail restart postalEjecutar inmediatamente después de cada deploy:
# 1. Todos los contenedores en estado Up
dc ps
# 2. Health check del backend
curl -s http://127.0.0.1:8000/api/health/ | python3 -m json.tool
# Esperado: {"status":"ok","db":"ok","redis":"ok"}
# 3. Sin errores recientes en logs
dc logs backend --tail=20 | grep -E "ERROR|CRITICAL" || echo "Sin errores"
# 4. Worker Celery responde
dc exec celery celery -A exogram inspect ping
# 5. Migraciones aplicadas
dc exec backend python manage.py showmigrations | grep '\[ \]' || echo "Sin migraciones pendientes"| Métrica | Fuente | Umbral de alerta |
|---|---|---|
| HTTP 5xx rate | dc logs backend |
> 1% de requests en 5 min |
| Latencia p95 backend | dc logs backend |
> 2 s sostenido |
| Cola Celery pendiente | Flower / inspect reserved |
> 100 tareas por > 5 min |
| Heartbeat Celery Beat | Redis celerybeat:heartbeat |
Más de 120 s sin actualizar |
| Espacio en disco | df -h /var/lib/docker |
> 80% |
| Memoria contenedores | docker stats --no-stream |
backend > 1.8 GB, celery > 2.5 GB |
ssh usuario@servidor
cd /srv/exogramEl alias del compose en producción es:
alias dc='docker compose -f docker-compose.yml -f docker-compose.prod.yml'# Backend (Django/Gunicorn)
dc logs backend -f
# Worker Celery
dc logs celery -f
# Beat (tareas periódicas)
dc logs celery-beat -f
# Todos a la vez
dc logs -f
# Últimas 100 líneas de un servicio
dc logs backend --tail=100# Restart de un servicio sin reconstruir imagen
dc restart backend
dc restart celery
dc restart celery-beat
# Restart de todos
dc restart
# Restart forzado (detiene y vuelve a levantar)
dc stop backend && dc up -d backend# Health check (espera {"status":"ok","db":"ok","redis":"ok"})
curl -s http://127.0.0.1:8000/api/health/ | python3 -m json.tool
# Estado de contenedores
dc ps
# Uso de recursos
docker stats --no-stream
# Espacio en disco (volúmenes Docker)
docker system df# Desde Flower (UI web, solo accesible en localhost)
# ⚠️ Flower no tiene autenticación. Está bound a 127.0.0.1:5555 en producción,
# por lo que no es accesible desde internet. Acceder solo vía túnel SSH:
# Abrir un tunel SSH: ssh -L 5555:127.0.0.1:5555 usuario@servidor
# Luego abrir: http://localhost:5555
# Por línea de comandos
dc exec celery celery -A exogram inspect active
dc exec celery celery -A exogram inspect statsEl beat escribe un timestamp en Redis cada 60 segundos como señal de vida.
dc exec redis redis-cli get celerybeat:heartbeat
# Devuelve el timestamp Unix de la última ejecución
# Si está vacío o muy desactualizado, el beat no está funcionandodc exec backend python manage.py shell -c "
from books.tasks import batch_generate_embeddings
batch_generate_embeddings.delay([1, 2, 3])
print('Tarea enviada')
"dc exec celery celery -A exogram purgedc exec db psql -U exogram -d exogramdc exec db pg_dump -U exogram exogram | gzip > backup_$(date +%Y%m%d_%H%M%S).sql.gzgunzip < backup_20260318_120000.sql.gz | dc exec -T db psql -U exogram -d exogramLas migraciones corren automáticamente en cada deploy vía start-web.sh.
Para correrlas a mano:
dc exec backend python manage.py migrate --noinputdc exec backend python manage.py showmigrations | grep '\[ \]'dc exec backend python manage.py createsuperuserdc exec backend python manage.py changepassword <username>dc exec backend python manage.py shellLos refresh tokens rotados se almacenan en la blacklist de simplejwt en PostgreSQL. Con el tiempo, la tabla puede crecer. Django simplejwt incluye un management command para limpiar tokens expirados:
dc exec backend python manage.py flushexpiredtokensSe recomienda schedulear esto periódicamente (se puede agregar a CELERY_BEAT_SCHEDULE).
Rotar la SECRET_KEY invalida todas las sesiones activas, cookies de CSRF y
tokens de invitación/reset pendientes. Los usuarios deberán loguearse nuevamente.
- Generar nueva clave:
python3 -c "import secrets; print(secrets.token_urlsafe(50))" - Actualizar el secret
SECRET_KEYen GitHub - Actualizar
backend/.enven el servidor dc restart backend celery celery-beat
Caddy obtiene y renueva certificados Let's Encrypt automáticamente.
Los datos de certificados se persisten en el volumen caddy_data.
Para ver el estado del certificado:
dc exec caddy caddy version
# Los logs de Caddy muestran renovaciones automáticas
dc logs caddy | grep -i "certificate\|tls\|acme"Si el certificado no renueva (edge case), reiniciar Caddy es suficiente en la
mayoría de casos: dc restart caddy.
# Imágenes Docker sin usar
docker image prune -f
# Contenedores detenidos, redes y cache de build sin usar
docker system prune -f
# Con volúmenes (CUIDADO: elimina datos si no están en uso activo)
docker system prune --volumes -fdc logs backend --tail=100 | grep ERROR
dc exec backend python manage.py check# Verificar conexión con Redis
dc exec celery celery -A exogram inspect ping
# Verificar que Redis esté arriba
dc exec redis redis-cli ping # debe responder PONG
# Reiniciar worker
dc restart celery# Ver logs del backend para errores SMTP
dc logs backend | grep -i "email\|smtp\|postal"
# Verificar que Postal está levantado (si usás el profile mail)
dc --profile mail ps postal
# Test manual desde Django
dc exec backend python manage.py shell -c "
from django.core.mail import send_mail
send_mail('Test', 'Cuerpo', 'from@exogram.app', ['to@example.com'])
print('Enviado')
"