Despliegue de Lightdash en ctunlinux — cierre técnico
Fecha: 2026-08-09
Host: ctunlinux (2 vCPU / 5.2GB RAM+4GB swap / 48GB disco)
Resultado: Lightdash 1.107.0 en producción, https://dash.frento.com.mx,
proyecto 82df3b0a-4b56-4dee-837b-20706bae801b, 3 dashboards / 17 gráficas.
Por qué el pull de la imagen fue el cuello de botella real
lightdash/lightdash:latest pesa 14.1GB (docker images — SIZE 14.1GB,
UNIQUE SIZE 14.06GB). docker history desglosado:
| Capa | Tamaño |
|---|---|
COPY /usr/app /usr/app (código de la app) | 2.79GB |
dbt1.12 | 1.12GB |
dbt1.11 | 966MB |
dbt1.10 | 963MB |
dbt1.9 | 958MB |
dbt1.5 | 731MB |
dbt1.8 | 701MB |
dbt1.7 | 688MB |
dbt1.4 | 687MB |
dbt1.6 | 683MB |
apt-get (python3, git, build-essential, fontconfig, etc.) | 494MB |
| Node.js runtime | 155MB |
| resto (duckdb extensions, pnpm, debian base) | ~170MB |
El Dockerfile oficial
instala las 9 versiones de dbt en venvs separados
(/usr/local/dbt1.X) porque cuando un proyecto se conecta vía integración
GitHub/GitLab, el propio servidor de Lightdash ejecuta dbt compile — y cada
proyecto de cliente puede pinear una versión de dbt distinta. No hay build-arg
ni stage para excluir versiones; la única forma de recortarlo es modificar el
Dockerfile y compilar la imagen desde el código fuente (repo ~490MB sin
node_modules, que en un monorepo pnpm de este tamaño fácilmente suma varios
GB más) — descartado por el mismo motivo que el resto de este documento: este
host no tiene margen de RAM/disco para un build de ese peso, y ya costó tres
intentos solo extraer la imagen prearmada.
Como el proyecto usa el flujo CLI (lightdash deploy desde este mismo host,
subiendo el manifest ya compilado — ver api-referencia.md), ninguna de las 9
versiones de dbt embebidas se usa nunca en este deployment. Es peso muerto en
disco, cero impacto en RAM/CPU en operación normal.
Intento 1 — falla real de containerd/overlayfs
docker compose up -d (vía el docker-compose.yml inicial, con MinIO +
Postgres + Lightdash). Tras ~35 minutos de descarga/extracción muy lenta
(confirmado con ps aux que no estaba colgado: unpigz -d -c con CPU activo,
solo lento por la RAM limitada del host generando swapping pesado), la
extracción de la capa f6889744f9f2 (una de las de node_modules) falló:
failed to extract layer (application/vnd.oci.image.layer.v1.tar+gzip
sha256:f6889744f9f2...) to overlayfs as "extract-617546470-LLXG
sha256:357ae134...": failed call to UtimesNanoAt for
/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/323/fs/usr/app/node_modules/.pnpm/@75lb+deep-merge@1.1.2:
no such file or directory
Error conocido de containerd/overlayfs al extraer árboles node_modules de
pnpm (muchísimos archivos pequeños con hardlinks) bajo presión de
memoria/inodos. docker compose down + docker rmi no dejó residuos (la capa
parcial se descarta automáticamente al fallar), pero sí dejó el disco en
~6.9GB libres desde los ~18GB iniciales — la mayoría se recuperó al abortar,
no se perdió permanentemente.
Intento 2 — el tag :latest cambió a mitad de la descarga
Segundo docker pull lightdash/lightdash:latest mostró un manifest distinto
al del intento 1 (varios hashes de capa nuevos: bd9ddc54bea9, 04dfc350cea9,
7c7752f95ec8, 4b874653a391, junto con algunos reutilizados del intento
anterior). Docker Hub publicó una build nueva de latest mientras el intento 1
todavía estaba en curso, invalidando buena parte del caché ya descargado.
Abortado manualmente (kill al proceso docker pull) al ver que el disco
volvía a bajar de forma sostenida sin garantía de terminar con el mismo
manifest que ya se tenía parcialmente cacheado.
Intento 3 — éxito, después de liberar RAM
Antes de reintentar, se liberó RAM del host decomisionando servicios sin uso (no relacionado técnicamente con el pull, pero coincide con el diagnóstico de que la lentitud/fragilidad venía de swapping pesado en una VM de 5.2GB):
docker compose downen~/authentik/— 3 contenedores (authentik-server-1,authentik-worker-1,authentik-postgresql-1), sin evidencia de que protegiera nada activamente.docker rm minio+docker rmi quay.io/minio/minio:latest— contenedor standalone ya exited hacía 4 semanas, bind mount a/usr/local/share/minio, sin relación con ellightdash-miniode este despliegue (nombre distinto, imagen distinta —minio/miniovsquay.io/minio/minio, mismo contenido pero repos Docker Hub separados).docker stop dvt-checks soda-perses soda-loki— pausados temporalmente, reactivados al terminar el despliegue (docker start, ver nota abajo).
RAM libre pasó de 221MB a 731MB (swap de 875MB a 284MB) solo con Authentik +
minio fuera. El tercer docker pull completó tras otro tramo largo (~15-20 min
adicionales, con el disco tocando mínimos de ~2.6GB libres en el peor
momento — nunca cruzó el umbral de aborto de seguridad de 2.5GB fijado a mano
durante el proceso). docker compose up -d levantó los 4 contenedores
(lightdash, lightdash-db, lightdash-minio, lightdash-minio-init) sin
incidentes.
Nota operativa post-mortem: dvt-checks volvió a estar Up por su cuenta
horas después (política restart: unless-stopped + algo lo reinició, no
investigado a fondo). soda-perses/soda-loki, en cambio, quedaron
detenidos varias horas hasta que se les hizo docker start manual al
redactar esta documentación — data.frento.com.mx estuvo caído (502) todo ese
tiempo sin que nadie lo notara. Lección: pausar contenedores para liberar
recursos temporalmente necesita un recordatorio explícito de reactivarlos, no
asumir que se reactivan solos.
Limpieza de disco adicional
Durante el incidente se aprovechó para borrar imágenes huérfanas/duplicadas sin contenedor activo, liberando ~2.1GB adicionales:
ghcr.io/goauthentik/server:2026.5.4(1.84GB) ypostgres:16-alpine(420MB) — huérfanas tras decomisionar Authentik.apache/superset:latest(1.28GB nominal, casi todo compartido consuperset-superset:latest— poco espacio único real) — duplicado sin contenedor.- Contenedor
nifty_robinson(nombre autogenerado, comandoapt-get..., exited hacía 4 semanas) + su imagen<none>asociada (6635f3d24af8, 1.28GB) — experimento abandonado, sin relación con ningún servicio documentado.
docker system df después de la limpieza: Images 15 total / 11 activas /
8.9GB, 696.8MB reclaimable en capas dangling.
Migración temporal de SECURE_COOKIES para setup por API
El registro del primer usuario/organización, la creación del proyecto, el
token de acceso personal y los 3 dashboards se hicieron por curl contra la
API REST (ver api-referencia.md), no desde la UI. Con SECURE_COOKIES=true
(el valor correcto en producción, ya detrás de HTTPS), el Set-Cookie de
login lleva el atributo Secure — curl no la persiste ni la reenvía sobre
http://localhost:8090 en plano, así que cualquier llamada autenticada
posterior fallaba con 401 Unauthorized en silencio (el login en sí devolvía
200 OK, la sesión simplemente no quedaba guardada).
Se bajó SECURE_COOKIES=false en el docker-compose.yml, se hizo
docker compose up -d lightdash (recrea el contenedor), se completó todo el
setup por API, y se volvió a subir SECURE_COOKIES=true con otro
docker compose up -d lightdash antes de exponer el hostname en el túnel de
Cloudflare — evitar una ventana donde cualquiera que adivinara la URL pública
pudiera registrar la primera organización (el endpoint de registro está
abierto hasta que existe al menos un usuario).
Esa segunda recreación del contenedor es la que causó el bug de red
documentado en
Lightdash § Red
— la conexión a postgres-warehouse_default se había hecho a mano
(docker network connect) después del primer docker compose up -d, sin
declararla en el compose; la segunda recreación la perdió sin aviso hasta que
el dashboard en producción tiró getaddrinfo EAI_AGAIN postgres-dw. Fix
aplicado: declarar ambas redes en el docker-compose.yml (ver el archivo
completo más abajo), que sobrevive cualquier recreación futura.
El bug de fct_movimientos — por qué deploy no es lo mismo que run
fct_movimientos se cambió de materialized: table a materialized: view en
models/marts/fct_movimientos.sql durante el despliegue original, porque
materializarlo como tabla completa (join sobre 4.5M filas de movimiento)
agotó el disco del host a mitad de un dbt run (mismo tipo de límite que
documenta
Warehouse Postgres § movimiento
para el pipeline de ingesta, aplicado ahora al lado de transformación).
Después de ese cambio se corrió lightdash deploy --ignore-errors -y, que
completó sin error y dejó el dashboard “Inventario y Movimientos” funcionando
en apariencia. lightdash deploy internamente corre dbt compile (o
dbt list), no dbt run — regenera el manifest/SQL compilado que Lightdash
usa para construir sus “explores”, pero nunca ejecuta ese SQL contra Postgres.
La vista física analytics_marts.fct_movimientos nunca se creó. El error solo
apareció días después, en el dashboard ya público, al intentar consultar una
relación que Lightdash “sabía” que debía existir (estaba en el manifest) pero
que Postgres nunca vio:
Chart 'Movimientos por mes': Error
relation "analytics_marts.fct_movimientos" does not exist
Fix: dbt run --select fct_movimientos (0.54s, CREATE VIEW) + dbt run
completo sin selector para confirmar que los 20 modelos seguían consistentes
(PASS=20, 5 tablas + 15 vistas, 8.25s totales — rápido porque casi todo ya
estaba materializado, solo la vista faltante se creó de cero). Regla para
cualquier cambio de modelo dbt futuro en este proyecto: dbt run primero,
lightdash deploy después — nunca asumir que el segundo cubre al primero.
docker-compose.yml final (trivasa-bi-dev/lightdash/)
services:
lightdash-minio:
image: minio/minio:latest
container_name: lightdash-minio
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: ${MINIO_PASS}
volumes:
- lightdash-minio-data:/data
restart: unless-stopped
lightdash-minio-init:
image: minio/mc:latest
container_name: lightdash-minio-init
depends_on:
- lightdash-minio
entrypoint: >
/bin/sh -c "
until mc alias set local http://lightdash-minio:9000 minioadmin ${MINIO_PASS}; do sleep 1; done;
mc mb -p local/lightdash;
exit 0;
"
restart: "no"
lightdash-db:
image: pgvector/pgvector:pg15
container_name: lightdash-db
restart: unless-stopped
environment:
POSTGRES_USER: lightdash
POSTGRES_PASSWORD: ${PG_PASS}
POSTGRES_DB: lightdash
volumes:
- lightdash-db-data:/var/lib/postgresql/data
lightdash:
platform: linux/amd64
image: lightdash/lightdash:latest
container_name: lightdash
depends_on:
- lightdash-db
- lightdash-minio
environment:
- PGHOST=lightdash-db
- PGPORT=5432
- PGUSER=lightdash
- PGPASSWORD=${PG_PASS}
- PGDATABASE=lightdash
- SECURE_COOKIES=true
- TRUST_PROXY=true
- LIGHTDASH_SECRET=${LIGHTDASH_SECRET}
- PORT=8080
- SITE_URL=https://dash.frento.com.mx
- LIGHTDASH_INSTALL_TYPE=docker_image
- ALLOW_MULTIPLE_ORGS=false
- SCHEDULER_ENABLED=true
- S3_ENDPOINT=http://lightdash-minio:9000
- S3_REGION=us-east-1
- S3_BUCKET=lightdash
- S3_ACCESS_KEY=minioadmin
- S3_SECRET_KEY=${MINIO_PASS}
- S3_FORCE_PATH_STYLE=true
ports:
- "127.0.0.1:8090:8080"
restart: unless-stopped
networks:
- default
- postgres-warehouse_default
volumes:
lightdash-db-data:
lightdash-minio-data:
networks:
default: {}
postgres-warehouse_default:
external: true.env correspondiente (trivasa-bi-dev/lightdash/.env, permisos 600):
LIGHTDASH_SECRET, PG_PASS, MINIO_PASS (los tres generados con
openssl rand), ADMIN_PASS (contraseña del usuario admin, no reutilizada
por ningún otro servicio de este host).
Véase también
api-referencia.md— todos los endpoints REST usados para el setup inicial y la creación de dashboards, reversados desde el propio binario del contenedor (sin documentación pública a este nivel de detalle).- Lightdash — página de wiki que resume esto en prosa consultable.