Desplegar Baserow con Docker y publicarlo con ProxyPass desde Webhosting Management


#1

Introducción

Baserow es una alternativa libre a Airtable: una base de datos colaborativa con interfaz de hoja de cálculo, edición en tiempo real y API REST. A diferencia de la mayoría de aplicaciones que se despliegan en un hosting de Core-Admin, Baserow no es PHP: es un conjunto de servicios (Django, Celery, PostgreSQL, Redis y un frontend Nuxt) que necesitan ejecutarse de forma independiente.

Este artículo describe cómo desplegar Baserow como contenedor Docker escuchando únicamente en localhost, y publicarlo bajo un dominio con certificado usando la función Reverse proxy (ProxyPass) del módulo Webhosting Management de Core-Admin.

El mismo patrón sirve para cualquier otro servicio que no sea PHP y hable HTTP: Grafana, n8n, Metabase, Gitea, Uptime Kuma, etc. Los detalles específicos de Baserow están señalados como tales.

Requisitos previos

  • Core-Admin instalado con el módulo Webhosting Management activo
  • Docker instalado y funcionando en el servidor
  • Un alojamiento ya creado para el dominio que se va a usar (por ejemplo baserow.ejemplo.com), con su certificado SSL ya emitido (Let’s Encrypt o importado)
  • El dominio resolviendo a la IP del servidor
  • Acceso de administrador al panel de Core-Admin (la pestaña Site Options requiere permisos de administrador)
  • Un puerto local libre por encima de 1024. En los ejemplos se usa el 18267

Nota: el alojamiento no necesita contenido. Su document_root quedará vacío: sólo actuará de frontal HTTPS.

Por qué este planteamiento y no otro

Antes de entrar en materia conviene justificar la elección, porque hay varias formas de hacerlo y no todas son equivalentes.

Opción Valoración
Imagen todo-en-uno baserow/baserow + ProxyPass del alojamiento Recomendada. Un único contenedor, un único puerto, y el certificado, la redirección a HTTPS y los logs los sigue gestionando Core-Admin
docker-compose con los servicios separados Sólo si se va a escalar o se quiere el PostgreSQL fuera del contenedor. Más piezas que mantener y actualizar
Instalación manual sobre el sistema Desaconsejada: obliga a mantener a mano Python, Node, PostgreSQL, Redis y supervisord, y complica mucho las actualizaciones
Contenedor LXC dedicado (módulo LXC Management) + ProxyPass Alternativa razonable si el servidor ya trabaja con LXC y no se quiere introducir Docker. El esquema de publicación es idéntico

Sobre el módulo Docker Manager

Core-Admin incluye un módulo Docker Manager que lista contenedores, imágenes y volúmenes, y permite arrancarlos, pararlos y suspenderlos desde el panel. Es cómodo para el día a día y el contenedor que creemos a mano aparecerá en él automáticamente, porque el listado se obtiene de docker ps.

Sin embargo, para este despliegue conviene tener presentes dos limitaciones:

  • La entidad Web mapping de Docker Manager genera su propio VirtualHost de Apache con su propio Let’s Encrypt. Si el dominio ya tiene un alojamiento creado, ambas configuraciones entran en conflicto. Para este caso hay que publicar con el ProxyPass del alojamiento, no con el Web mapping.
  • El servicio de creación de contenedores construye el docker run con --name, -e, -p y --restart, pero no soporta volúmenes (-v). Cualquier servicio que necesite persistencia —Baserow la necesita— debe crearse desde línea de comandos.

Paso 1: desplegar el contenedor

Crear el volumen de datos

Baserow guarda absolutamente todo su estado en /baserow/data: la base de datos PostgreSQL, los ficheros subidos por los usuarios y los secretos generados en el primer arranque. Ese directorio debe ir en un volumen.

docker volume create baserow_data

Arrancar el contenedor

docker run -d --name baserow --restart unless-stopped --shm-size=1g -e BASEROW_PUBLIC_URL=https://baserow.ejemplo.com -e BASEROW_CADDY_ADDRESSES=http://:80 -e BASEROW_TRIGGER_SYNC_TEMPLATES_AFTER_MIGRATION=false -v baserow_data:/baserow/data -p 127.0.0.1:18267:80 baserow/baserow:2.3.3

