Plane
Project management tool. URL: plane.gunei.xyz — v1.2.2.
Admin panel: plane.gunei.xyz/god-mode
Las intake forms nativas de Plane requieren plan pago. Usamos Tally.so (free) + webhook como workaround.
Deployment
Docker Compose en /opt/apps/plane/. Red: gunei-network.
Servicios:
| Servicio | Descripción |
|---|---|
web | Frontend principal |
space | Spaces (portal público) |
admin | God mode / admin panel |
live | Colaboración en tiempo real |
api | API REST |
worker | Worker de tareas async |
beat-worker | Scheduler (Celery beat) |
migrator | Migraciones de DB |
plane-db | PostgreSQL |
plane-redis | Redis |
plane-mq | RabbitMQ |
plane-minio | MinIO (storage de archivos) |
cd /opt/apps/plane
# Levantar
docker compose up -d
# Bajar
docker compose down
# Ver logs
docker compose logs -f api
docker compose logs -f worker
Configuración
Variables clave en plane.env:
# Dominio
WEB_URL=https://plane.gunei.xyz
CORS_ALLOWED_ORIGINS=https://plane.gunei.xyz
# Base de datos
PGHOST=plane-db
PGDATABASE=plane
PGUSER=plane
PGPASSWORD=<secret>
# Redis
REDIS_URL=redis://plane-redis:6379/
# RabbitMQ
RABBITMQ_HOST=plane-mq
RABBITMQ_DEFAULT_USER=plane
RABBITMQ_DEFAULT_PASS=<secret>
RABBITMQ_DEFAULT_VHOST=plane
# MinIO
AWS_S3_ENDPOINT_URL=http://plane-minio:9000
AWS_S3_BUCKET_NAME=uploads
AWS_ACCESS_KEY_ID=<secret>
AWS_SECRET_ACCESS_KEY=<secret>
# Email (via Postal)
EMAIL_HOST=postal.gunei.xyz
EMAIL_HOST_USER=<user>
EMAIL_HOST_PASSWORD=<secret>
EMAIL_FROM=noreply@gunei.xyz
Proxy Caddy
Bloque en el Caddyfile para plane.gunei.xyz:
plane.gunei.xyz {
@spaces path /spaces*
handle @spaces {
reverse_proxy space:3000
}
@god-mode path /god-mode*
handle @god-mode {
reverse_proxy admin:3000
}
@live path /live*
handle @live {
reverse_proxy live:8090
}
@api path /api*
handle @api {
reverse_proxy api:8000
}
@auth path /auth*
handle @auth {
reverse_proxy api:8000
}
@uploads path /uploads*
handle @uploads {
reverse_proxy api:8000
}
handle {
reverse_proxy web:3000
}
}
Networking gotcha: Caddy corre en Docker (
caddy-sharedcontainer). Para proxear a servicios corriendo en el host (no en Docker), usarhost.docker.internalen vez delocalhost. Requiereextra_hosts: ["host.docker.internal:host-gateway"]en el Docker Compose de Caddy.
Backups
PostgreSQL backup diario via cron — 3am, retención 7 días.
# Ubicación de backups
ls /opt/apps/plane/backups/
# Verificar cron
crontab -l | grep plane
# Restore manual (ejemplo)
cat /opt/apps/plane/backups/plane_backup_YYYY-MM-DD.sql | docker exec -i plane-db psql -U plane -d plane
Integración Tally → Plane
Webhook server que recibe submissions de Tally y crea intake issues en Plane.
- Endpoint:
https://hooks.gunei.xyz/tally - Código:
/root/webhook/server.py - Proxy: Caddy en
caddy-shared→webhook:8091
Cómo funciona
- Tally envía POST al webhook con la submission
- Server verifica firma HMAC-SHA256 con
TALLY_SIGNING_SECRET - Parsea campos del formulario (título, descripción, adjuntos)
- Crea intake issue via API de Plane (
/intake-issues/con payload{"issue": {...}}) - Sube adjuntos al issue (flujo de 3 pasos: presigned creds → POST multipart a Minio → PATCH mark complete)
Tally gotcha: Los file upload fields devuelven arrays de objetos con
id,name,url,mimeType,size. Las URLs requieren headerUser-Agentpara descargar.
Variables de entorno
PLANE_API_KEY=<token de Plane>
PLANE_BASE_URL=https://plane.gunei.xyz
WORKSPACE_SLUG=gunei
PROJECT_ID=<uuid del proyecto>
TALLY_SIGNING_SECRET=<secret configurado en Tally>
PORT=8091
Correr el servidor
El servidor corre como contenedor Docker en gunei-network:
# Ver estado
docker ps | grep webhook
# Restart
cd /root/webhook && docker compose restart
# Logs
docker logs -f webhook
cd /root/webhook
docker compose up -d
Bugs conocidos
- Minio
EntityTooLarge: El POST multipart a Minio falla porque el body excede elContent-Lengthdeclarado del archivo. Fix propuesto: pedirsize * 2al solicitar las presigned credentials. Pendiente de implementar.
Agregar un nuevo formulario de intake
El server actual está hardcodeado a un solo proyecto y un solo formulario de Tally. Para agregar otro form (ej: otro proyecto de Plane, otro tipo de reporte), hay que tocar 3 cosas: Tally, el server, y Plane.
1. Crear el formulario en Tally
Ir a tally.so y crear un nuevo form. Tener en cuenta:
- Los campos se mapean por label (el texto que el usuario ve). El server usa
get_field(fields, "Label exacto")para extraer valores, así que los labels importan. - Si necesitás adjuntos, agregar un campo de tipo File Upload.
- Una vez publicado, ir a Settings → Webhooks y configurar:
- URL:
https://hooks.gunei.xyz/<tu-nuevo-path>(ej:/tally-soporte) - Signing secret: generar uno nuevo con
openssl rand -hex 32y guardarlo.
- URL:
2. Modificar el server
Editar /root/webhook/server.py:
a) Agregar constantes para el nuevo proyecto (al inicio del archivo):
# Proyecto Soporte (ejemplo)
PROJECT_ID_SOPORTE = "uuid-del-proyecto-en-plane"
TALLY_SIGNING_SECRET_SOPORTE = "nuevo-secret-generado"
b) Crear un handler para el nuevo form:
Copiar handle_tally() y adaptarlo. Lo que cambia:
- El
PROJECT_IDusado en las llamadas aplane_request - Los labels de
get_field()— deben coincidir exactamente con los del nuevo form de Tally - La lógica de armado del
tituloydescription_html
def handle_soporte(payload):
fields = payload["data"]["fields"]
# Adaptar estos labels a los del nuevo form
asunto = get_field(fields, "Asunto")
detalle = get_field(fields, "Detalle")
files = get_files(fields)
titulo = asunto[:80] + ("..." if len(asunto) > 80 else "")
description_html = f"<p>{detalle}</p>"
result = plane_request("POST",
f"/workspaces/{WORKSPACE_SLUG}/projects/{PROJECT_ID_SOPORTE}/intake-issues/",
{"issue": {"name": titulo, "description_html": description_html, "priority": "none"}}
)
issue_id = result["issue"]
print(f"Issue soporte creado: {issue_id}")
for f in files:
try:
upload_attachment(issue_id, f, PROJECT_ID_SOPORTE)
except Exception as e:
print(f" Error subiendo {f['name']}: {e}")
Ojo con
upload_attachment(): esa función usa elPROJECT_IDglobal hardcodeado. Si el nuevo form es para otro proyecto, vas a tener que pasarle elproject_idcomo parámetro en vez de usar la constante global.
c) Agregar la nueva ruta en el Handler:
def do_POST(self):
if self.path == "/tally":
secret = TALLY_SIGNING_SECRET
handler = handle_tally
elif self.path == "/tally-soporte":
secret = TALLY_SIGNING_SECRET_SOPORTE
handler = handle_soporte
else:
self.send_response(404)
self.end_headers()
return
# verificar firma con el secret correspondiente
# ... (misma lógica de HMAC, usando `secret`)
handler(payload)
d) Reiniciar el server:
cd /root/webhook && docker compose restart
3. Habilitar intake en el proyecto de Plane
El proyecto destino tiene que tener intake habilitado, si no la API devuelve 403.
- Ir a
plane.gunei.xyz→ Proyecto → Settings → Features - Activar Intake (puede aparecer como "Triage" en algunas versiones)
4. Obtener el PROJECT_ID
Ir al proyecto en Plane → Settings → General. El UUID aparece en la URL del browser:
https://plane.gunei.xyz/gunei/projects/UUID-ACA/settings/
Checklist rápido
- [ ] Form creado en Tally con los campos necesarios
- [ ] Webhook configurado en Tally apuntando a
hooks.gunei.xyz/<path> - [ ] Signing secret generado y configurado en ambos lados
- [ ] Handler creado en
server.pycon los labels correctos - [ ] Ruta agregada en
do_POST - [ ]
upload_attachmentrecibeproject_idsi es otro proyecto - [ ] Intake habilitado en el proyecto de Plane
- [ ] Server reiniciado
- [ ] Probar con un submit de prueba y verificar con
docker logs -f webhook
Operaciones comunes
# Restart completo
docker compose restart
# Restart servicio específico
docker compose restart api worker
# Update a nueva versión
# 1. Editar imagen en docker-compose.yml
# 2. Pull
docker compose pull
# 3. Recrear con migración
docker compose up -d migrator
docker compose logs -f migrator # esperar que termine
docker compose up -d
Troubleshooting
# Verificar que todos los containers estén healthy
docker compose ps
# Minio: verificar acceso
docker exec plane-minio mc ls local/uploads/
# Worker: verificar que procesa tareas
docker compose logs --tail=50 worker
# API: health check
curl -s https://plane.gunei.xyz/api/health/ | jq
# Redis: verificar conexión
docker exec plane-redis redis-cli ping
# RabbitMQ: verificar colas
docker exec plane-mq rabbitmqctl list_queues