La Raya · backend (5): ponerlo en producción duele — Docker, Dokploy, Celery y el día que nada arrancaba

Publicado el 17/06/2026

Hay una mentira que nos contamos: "el código ya está, solo falta desplegarlo". Este post es el antídoto. Llevar La Raya a producción (Docker + Dokploy sobre un VPS, con Postgres y Redis) fue una sucesión de pequeños incendios, y casi ninguno tenía que ver con la lógica de la app.

El contenedor que nadie encontraba

Primer acto: el sitio no respondía. Causas encadenadas, cada una con su commit:

  • gunicorn escuchaba en 127.0.0.1 en vez de 0.0.0.0, así que dentro del contenedor funcionaba y desde fuera no llegaba nadie.
  • Traefik (el proxy de Dokploy) no enrutaba al backend porque no estaba en la red correcta. Añadí el servicio a dokploy-network.
  • Intenté poner labels de Traefik a mano… y resultó que chocaban con la config que genera Dokploy. Los acabé quitando. Lección: cuando la plataforma genera la configuración del proxy, no te pelees con ella metiendo labels manuales.
  • El healthcheck fallaba, así que añadí un endpoint /health/ minúsculo que devuelve {"status":"ok"}. Ese endpoint de juguete acabó siendo clave (la app lo usa para mostrar el estado del servicio, pero esa es otra historia).
  • Y los estáticos no se servían en producción: WhiteNoise al rescate.

El bug de Celery que me costó tres commits

Mi favorito por lo absurdo. Los workers de Celery petaban al arrancar por un bug conocido de Celery 5.3.1 con worker_state_db. Fui probando, commit a commit:

  1. CELERY_WORKER_STATE_DB… no.
  2. parchear worker_state_db directamente sobre app.conf… tampoco del todo.
  3. CELERY_STATEDB para esquivarlo… por fin.

Tres intentos públicos en el historial. No lo escondo: así se ve realmente depurar un bug de una dependencia que no controlas. La moraleja no es "qué torpe", es fija las versiones y desconfía de los .1 de cualquier librería.

Las variables, la red y los permisos

Más fuego de fontanería, todo real:

  • Había que pasar explícitamente todas las variables de entorno a cada contenedor (web, worker, beat). Lo que el web tenía, el worker no, y fallaba en silencio.
  • celery-worker y celery-beat también necesitaban estar en dokploy-network.
  • LibreTranslate (el traductor autoalojado del capítulo anterior) no arrancaba por permisos de volumen → tuvo que correr como root; además, no debía bloquear el arranque del backend si tardaba, así que dejé de esperar a su healthcheck y su URL pasó a ser inyectable por entorno.
  • Playwright (para los scrapers HTML) no traía los navegadores en la imagen Docker y necesitaba flags específicos para correr en contenedor.

Los ficheros que desaparecían

Las imágenes scrapeadas se subían… y se esfumaban al reiniciar. Dos cosas: en Django 6 static() es un no-op con DEBUG=False, así que registré el serve de media directamente; y, sobre todo, persistí los media en un volumen Docker y lo monté también en el worker (que es quien descarga las imágenes). Obvio una vez lo ves; invisible hasta que un usuario te dice "las fotos no salen".

Y de paso, rendimiento

Ya puestos en producción llegaron las optimizaciones que solo duelen con datos reales: cacheops (caché de querysets en Redis), índices de BD para los filtros y ordenaciones habituales, y un fix para colisiones de slug entre workers concurrentes (dos scrapers creando la misma página a la vez). También configuré el email por SMTP (DonDominio) vía variables de entorno, base de las alertas y, mucho después, del formulario de contacto.

Lo que me llevo (y repito como un mantra)

  • "Funciona en local" no significa nada. Producción es otra red, otros permisos, otro DEBUG, otros volúmenes. Despliega pronto y a menudo para que los incendios sean pequeños.
  • No pelees con tu plataforma. Si Dokploy/Traefik generan la config, deja que la generen.
  • Fija versiones. Un bug de un 5.3.1 te come una tarde; poder reproducir es lo que te salva.
  • Healthchecks y persistencia no son opcionales. Un /health/ tonto y un volumen bien montado evitan media app caída y fotos fantasma.

Con el backend por fin en pie y estable, pude volver a lo bonito: afinar la guía. Búsqueda, ordenar por cercanía de verdad… y un susto con Google que cuento en la próxima entrada.