Conviene detenerse en cada opción, porque casi todas resuelven un problema concreto:

Opción Motivo
-p 127.0.0.1:18267:80 Crítico por seguridad. Sin el 127.0.0.1, Docker inserta sus reglas en la cadena DOCKER y en nat PREROUTING, por delante de INPUT, y el servicio queda accesible desde internet saltándose el Firewall Manager de Core-Admin y sin cifrado
--shm-size=1g Imprescindible para las copias de seguridad. Docker asigna sólo 64 MB de memoria compartida, y Baserow construye los volcados de PostgreSQL en /dev/shm. Con la base vacía funciona; en cuanto haya datos reales el backup fallará con No space left on device
BASEROW_PUBLIC_URL URL pública del servicio. De aquí deriva Baserow su ALLOWED_HOSTS y las URL que genera
BASEROW_CADDY_ADDRESSES=http://:80 Evita que el Caddy interno intente gestionar su propio HTTPS. El TLS lo termina Apache
BASEROW_TRIGGER_SYNC_TEMPLATES_AFTER_MIGRATION=false Evita la sincronización de las ~157 plantillas de ejemplo, que consume varios minutos de CPU y disco en cada actualización. Omítelo si quieres disponer de las plantillas
--restart unless-stopped El contenedor vuelve a levantarse tras un reinicio del servidor
baserow/baserow:2.3.3 Fija siempre una versión concreta. Con latest una actualización de imagen puede cambiarte de versión sin pretenderlo

Nota sobre descargas: la imagen ronda los 600 MB repartidos en unas 25 capas. En servidores con salida inestable es habitual que el docker pull se corte con connection reset by peer. Reintentar retoma donde se quedó; si ocurre a menudo, {"max-concurrent-downloads": 1} en /etc/docker/daemon.json hace las descargas más lentas pero mucho más fiables.

Seguir el primer arranque

El primer arranque crea el PostgreSQL interno y ejecuta todas las migraciones. Puede tardar varios minutos:

docker logs -f baserow

Sal con Ctrl+C cuando deje de moverse (eso no para el contenedor) y espera a que el healthcheck pase a healthy:

docker ps --filter name=baserow --format '{{.Status}} | {{.Ports}}'

Debe mostrar Up X minutes (healthy) | 127.0.0.1:18267->80/tcp. Ese 127.0.0.1 en la columna de puertos es la confirmación de que el servicio no está expuesto.

Comprobar que responde

curl -s -o /dev/null -w "%{http_code}\n" -H "Host: baserow.ejemplo.com" http://127.0.0.1:18267/
curl -s -H "Host: baserow.ejemplo.com" http://127.0.0.1:18267/api/settings/ | head -c 200; echo

El primero debe devolver 302 (redirección al login) y el segundo un JSON con la configuración de la instancia.

Muy importante: la cabecera -H "Host: ..." no es un adorno. Si haces la prueba contra 127.0.0.1 sin ella, obtendrás 404 en toda la aplicación, incluida la API. No es un fallo: el Caddy interno enruta según el Host, y con un Host desconocido lo manda todo al frontend. Esto tiene una consecuencia directa en el siguiente paso.

Paso 2: preparar el alojamiento

Estos tres ajustes se aplican desde línea de comandos sobre el alojamiento ya existente.

Desactivar el motor PHP

El alojamiento sólo va a hacer de proxy, así que no tiene sentido mantener un pool PHP-FPM levantado. Además elimina toda la superficie de ataque de PHP en ese sitio:

crad-webhosting-mgr.pyc --set-php-engine baserow.ejemplo.com off

Forzar HTTPS

crad-webhosting-mgr.pyc --set-option baserow.ejemplo.com redirect_http_to_https 1

Ampliar el tiempo de espera del proxy

Las importaciones y exportaciones grandes de Baserow pueden superar los 300 segundos por defecto:

crad-webhosting-mgr.pyc --set-option baserow.ejemplo.com proxy_timeout 600

Paso 3: crear el ProxyPass

Esta parte se hace desde el panel web. No existe comando de línea para las reglas de ProxyPass.

  1. Inicia sesión en el panel de Core-Admin
  2. Entra en el módulo Webhosting Management
  3. Selecciona el alojamiento baserow.ejemplo.com
  4. Abre la pestaña Reverse proxy
  5. Pulsa Add Proxy pass

Rellena la ficha con estos valores:

Campo Valor
Path /
Remote URL http://127.0.0.1:18267
Is active activado
Preserve host header activado
Skip peer SSL certificate check desactivado (el backend es HTTP, no aplica)
Bypass folders vacío

Preserve host header es obligatorio aquí, no opcional. Sin esta casilla Apache envía Host: 127.0.0.1:18267 al backend y, como vimos en el paso anterior, Baserow responde 404 en toda la aplicación. El panel advierte de que la opción afecta a todos los ProxyPass del alojamiento; si sólo hay una regla, como es el caso, no tiene efectos colaterales.

Al guardar, Core-Admin regenera la configuración del VirtualHost, activa los módulos de proxy necesarios (proxy, proxy_http, proxy_wstunnel, ssl…) y recarga Apache automáticamente. No hay que hacer nada más a mano.

Paso 4: WebSockets y cabeceras de protocolo

Este paso es el que más se olvida, y sin él Baserow parece funcionar pero se comporta de forma extraña.

El generador de configuración de Core-Admin escribe directivas ProxyPass y ProxyPassReverse planas. Eso basta para HTTP, pero no cubre dos cosas:

  • WebSockets: Baserow usa /ws/ para la colaboración en tiempo real, las notificaciones y el progreso de las importaciones. Sin proxy de WebSocket, la aplicación carga pero se queda reconectando: los cambios hechos en una pestaña no aparecen en otra sin recargar.
  • Protocolo original: mod_proxy_http añade X-Forwarded-For, X-Forwarded-Host y X-Forwarded-Server, pero no X-Forwarded-Proto. Sin ella, Django puede generar enlaces y redirecciones en http://.

La solución limpia, sin dejar de que Core-Admin gestione el VirtualHost, es la opción de sitio custom_apache2_site_definitions_before, cuyo contenido se escribe antes de las reglas de ProxyPass. Como Apache resuelve por orden de aparición, la regla de /ws/ gana sobre la de /.

  1. En el alojamiento, abre la pestaña Site Options (requiere permisos de administrador)
  2. Localiza la opción custom_apache2_site_definitions_before
  3. Introduce este contenido:
ProxyPass "/ws/" "ws://127.0.0.1:18267/ws/"
ProxyPassReverse "/ws/" "ws://127.0.0.1:18267/ws/"
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-Port "443"

Los módulos mod_proxy_wstunnel y mod_headers ya los activa Core-Admin al crear el ProxyPass, no hay que hacer nada más.

Nota: estas directivas se aplican también al VirtualHost del puerto 80. Por eso se combinan con redirect_http_to_https=1 del paso 2: así por HTTP nunca llega a servirse tráfico real.

Paso 5: verificación

Comprueba que la configuración generada tiene el orden correcto:

grep -n "ws://\|ProxyPass \"" /etc/apache2/sites-enabled/baserow.ejemplo.com*.conf

Las líneas de /ws/ deben tener números de línea menores que las de ProxyPass "/", en los dos VirtualHost (el de :80 y el de :443). Algo así:

10:      ProxyPass "/ws/" "ws://127.0.0.1:18267/ws/"
11:      ProxyPassReverse "/ws/" "ws://127.0.0.1:18267/ws/"
17:    ProxyPass "/" "http://127.0.0.1:18267/"
71:      ProxyPass "/ws/" "ws://127.0.0.1:18267/ws/"
72:      ProxyPassReverse "/ws/" "ws://127.0.0.1:18267/ws/"
78:    ProxyPass "/" "http://127.0.0.1:18267/"

Y que el servicio responde a través de Apache:

curl -s -o /dev/null -w "%{http_code}\n" https://baserow.ejemplo.com/
curl -s https://baserow.ejemplo.com/api/settings/ | head -c 200; echo

La comprobación definitiva del WebSocket sólo puede hacerse desde el navegador: abre el sitio, pulsa F12, ve a Network y filtra por WS. La conexión a wss://baserow.ejemplo.com/ws/core/ debe aparecer como 101 Switching Protocols y mantenerse abierta. Si en su lugar ves reconexiones continuas, revisa el orden del paso 4.

Paso 6: primer acceso y cierre del registro

Recién instalado, Baserow permite que cualquiera se registre. El sitio ya es público, así que este paso no admite demora:

  1. Entra en https://baserow.ejemplo.com/signup y crea tu cuenta. La primera cuenta es la de administrador
  2. Ve a Admin → Settings y desactiva Allow new signups
  3. Si necesitas dar de alta a más gente, usa las invitaciones a espacio de trabajo

Puedes verificar el estado en cualquier momento:

curl -s https://baserow.ejemplo.com/api/settings/ | head -c 120; echo

El campo allow_new_signups debe estar a false.

Copias de seguridad

Este es el punto donde más despliegues se quedan a medias, porque no resulta evidente: el backup de alojamiento de Core-Admin no cubre nada de esto. Ese backup respalda el document_root, que en este montaje está vacío. Los datos viven en el volumen de Docker.

Qué hay que respaldar, y cómo

El volumen contiene dos cosas con necesidades distintas:

Contenido Ruta en el volumen Método
Base de datos PostgreSQL /baserow/data/postgres Volcado lógico, nunca copia en crudo
Ficheros subidos por los usuarios /baserow/data/media Copia directa (no se reescriben una vez subidos)

La documentación oficial de Baserow propone parar el contenedor y hacer un tar del volumen entero. Es correcto, pero implica cortar el servicio en cada copia. Y copiar en caliente el directorio postgres sin parar es aún peor: produce un backup silenciosamente corrupto, porque los ficheros de datos cambian mientras se leen.

La alternativa sin cortes es el volcado lógico que la propia imagen expone, y que sí produce una instantánea consistente:

docker exec baserow ./baserow.sh backend-cmd backup -f /baserow/data/backups/prueba.tar.gz

Si termina con Successfully backed up Baserow, ya tienes el mecanismo. (Aquí es donde importa el --shm-size=1g del paso 1.)

Script de copia diaria

Guarda esto como /usr/local/bin/crad-baserow-backup.sh con permisos 0700:

#!/bin/bash
# Baserow backup: hot PostgreSQL dump + uploaded media, without stopping the service.
set -u

CONTAINER="baserow"
DEST="/var/backups/baserow"
KEEP_DAYS=7
STAMP=$(date +%Y%m%d-%H%M%S)
LOG="/var/log/baserow-backup.log"

log () { echo "$(date '+%F %T') $*" >> "$LOG"; }

mkdir -p "$DEST" && chmod 0700 "$DEST" || exit 1

# the hot dump requires a running container
if [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null)" != "true" ]; then
    log "ERROR: container $CONTAINER is not running, aborting"
    exit 1
fi

DATA=$(docker volume inspect -f '{{.Mountpoint}}' baserow_data 2>/dev/null)
if [ -z "$DATA" ] || [ ! -d "$DATA" ]; then
    log "ERROR: unable to resolve baserow_data mountpoint"
    exit 1
fi

# 1. consistent logical dump of PostgreSQL
docker exec "$CONTAINER" mkdir -p /baserow/data/backups >> "$LOG" 2>&1
if ! docker exec "$CONTAINER" ./baserow.sh backend-cmd backup \
        -f "/baserow/data/backups/db-$STAMP.tar.gz" >> "$LOG" 2>&1; then
    log "ERROR: database dump failed"
    exit 1
fi

# 2. move it out of the very volume it is backing up
if ! mv "$DATA/backups/db-$STAMP.tar.gz" "$DEST/"; then
    log "ERROR: unable to move the dump out of the volume"
    exit 1
fi
chown root:root "$DEST/db-$STAMP.tar.gz"
chmod 0600 "$DEST/db-$STAMP.tar.gz"

# 3. uploaded files: safe to copy hot, they are never rewritten
if [ -d "$DATA/media" ]; then
    tar -C "$DATA" -czf "$DEST/media-$STAMP.tar.gz" media 2>> "$LOG"
    chmod 0600 "$DEST/media-$STAMP.tar.gz"
else
    log "INFO: no media directory yet, skipping uploaded files"
fi

# 4. rotation
find "$DEST" -maxdepth 1 -name 'db-*.tar.gz'    -mtime +$KEEP_DAYS -delete
find "$DEST" -maxdepth 1 -name 'media-*.tar.gz' -mtime +$KEEP_DAYS -delete

log "OK: db-$STAMP.tar.gz $(du -h "$DEST/db-$STAMP.tar.gz" 2>/dev/null | cut -f1)"

El paso 2 del script no es un detalle menor: una copia guardada dentro del volumen que respalda no es una copia. Los ficheros terminan en /var/backups/baserow, fuera de Docker.

Programación y retención

Programa la ejecución antes de que pase tu sistema de copias, no a una hora arbitraria. Si tu trabajo de Bacula arranca a las 23:55:

echo '30 23 * * *  root  /usr/local/bin/crad-baserow-backup.sh' > /etc/cron.d/baserow-backup && chmod 0644 /etc/cron.d/baserow-backup

Un volcado a las 03:15 cuando la copia externa pasa a las 23:55 significa que cada cinta se lleva el volcado del día anterior: un día entero de datos perdidos en una restauración.

La retención local de 7 días sólo tiene que cubrir un par de fallos seguidos; el histórico largo lo guarda el sistema de copias.

Integración con el sistema de copias

Comprueba que /var/backups/baserow entra en el FileSet de tu cliente de backup. Con Bacula, el FileSet que genera Core-Admin para un servidor gestionado incluye /, /boot, /var, /dev y /storage, así que normalmente queda cubierto sin tocar nada. Puedes verificarlo sobre un trabajo Full desde el Director:

crad-bacula-mgr.pyc --list-client-jobs <cliente>-fd --level Full --limit 5
crad-bacula-mgr.pyc --browse-job-files <JobId> --catalog <catalogo> --directory /var/backups/

Importante: en Directores que gestionan muchos clientes, los JobId se repiten entre catálogos. Pasa siempre --catalog (el nombre aparece en la columna Catalog del listado de trabajos); de lo contrario puedes acabar consultando el trabajo de otro cliente y obtener un listado vacío sin ninguna pista de por qué.

Considera excluir /var/lib/docker

Al introducir Docker en el servidor, el backup pasa a llevarse también las capas de imagen (varios GB) y el directorio de datos crudo de PostgreSQL del contenedor. Ese segundo punto es el problemático: es una copia inconsistente, inservible y engañosa, porque alguien podría intentar restaurar desde ahí en una urgencia. Las imágenes, por su parte, se vuelven a descargar.

Valora excluir /var/lib/docker del FileSet: el respaldo bueno es el volcado lógico del script.

Prueba la restauración

Un backup no probado no es un backup. La restauración puede ensayarse en un volumen desechable, sin tocar producción:

docker volume create baserow_restore_test
docker run -it --rm -v /var/backups/baserow:/backup -v baserow_restore_test:/baserow/data baserow/baserow:2.3.3 backend-cmd-with-db restore -f /backup/db-XXXXXXXX-XXXXXX.tar.gz
docker volume rm baserow_restore_test

Actualizaciones

docker pull baserow/baserow:<nueva-version>
docker stop baserow && docker rm baserow

Vuelve a lanzar el docker run del paso 1 cambiando la etiqueta de versión y manteniendo el mismo volumen. Las migraciones de base de datos las ejecuta la imagen al arrancar.

Recomendaciones:

  • Ejecuta el script de backup antes de actualizar
  • Cambia de versión de una en una dentro de la rama, y lee las notas de publicación antes de saltar de rama mayor
  • No hay que tocar nada en Core-Admin: el puerto y el Host no cambian, así que el ProxyPass sigue siendo válido

Detalles técnicos

Qué genera Core-Admin en el VirtualHost

La regla de ProxyPass creada desde el panel produce, dentro de cada VirtualHost del alojamiento:

    # Proxy pass from core-admin panel
    # ProxyPass from rule-id=5
    ProxyPass "/" "http://127.0.0.1:18267/"
    ProxyPassReverse "/" "http://127.0.0.1:18267/"
    # Preserve host header as indicated by caller
    ProxyPreserveHost On

Las reglas se ordenan automáticamente de la ruta más específica a la más general. Si la URL remota empieza por https://, Core-Admin añade además SSLProxyEngine on y, si se marca la casilla correspondiente, SSLProxyCheckPeerCN off y SSLProxyCheckPeerExpire off.

El campo Bypass folders

La ficha de ProxyPass tiene un campo Bypass folders en la pestaña Advanced que acepta una lista de carpetas separadas por comas. Genera declaraciones del tipo:

    ProxyPass "/static" "!"

Sirve para que ciertas rutas se sirvan directamente desde el document_root del alojamiento en lugar de ir al backend. Con Baserow no hace falta, porque el contenedor sirve sus propios estáticos, pero es útil con otras aplicaciones.

Nunca actives Custom site config en este alojamiento

En la pestaña Custom site config existe una casilla para tomar el control manual de la configuración del sitio. Si se activa, Core-Admin marca el alojamiento con # core_admin_dont_modify y deja de gestionar el VirtualHost: a partir de ahí, ni el ProxyPass del panel ni las opciones de sitio se aplicarán. Para este despliegue no es necesaria en ningún momento.

Cuota de disco y estadísticas

El volumen de Docker no cuenta contra la cuota de disco del alojamiento, y su crecimiento no aparece en las estadísticas del panel. Conviene tenerlo presente al dimensionar el servidor.

Monitorización

El checker de Apache no sabe si el contenedor está vivo: si Baserow se cae, Apache devolverá 503 y el sitio seguirá “respondiendo”. Merece la pena configurar una comprobación HTTP contra https://baserow.ejemplo.com/api/settings/ para detectarlo.

Preguntas frecuentes

Baserow carga, pero los cambios de una pestaña no aparecen en otra sin recargar.
Falta el proxy de WebSocket, o las reglas de /ws/ han quedado por debajo del ProxyPass "/". Revisa el paso 4 y comprueba el orden con el grep del paso 5.

Obtengo 404 en toda la aplicación, incluida la API.
Casi con seguridad falta marcar Preserve host header en la ficha del ProxyPass. El Caddy interno de Baserow enruta según la cabecera Host.

curl contra 127.0.0.1:18267 me devuelve 404, ¿está mal el despliegue?
No. Prueba con -H "Host: baserow.ejemplo.com". Sin la cabecera correcta el 404 es el comportamiento esperado.

El backup falla con No space left on device.
El contenedor se creó sin --shm-size. Baserow construye el volcado en /dev/shm, al que Docker asigna 64 MB por defecto. Recrea el contenedor con --shm-size=1g; los datos están en el volumen y no se pierde nada. Verifica con docker exec baserow df -h /dev/shm.

El arranque tarda muchísimo y el log habla de plantillas.
Es la sincronización de las ~157 plantillas de ejemplo. Añade -e BASEROW_TRIGGER_SYNC_TEMPLATES_AFTER_MIGRATION=false al recrear el contenedor.

¿Puedo usar el mismo alojamiento para servir además contenido PHP?
Técnicamente sí, con un Path distinto de / y usando Bypass folders, pero no es recomendable: mezcla dos cosas con ciclos de vida y superficies de ataque distintas. Es preferible un alojamiento por servicio.

¿Sirve esto para otros servicios?
Sí. Grafana, n8n, Metabase, Gitea, Uptime Kuma y en general cualquier servicio que hable HTTP encajan en este mismo patrón. Lo único que cambia es el puerto, las variables de entorno del contenedor y si necesita o no WebSockets.

¿Y si prefiero no instalar Docker en el servidor?
Usa el módulo LXC Management para levantar un contenedor de sistema con el servicio dentro, y publícalo con el mismo ProxyPass apuntando a la IP interna del contenedor.

Resumen

  1. docker volume create y docker run con -p 127.0.0.1:<puerto>:80 y --shm-size=1g
  2. --set-php-engine off, redirect_http_to_https 1 y proxy_timeout 600 sobre el alojamiento
  3. ProxyPass /http://127.0.0.1:<puerto> con Preserve host header
  4. /ws/ y X-Forwarded-Proto en custom_apache2_site_definitions_before
  5. Crear la cuenta de administrador y cerrar el registro público
  6. Script de volcado lógico diario a /var/backups/, programado antes de la copia externa

Los dos puntos que más problemas dan y que no son evidentes: Preserve host header (sin él, 404 en toda la aplicación) y --shm-size=1g (sin él, el backup falla justo cuando hay datos que perder).